summaryrefslogtreecommitdiffstats
path: root/smartproxy/ipa-smartproxy
diff options
context:
space:
mode:
authorRob Crittenden <rcritten@redhat.com>2013-12-03 09:14:00 -0700
committerRob Crittenden <rcritten@redhat.com>2014-02-27 15:50:37 -0500
commit4facb9d8ceea6ffe07297f375bf05d9c72bc6125 (patch)
tree44bd9f9645f87dccd84da37ccae0e2c109cd64c3 /smartproxy/ipa-smartproxy
parentadcd373931c50d91550f6b74b191d08ecce5b137 (diff)
downloadfreeipa.git-master.tar.gz
freeipa.git-master.tar.xz
freeipa.git-master.zip
Implement an IPA Foreman smartproxy serverHEADmaster
This currently server supports only host and hostgroup commands for retrieving, adding and deleting entries. The incoming requests are completely unauthenticated and by default requests must be local. Utilize GSS-Proxy to manage the TGT. Configuration information is in the ipa-smartproxy man page. Design: http://www.freeipa.org/page/V3/Smart_Proxy
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)