summaryrefslogtreecommitdiffstats
path: root/smartproxy/ipa-smartproxy
diff options
context:
space:
mode:
Diffstat (limited to 'smartproxy/ipa-smartproxy')
-rwxr-xr-xsmartproxy/ipa-smartproxy336
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)