From 9600cff7a3f93529ab56044968b489030f74b76c Mon Sep 17 00:00:00 2001 From: Nathan Kinder Date: Apr 02 2015 02:54:20 +0000 Subject: Allow SP registration from ipsilon-client-install This optionally allows a SAML SP to be registered with the IDP when running ipsilon-client-install. To register an SP, the following options are used: --saml-idp-url (Ipsilon IDP URL) --saml-sp-name (Name to register the SP as) --admin-user (Ipsilon admin user) --admin-password (Ipsilon admin password file) If the --saml-idp-url option is set, we attempt to register the SP. The --saml-sp-name option is required if you are registering a SP. The --admin-user already defaults to admin, so it only needs to be specified if your admin user has a different username. If the --admin-password option is not specified, we prompt for the password. The --saml-idp-metadata was previously required, but this option is redundant if the new --saml-idp-url option is specified and you are not using a local copy of the IDP metadata. You can now just use the --saml-idp-url option, and we build the metadata URL from it. This helps to minimize the number of required options when you are registering an SP during installation. https://fedorahosted.org/ipsilon/ticket/101 Signed-off-by: Nathan Kinder Reviewed-by: Rob Crittenden --- diff --git a/ipsilon/install/ipsilon-client-install b/ipsilon/install/ipsilon-client-install index b5b6ad1..02b4d5f 100755 --- a/ipsilon/install/ipsilon-client-install +++ b/ipsilon/install/ipsilon-client-install @@ -22,8 +22,11 @@ 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 +61,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 @@ -110,6 +117,44 @@ def saml2(): 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 + 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: @@ -170,6 +215,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() @@ -250,10 +323,15 @@ def parse_args(): 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, @@ -273,6 +351,8 @@ def parse_args(): 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, @@ -312,6 +392,12 @@ def parse_args(): 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'] present = False diff --git a/tests/test1.py b/tests/test1.py index 8589f38..3e0cfc2 100755 --- a/tests/test1.py +++ b/tests/test1.py @@ -25,7 +25,6 @@ import pwd import sys from string import Template - idp_g = {'TEMPLATES': '${TESTDIR}/templates/install', 'CONFDIR': '${TESTDIR}/etc', 'DATADIR': '${TESTDIR}/lib', @@ -59,6 +58,20 @@ sp_a = {'hostname': '${ADDRESS}:${PORT}', 'saml_auth': '/sp', 'httpd_user': '${TEST_USER}'} +sp2_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d', + 'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf', + 'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf', + 'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'} + +sp2_a = {'hostname': '${ADDRESS}:${PORT}', + 'saml_idp_url': 'http://127.0.0.10:45080/idp1', + 'admin_user': '${TEST_USER}', + 'admin_password': '${TESTDIR}/pw.txt', + 'saml_sp_name': 'sp2', + 'saml_secure_setup': 'False', + 'saml_auth': '/sp', + 'httpd_user': '${TEST_USER}'} + def fixup_sp_httpd(httpdir): location = """ @@ -97,7 +110,7 @@ class IpsilonTest(IpsilonTestBase): print "Starting IDP's httpd server" self.start_http_server(conf, env) - print "Installing SP server" + print "Installing first SP server" name = 'sp1' addr = '127.0.0.11' port = '45081' @@ -105,19 +118,35 @@ class IpsilonTest(IpsilonTestBase): conf = self.setup_sp_server(sp, name, addr, port, env) fixup_sp_httpd(os.path.dirname(conf)) - print "Starting SP's httpd server" + print "Starting first SP's httpd server" + self.start_http_server(conf, env) + + print "Installing second SP server" + name = 'sp2' + addr = '127.0.0.11' + port = '45082' + sp = self.generate_profile(sp2_g, sp2_a, name, addr, port) + with open(os.path.dirname(sp) + '/pw.txt', 'a') as f: + f.write('ipsilon') + conf = self.setup_sp_server(sp, name, addr, port, env) + os.remove(os.path.dirname(sp) + '/pw.txt') + fixup_sp_httpd(os.path.dirname(conf)) + + print "Starting second SP's httpd server" self.start_http_server(conf, env) if __name__ == '__main__': idpname = 'idp1' - spname = 'sp1' + sp1name = 'sp1' + sp2name = 'sp2' user = pwd.getpwuid(os.getuid())[0] sess = HttpSessions() sess.add_server(idpname, 'http://127.0.0.10:45080', user, 'ipsilon') - sess.add_server(spname, 'http://127.0.0.11:45081') + sess.add_server(sp1name, 'http://127.0.0.11:45081') + sess.add_server(sp2name, 'http://127.0.0.11:45082') print "test1: Authenticate to IDP ...", try: @@ -127,15 +156,15 @@ if __name__ == '__main__': sys.exit(1) print " SUCCESS" - print "test1: Add SP Metadata to IDP ...", + print "test1: Add first SP Metadata to IDP ...", try: - sess.add_sp_metadata(idpname, spname) + sess.add_sp_metadata(idpname, sp1name) except Exception, e: # pylint: disable=broad-except print >> sys.stderr, " ERROR: %s" % repr(e) sys.exit(1) print " SUCCESS" - print "test1: Access SP Protected Area ...", + print "test1: Access first SP Protected Area ...", try: page = sess.fetch_page(idpname, 'http://127.0.0.11:45081/sp/') page.expected_value('text()', 'WORKS!') @@ -144,6 +173,15 @@ if __name__ == '__main__': sys.exit(1) print " SUCCESS" + print "test1: Access second SP Protected Area ...", + try: + page = sess.fetch_page(idpname, 'http://127.0.0.11:45082/sp/') + page.expected_value('text()', 'WORKS!') + except ValueError, e: + print >> sys.stderr, " ERROR: %s" % repr(e) + sys.exit(1) + print " SUCCESS" + print "test1: Try authentication failure ...", newsess = HttpSessions() newsess.add_server(idpname, 'http://127.0.0.10:45080', user, 'wrong')