X-Git-Url: http://git.cascardo.info/?p=cascardo%2Fipsilon.git;a=blobdiff_plain;f=ipsilon%2Finstall%2Fipsilon-client-install;h=2c6df8eed914fb8ec18d7bc1905d8baa784cc395;hp=237b4398af881be33c678a7950895419d9639f75;hb=ce2bbec3f2a010cfa26363a91a6224efe484f06f;hpb=d67664fbffe9c380a354abe115ee5afa1ff968be diff --git a/ipsilon/install/ipsilon-client-install b/ipsilon/install/ipsilon-client-install index 237b439..2c6df8e 100755 --- a/ipsilon/install/ipsilon-client-install +++ b/ipsilon/install/ipsilon-client-install @@ -1,29 +1,17 @@ #!/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 . +# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING 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 ipsilon.tools import files +from urllib import urlencode import argparse import ConfigParser +import getpass +import json import logging import os import pwd @@ -58,7 +46,11 @@ def saml2(): if args['saml_idp_metadata'] is None: #TODO: detect via SRV records ? - raise ValueError('An IDP metadata file/url is required.') + if args['saml_idp_url']: + args['saml_idp_metadata'] = ('%s/saml2/metadata' % + args['saml_idp_url'].rstrip('/')) + else: + raise ValueError('An IDP URL or metadata file/URL is required.') idpmeta = None @@ -88,7 +80,12 @@ def saml2(): proto = 'https' if not args['saml_secure_setup']: proto = 'http' - url = '%s://%s' % (proto, args['hostname']) + + port_str = '' + if args['port']: + port_str = ':%s' % args['port'] + + url = '%s://%s%s' % (proto, args['hostname'], port_str) url_sp = url + args['saml_sp'] url_logout = url + args['saml_sp_logout'] url_post = url + args['saml_sp_post'] @@ -100,10 +97,54 @@ def saml2(): m.set_entity_id(url_sp) m.add_certs(c) m.add_service(SAML2_SERVICE_MAP['logout-redirect'], url_logout) + if not args['no_saml_soap_logout']: + m.add_service(SAML2_SERVICE_MAP['slo-soap'], url_logout) m.add_service(SAML2_SERVICE_MAP['response-post'], url_post, index="0") + m.add_allowed_name_format(SAML2_NAMEID_MAP[args['saml_nameid']]) sp_metafile = os.path.join(path, 'metadata.xml') m.output(sp_metafile) + # Register with the IDP if the IDP URL was provided + if args['saml_idp_url']: + if args['admin_password']: + if args['admin_password'] == '-': + admin_password = sys.stdin.readline().rstrip('\n') + else: + try: + with open(args['admin_password']) as f: + admin_password = f.read().rstrip('\n') + except Exception as e: # pylint: disable=broad-except + logger.error("Failed to read password file!\n" + + "Error: [%s]" % e) + raise + elif ('IPSILON_ADMIN_PASSWORD' in os.environ) and \ + (os.environ['IPSILON_ADMIN_PASSWORD']): + admin_password = os.environ['IPSILON_ADMIN_PASSWORD'] + else: + admin_password = getpass.getpass('%s password: ' % + args['admin_user']) + + # Read our metadata + sp_metadata = '' + try: + with open(sp_metafile) as f: + for line in f: + sp_metadata += line.strip() + except Exception as e: # pylint: disable=broad-except + logger.error("Failed to read SP Metadata file!\n" + + "Error: [%s]" % e) + raise + + # Register the SP + try: + saml2_register_sp(args['saml_idp_url'], args['admin_user'], + admin_password, args['saml_sp_name'], + sp_metadata) + except Exception as e: # pylint: disable=broad-except + logger.error("Failed to register SP with IDP!\n" + + "Error: [%s]" % e) + raise + if not args['saml_no_httpd']: idp_metafile = os.path.join(path, 'idp-metadata.xml') with open(idp_metafile, 'w+') as f: @@ -126,6 +167,11 @@ def saml2(): saml_secure = 'Off' ssl_require = '#' ssl_rewrite = '#' + if args['port']: + ssl_port = args['port'] + else: + ssl_port = '443' + if args['saml_secure_setup']: saml_secure = 'On' ssl_require = '' @@ -142,7 +188,9 @@ def saml2(): 'saml_auth': saml_auth, 'ssl_require': ssl_require, 'ssl_rewrite': ssl_rewrite, + 'ssl_port': ssl_port, 'sp_hostname': args['hostname'], + 'sp_port': port_str, 'sp': psp} files.write_from_template(SAML2_CONFFILE, SAML2_TEMPLATE, samlopts) @@ -157,6 +205,34 @@ def saml2(): ' configure your Service Provider') +def saml2_register_sp(url, user, password, sp_name, sp_metadata): + s = requests.Session() + + # Authenticate to the IdP + form_auth_url = '%s/login/form' % url.rstrip('/') + test_auth_url = '%s/login/testauth' % url.rstrip('/') + auth_data = {'login_name': user, + 'login_password': password} + + r = s.post(form_auth_url, data=auth_data) + if r.status_code == 404: + r = s.post(test_auth_url, data=auth_data) + + if r.status_code != 200: + raise Exception('Unable to authenticate to IdP (%d)' % r.status_code) + + # Add the SP + sp_url = '%s/rest/providers/saml2/SPS/%s' % (url.rstrip('/'), sp_name) + sp_headers = {'Content-type': 'application/x-www-form-urlencoded', + 'Referer': sp_url} + sp_data = urlencode({'metadata': sp_metadata}) + + r = s.post(sp_url, headers=sp_headers, data=sp_data) + if r.status_code != 201: + message = json.loads(r.text)['message'] + raise Exception('%s' % message) + + def install(): if args['saml']: saml2() @@ -205,7 +281,7 @@ def parse_config_profile(args): if g in globals(): globals()[g] = val else: - for k in globals().keys(): + for k in globals(): if k.lower() == g.lower(): globals()[k] = val break @@ -233,12 +309,19 @@ def parse_args(): action='version', version='%(prog)s 0.1') parser.add_argument('--hostname', default=socket.getfqdn(), help="Machine's fully qualified host name") + parser.add_argument('--port', default=None, + help="Port number that SP listens on") parser.add_argument('--admin-user', default='admin', help="Account allowed to create a SP") + parser.add_argument('--admin-password', default=None, + help="File containing the password for the account " + + "used to create a SP (- to read from stdin)") parser.add_argument('--httpd-user', default='apache', help="Web server account used to read certs") parser.add_argument('--saml', action='store_true', default=True, help="Whether to install a saml2 SP") + parser.add_argument('--saml-idp-url', default=None, + help="A URL of the IDP to register the SP with") parser.add_argument('--saml-idp-metadata', default=None, help="A URL pointing at the IDP Metadata (FILE or HTTP)") parser.add_argument('--saml-no-httpd', action='store_true', default=False, @@ -249,16 +332,24 @@ def parse_args(): 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', + parser.add_argument('--saml-sp-logout', default=None, help="Single Logout URL") - parser.add_argument('--saml-sp-post', default='/saml2/postResponse', + parser.add_argument('--saml-sp-post', default=None, help="Post response URL") + parser.add_argument('--no-saml-soap-logout', action='store_true', + default=False, + help="Disable Single Logout over SOAP") parser.add_argument('--saml-secure-setup', action='store_true', default=True, help="Turn on all security checks") + parser.add_argument('--saml-nameid', default='unspecified', + choices=SAML2_NAMEID_MAP.keys(), + help="SAML NameID format to use") + parser.add_argument('--saml-sp-name', default=None, + help="The SP name to register with the IdP") parser.add_argument('--debug', action='store_true', default=False, help="Turn on script debugging") parser.add_argument('--config-profile', default=None, - help="File containing install options") + help=argparse.SUPPRESS) parser.add_argument('--uninstall', action='store_true', help="Uninstall the server and all data") @@ -268,7 +359,42 @@ def parse_args(): args = parse_config_profile(args) if len(args['hostname'].split('.')) < 2: - raise ValueError('Hostname: %s is not a FQDN.') + raise ValueError('Hostname: %s is not a FQDN.' % args['hostname']) + + if args['port'] and not args['port'].isdigit(): + raise ValueError('Port number: %s is not an integer.' % args['port']) + + # Validate that all path options begin with '/' + path_args = ['saml_base', 'saml_auth', 'saml_sp', 'saml_sp_logout', + 'saml_sp_post'] + for path_arg in path_args: + if args[path_arg] is not None and not args[path_arg].startswith('/'): + raise ValueError('--%s must begin with a / character.' % + path_arg.replace('_', '-')) + + # The saml_sp setting must be a subpath of saml_base since it is + # used as the MellonEndpointPath. + if not args['saml_sp'].startswith(args['saml_base']): + raise ValueError('--saml-sp must be a subpath of --saml-base.') + + # The saml_sp_logout and saml_sp_post settings must be subpaths + # of saml_sp (the mellon endpoint). + path_args = {'saml_sp_logout': 'logout', + 'saml_sp_post': 'postResponse'} + for path_arg, default_path in path_args.items(): + if args[path_arg] is None: + args[path_arg] = '%s/%s' % (args['saml_sp'].rstrip('/'), + default_path) + + elif not args[path_arg].startswith(args['saml_sp']): + raise ValueError('--%s must be a subpath of --saml-sp' % + path_arg.replace('_', '-')) + + # If saml_idp_url if being used, we require saml_sp_name to + # use when registering the SP. + if args['saml_idp_url'] and not args['saml_sp_name']: + raise ValueError('--saml-sp-name must be specified when using' + + '--saml-idp-url') # At least one on this list needs to be specified or we do nothing sp_list = ['saml'] @@ -288,19 +414,19 @@ if __name__ == '__main__': if 'uninstall' in args and args['uninstall'] is True: uninstall() - - install() + else: + install() except Exception, e: # pylint: disable=broad-except log_exception(e) if 'uninstall' in args and args['uninstall'] is True: - print 'Uninstallation aborted.' + logging.info('Uninstallation aborted.') else: - print 'Installation aborted.' + logging.info('Installation aborted.') out = 1 finally: if out == 0: if 'uninstall' in args and args['uninstall'] is True: - print 'Uninstallation complete.' + logging.info('Uninstallation complete.') else: - print 'Installation complete.' + logging.info('Installation complete.') sys.exit(out)