From: Simo Sorce Date: Sat, 5 Apr 2014 17:23:02 +0000 (-0400) Subject: Add basic installation script with saml support X-Git-Tag: v0.2.2~27 X-Git-Url: http://git.cascardo.info/?p=cascardo%2Fipsilon.git;a=commitdiff_plain;h=a0374da67060c6e69ff6f1c2d25d2df357c25751;ds=sidebyside Add basic installation script with saml support Generates (self signed) certificates and a metdata.xml file. Optionally configures an Apache Httpd server. If the admin does not configure a specific application at install time a default landing page is made available to be able to test that the SP configuration works. Uninstall removes all certificates and metadata file and is irreversible. --- diff --git a/ipsilon/install/ipsilon-client-install b/ipsilon/install/ipsilon-client-install new file mode 100755 index 0000000..8802ea1 --- /dev/null +++ b/ipsilon/install/ipsilon-client-install @@ -0,0 +1,259 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 Simo Sorce +# +# 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 . + +from ipsilon.tools.saml2metadata import Metadata +from ipsilon.tools.saml2metadata import SAML2_NAMEID_MAP +from ipsilon.tools.saml2metadata import SAML2_SERVICE_MAP +from ipsilon.tools.certs import Certificate +from string import Template +import argparse +import logging +import os +import pwd +import requests +import shutil +import socket +import sys + + +HTTPDCONFD = '/etc/httpd/conf.d' +SAML2_TEMPLATE = '/usr/share/ipsilon/templates/install/saml2/sp.conf' +SAML2_CONFFILE = '/etc/httpd/conf.d/ipsilon-saml.conf' +SAML2_HTTPDIR = '/etc/httpd/saml2' +SAML2_PROTECTED = '/saml2protected' + +#Installation arguments +args = dict() + +# Regular logging +logger = logging.getLogger() + + +def openlogs(): + global logger # pylint: disable=W0603 + logger = logging.getLogger() + lh = logging.StreamHandler(sys.stderr) + logger.addHandler(lh) + + +def saml2(): + logger.info('Installing SAML2 Service Provider') + + if args['saml_idp_metadata'] is None: + #TODO: detect via SRV records ? + raise ValueError('An IDP metadata file/url is required.') + + idpmeta = None + + try: + if os.path.exists(args['saml_idp_metadata']): + with open(args['saml_idp_metadata']) as f: + idpmeta = f.read() + elif args['saml_idp_metadata'].startswith('file://'): + with open(args['saml_idp_metadata'][7:]) as f: + idpmeta = f.read() + else: + r = requests.get(args['saml_idp_metadata']) + r.raise_for_status() + idpmeta = r.content + except Exception, e: # pylint: disable=broad-except + logger.error("Failed to retrieve IDP Metadata file!\n" + + "Error: [%s]" % repr(e)) + raise + + path = None + if args['saml_httpd']: + path = os.path.join(SAML2_HTTPDIR, args['hostname']) + os.makedirs(path, 0750) + else: + path = os.getcwd() + + url = 'https://' + args['hostname'] + url_sp = url + args['saml_sp'] + url_logout = url + args['saml_sp_logout'] + url_post = url + args['saml_sp_post'] + + # Generate metadata + m = Metadata('sp') + c = Certificate(path) + c.generate('certificate', args['hostname']) + m.set_entity_id(url_sp) + m.add_certs(c) + m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout) + m.add_service(SAML2_SERVICE_MAP['response-post'], url_post, index="0") + sp_metafile = os.path.join(path, 'metadata.xml') + m.output(sp_metafile) + + if args['saml_httpd']: + idp_metafile = os.path.join(path, 'idp-metadata.xml') + with open(idp_metafile, 'w+') as f: + f.write(idpmeta) + + saml_protect = 'auth' + saml_auth='' + if args['saml_base'] != args['saml_auth']: + saml_protect = 'info' + saml_auth = '\n' \ + ' MellonEnable "auth"\n' \ + '\n' % args['saml_auth'] + + psp = '# ' + if args['saml_auth'] == SAML2_PROTECTED: + # default location, enable the default page + psp = '' + + with open(SAML2_TEMPLATE) as f: + template = f.read() + t = Template(template) + hunk = t.substitute(saml_base=args['saml_base'], + saml_protect=saml_protect, + saml_sp_key=c.key, + saml_sp_cert=c.cert, + saml_sp_meta=sp_metafile, + saml_idp_meta=idp_metafile, + saml_sp=args['saml_sp'], + saml_auth=saml_auth, sp=psp) + + with open(SAML2_CONFFILE, 'w+') as f: + f.write(hunk) + + pw = pwd.getpwnam(args['httpd_user']) + for root, dirs, files in os.walk(SAML2_HTTPDIR): + for name in dirs: + target = os.path.join(root, name) + os.chown(target, pw.pw_uid, pw.pw_gid) + os.chmod(target, 0700) + for name in files: + target = os.path.join(root, name) + os.chown(target, pw.pw_uid, pw.pw_gid) + os.chmod(target, 0600) + + logger.info('SAML Service Provider configured.') + logger.info('You should be able to restart the HTTPD server and' + + ' then access it at %s%s' % (url, args['saml_auth'])) + else: + logger.info('SAML Service Provider configuration ready.') + logger.info('Use the certificate, key and metadata.xml files to' + + ' configure your Service Provider') + + +def install(): + if args['saml']: + saml2() + + +def saml2_uninstall(): + try: + shutil.rmtree(os.path.join(SAML2_HTTPDIR, args['hostname'])) + except Exception, e: # pylint: disable=broad-except + log_exception(e) + try: + os.remove(SAML2_CONFFILE) + except Exception, e: # pylint: disable=broad-except + log_exception(e) + + +def uninstall(): + logger.info('Uninstalling Service Provider') + #FXIME: ask confirmation + saml2_uninstall() + logger.info('Uninstalled SAML2 data') + + +def log_exception(e): + if 'debug' in args and args['debug']: + logger.exception(e) + else: + logger.error(e) + + +def parse_args(): + global args + + fc = argparse.ArgumentDefaultsHelpFormatter + parser = argparse.ArgumentParser(description='Client Install Options', + formatter_class=fc) + parser.add_argument('--version', + action='version', version='%(prog)s 0.1') + parser.add_argument('--hostname', default=socket.getfqdn(), + help="Machine's fully qualified host name") + parser.add_argument('--admin-user', default='admin', + help="Account allowed to create a SP") + parser.add_argument('--httpd-user', default='apache', + help="Web server account used to read certs") + parser.add_argument('--saml', action='store_true', default=False, + help="Whether to install a saml2 SP") + parser.add_argument('--saml-idp-metadata', default=None, + help="A URL pointing at the IDP Metadata (FILE or HTTP)") + parser.add_argument('--saml-httpd', action='store_true', default=False, + help="Automatically configure httpd") + parser.add_argument('--saml-base', default='/', + help="Where saml2 authdata is available") + parser.add_argument('--saml-auth', default=SAML2_PROTECTED, + help="Where saml2 authentication is enforced") + parser.add_argument('--saml-sp', default='/saml2', + help="Where saml communication happens") + parser.add_argument('--saml-sp-logout', default='/saml2/logout', + help="Single Logout URL") + parser.add_argument('--saml-sp-post', default='/saml2/postResponse', + help="Post response URL") + parser.add_argument('--debug', action='store_true', default=False, + help="Turn on script debugging") + parser.add_argument('--uninstall', action='store_true', + help="Uninstall the server and all data") + + args = vars(parser.parse_args()) + + if len(args['hostname'].split('.')) < 2: + raise ValueError('Hostname: %s is not a FQDN.') + + # At least one on this list needs to be specified or we do nothing + sp_list = ['saml'] + present = False + for sp in sp_list: + if args[sp]: + present = True + if not present and not args['uninstall']: + raise ValueError('Nothing to install, please select a Service type.') + + +if __name__ == '__main__': + out = 0 + openlogs() + try: + parse_args() + + if 'uninstall' in args and args['uninstall'] is True: + uninstall() + + install() + except Exception, e: # pylint: disable=broad-except + log_exception(e) + if 'uninstall' in args and args['uninstall'] is True: + print 'Uninstallation aborted.' + else: + print 'Installation aborted.' + out = 1 + finally: + if out == 0: + if 'uninstall' in args and args['uninstall'] is True: + print 'Uninstallation complete.' + else: + print 'Installation complete.' + sys.exit(out) diff --git a/setup.py b/setup.py index 3de7faa..8b0b042 100755 --- a/setup.py +++ b/setup.py @@ -36,14 +36,19 @@ setup( (DATA+'ui/css', glob('ui/css/*.css')), (DATA+'ui/img', glob('ui/img/*')), (DATA+'ui/js', glob('ui/js/*.js')), + (DATA+'ui/saml2sp', glob('ui/saml2sp/*.html')), (DATA+'templates', glob('templates/*.html')), (DATA+'templates/admin', glob('templates/admin/*.html')), (DATA+'templates/login', glob('templates/login/*.html')), (DATA+'templates/saml2', glob('templates/saml2/*.html')), (DATA+'templates/install', glob('templates/install/*.conf')), + (DATA+'templates/install/saml2', + glob('templates/install/saml2/*.conf')), (DATA+'templates/admin/providers', glob('templates/admin/providers/*.html')), ], - scripts = ['ipsilon/ipsilon', 'ipsilon/install/ipsilon-server-install'] + scripts = ['ipsilon/ipsilon', + 'ipsilon/install/ipsilon-server-install', + 'ipsilon/install/ipsilon-client-install'] ) diff --git a/templates/install/saml2/sp.conf b/templates/install/saml2/sp.conf new file mode 100644 index 0000000..57abdfd --- /dev/null +++ b/templates/install/saml2/sp.conf @@ -0,0 +1,28 @@ +# This is a server-wide configuration that will add information from the Mellon +# session to all requests under this path. + + MellonEnable "${saml_protect}" + MellonSPPrivateKeyFile "${saml_sp_key}" + MellonSPCertFile "${saml_sp_cert}" + MellonSPMetadataFile "${saml_sp_meta}" + MellonIdPMetadataFile "${saml_idp_meta}" + MellonEndpointPath ${saml_sp} + MellonVariable "saml-sesion-cookie" + # Comment out the next line if you want to allow logins on bare HTTP + MellonsecureCookie On + MellonUser "NAME_ID" + MellonIdP "IDP" + MellonSessionLength 3600 + # MellonNoCookieErrorPage "https://idp.example.com/no-cookie-error.html" + # MellonPostDirectory "/var/lib/ipsilon/post_cache" + # MellonPostReplay On + + +${saml_auth} + +${sp}Alias /saml2protected /usr/share/ipsilon/ui/saml2sp +${sp} +${sp} +${sp} SSLRequireSSL +${sp} Require all granted +${sp} diff --git a/ui/saml2sp/index.html b/ui/saml2sp/index.html new file mode 100644 index 0000000..a7e6b66 --- /dev/null +++ b/ui/saml2sp/index.html @@ -0,0 +1,8 @@ + + + Service Provider Authenticated Page + + + Congratulations, your SAML2 Service Provider is working. + +