diff options
Diffstat (limited to 'smartproxy/ipa-smartproxy')
-rwxr-xr-x | smartproxy/ipa-smartproxy | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/smartproxy/ipa-smartproxy b/smartproxy/ipa-smartproxy new file mode 100755 index 00000000..d5e8f227 --- /dev/null +++ b/smartproxy/ipa-smartproxy @@ -0,0 +1,336 @@ +#!/usr/bin/python2 -E +# +# Authors: +# Rob Crittenden <rcritten@redhat.com> +# +# Copyright (C) 2014 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import os +import cherrypy +import json +import syslog +import pwd +from optparse import OptionParser +from functools import wraps +from cherrypy import response +from cherrypy.process import plugins +from ipalib import api +from ipalib import errors +from ipalib import util +from ipaserver.rpcserver import json_encode_binary +from ipapython.version import VERSION + + +def jsonout(func): + '''JSON output decorator''' + @wraps(func) + def wrapper(*args, **kw): + value = func(*args, **kw) + response.headers["Content-Type"] = "application/json;charset=utf-8" + data = json_encode_binary(value) + return json.dumps(data, sort_keys=True, indent=2) + + return wrapper + + +def handle_error(status, message, traceback, version): + """ + Return basic messages to user and log backtrace in case of 500 + error. + """ + if status.startswith('500'): + cherrypy.log(msg=message + '\n', context='IPA', traceback=True) + + resp = cherrypy.response + resp.headers['Content-Type'] = 'application/json' + return json.dumps({'status': status, 'message': message}) + + +def convert_unicode(value): + """ + IPA requires all incoming values to be unicode. Recursively + convert the values. + """ + if not isinstance(value, basestring): + return value + + if value is not None: + return unicode(value) + else: + return None + + +def Command(command, *args, **options): + if (cherrypy.request.config.get('local_only', False) and + cherrypy.request.remote.ip not in ['::1', '127.0.0.1']): + raise IPAError( + status=401, + message="Not a local request" + ) + + if not api.Backend.rpcclient.isconnected(): + api.Backend.rpcclient.connect() + try: + if not api.Backend.rpcclient.isconnected(): + api.Backend.rpcclient.connect() + except errors.CCacheError, e: + raise IPAError( + status=401, + message=e + ) + + # IPA wants all its strings as unicode + args = map(lambda v: convert_unicode(v), args) + options = dict(zip(options, map(convert_unicode, options.values()))) + + params = api.Command[command].args_options_2_params(*args, **options) + cherrypy.log(context='IPA', msg='%s(%s)' % + (command, ', '.join(api.Command[command]._repr_iter(**params)))) + try: + return api.Command[command](*args, **options)['result'] + except (errors.DuplicateEntry, errors.DNSNotARecordError, + errors.ValidationError, errors.ConversionError,) as e: + raise IPAError( + status=400, + message=e + ) + except errors.ACIError, e: + raise IPAError( + status=401, + message=e + ) + except errors.NotFound, e: + raise IPAError( + status=404, + message=e + ) + except Exception, e: + raise IPAError( + status=500, + message=e + ) + + +@jsonout +def GET(command, *args, **options): + return Command(command, *args, **options) + + +@jsonout +def POST(command, *args, **options): + cherrypy.response.status = 201 + return Command(command, *args, **options) + + +@jsonout +def DELETE(command, *args, **options): + return Command(command, *args, **options) + + +class IPAError(cherrypy.HTTPError): + """ + Return errors in IPA-style json. + + Local errors are treated as strings so do not include the code and + name attributes within the error dict. + + This is not padded for IE. + """ + + def set_response(self): + response = cherrypy.serving.response + + cherrypy._cperror.clean_headers(self.code) + + # In all cases, finalize will be called after this method, + # so don't bother cleaning up response values here. + response.status = self.status + + if isinstance(self._message, Exception): + error = {'code': self._message.errno, + 'message': self._message.message, + 'name': self._message.__class__.__name__} + elif isinstance(self._message, basestring): + error = {'message': self._message} + else: + error = {'message': + 'Unable to handle error message type %s' % type(self._message)} + + response.body = json.dumps({'error': error, + 'id': 0, + 'principal': util.get_current_principal(), + 'result': None, + 'version': VERSION}, + sort_keys=True, indent=2) + + +class Host(object): + + exposed = True + + def GET(self, fqdn=None): + + if fqdn is None: + command = 'host_find' + else: + command = 'host_show' + + return GET(command, fqdn) + + def POST(self, hostname, description=None, random=False, + macaddress=None, userclass=None, ip_address=None, + password=None): + return POST('host_add', hostname, + description=description, random=random, + force=True, macaddress=macaddress, + userclass=userclass, ip_address=ip_address, + userpassword=password) + + def DELETE(self, fqdn): + return DELETE('host_del', fqdn) + + +class Hostgroup(object): + + exposed = True + + def GET(self, name=None): + + if name is None: + command = 'hostgroup_find' + else: + command = 'hostgroup_show' + + return GET(command, name) + + def POST(self, name=None, description=None): + cherrypy.response.status = 201 + return POST('hostgroup_add', name, + description=description,) + + def DELETE(self, name): + return DELETE('hostgroup_del', name) + +class Features(object): + exposed = True + + def GET(self): + return '["realm"]' + + +def start(config=None, daemonize=False, pidfile=None): + # Set the umask so only the owner can read the log files + old_umask = os.umask(077) + + cherrypy.tree.mount( + Features(), '/features', + {'/': + {'request.dispatch': cherrypy.dispatch.MethodDispatcher()} + } + ) + cherrypy.tree.mount( + Host(), '/ipa/smartproxy/host', + {'/': + {'request.dispatch': cherrypy.dispatch.MethodDispatcher()} + } + ) + cherrypy.tree.mount( + Hostgroup(), '/ipa/smartproxy/hostgroup', + {'/': + {'request.dispatch': cherrypy.dispatch.MethodDispatcher()} + } + ) + + api.bootstrap(context='ipasmartproxy') + api.finalize() + + # Register the domain for requests from Foreman + cherrypy.tree.mount( + Host(), '/realm/%s' % api.env.domain, + {'/': + {'request.dispatch': cherrypy.dispatch.MethodDispatcher()} + } + ) + + for c in config or []: + try: + cherrypy.config.update(c) + except (IOError, OSError), e: + cherrypy.log(msg="Exception trying to load %s: %s" % (c, e), + context='IPA', traceback=False) + return 1 + + # Log files are created, reset umask + os.umask(old_umask) + + user = cherrypy.config.get('user', None) + if user is None: + cherrypy.log(msg="User is required", context='IPA', traceback=False) + return 1 + pent = pwd.getpwnam(user) + + if daemonize: + cherrypy.config.update({'log.screen': False}) + plugins.Daemonizer(cherrypy.engine).subscribe() + + if pidfile: + plugins.PIDFile(cherrypy.engine, pidfile).subscribe() + + cherrypy.engine.signal_handler.subscribe() + + cherrypy.config.update({'error_page.500': handle_error}) + + # If you don't use GSS-Proxy you're on your own in ensuring that + # there is always a valid ticket for the smartproxy to use. + if cherrypy.config.get('use_gssproxy', False): + cherrypy.log(msg="Enabling GSS-Proxy", context='IPA') + os.environ['GSS_USE_PROXY'] = '1' + + if os.geteuid() == 0: + cherrypy.log(msg="Dropping root privileges to %s" % user, + context='IPA') + plugins.DropPrivileges(cherrypy.engine, + uid=pent.pw_uid, + gid=pent.pw_gid).subscribe() + + try: + cherrypy.engine.start() + except Exception: + return 1 + else: + cherrypy.engine.block() + + return 0 + +if __name__ == '__main__': + p = OptionParser() + p.add_option('-c', '--config', action="append", dest='config', + help="specify config file(s)") + p.add_option('-d', action="store_true", dest='daemonize', + help="run the server as a daemon") + p.add_option('-p', '--pidfile', dest='pidfile', default=None, + help="store the process id in the given file") + options, args = p.parse_args() + + try: + sys.exit(start(options.config, options.daemonize, options.pidfile)) + except Exception, e: + cherrypy.log(msg="Exception trying to start: %s" % e, + context='IPA', + traceback=True) + syslog.syslog(syslog.LOG_ERR, "Exception trying to start: %s" % e) |