#125 Implement root-bound instances
Merged 7 years ago by puiterwijk. Opened 7 years ago by puiterwijk.
puiterwijk/ipsilon root-instance  into  master

file modified
+1
@@ -90,6 +90,7 @@ 

  tests: wrappers

  	echo "Testdir: $(TESTDIR)"

  	PYTHONPATH=./ ./tests/tests.py --path=$(TESTDIR) --test=test1

+ 	PYTHONPATH=./ ./tests/tests.py --path=$(TESTDIR) --test=testroot

  	PYTHONPATH=./ ./tests/tests.py --path=$(TESTDIR) --test=testlogout

  	PYTHONPATH=./ ./tests/tests.py --path=$(TESTDIR) --test=testnameid

  	PYTHONPATH=./ ./tests/tests.py --path=$(TESTDIR) --test=testrest

file modified
+2 -2
@@ -106,7 +106,7 @@ 

  CONF_TEMPLATE = """

  LoadModule lookup_identity_module modules/mod_lookup_identity.so

  

- <Location /${instance}>

+ <Location ${instanceurl}>

    LookupUserAttr sn REMOTE_USER_LASTNAME

    LookupUserAttr st REMOTE_USER_STATE

    LookupUserAttr locality REMOTE_USER_CITY
@@ -142,7 +142,7 @@ 

  

          configured = 0

  

-         confopts = {'instance': opts['instance']}

+         confopts = {'instanceurl': opts['instanceurl']}

  

          tmpl = Template(CONF_TEMPLATE)

          hunk = tmpl.substitute(**confopts)

@@ -96,6 +96,9 @@ 

      if not os.path.exists(instance_conf):

          os.makedirs(instance_conf, 0700)

      confopts = {'instance': args['instance'],

+                 'instanceurl': args['instanceurl'],

+                 'needs_mount': args.get('needs_mount'),

+                 'script_alias': args.get('script_alias'),

                  'datadir': args['data_dir'],

                  'publicdatadir': args['public_data_dir'],

                  'wellknowndir': args['wellknown_dir'],
@@ -385,6 +388,8 @@ 

                          help="Machine's fully qualified host name")

      parser.add_argument('--instance', default='idp',

                          help="IdP instance name, each is a separate idp")

+     parser.add_argument('--root-instance', default=False, action='store_true',

+                         help='Configure Ipsilon to mount to the web root')

      parser.add_argument('--system-user', default='ipsilon',

                          help="User account used to run the server")

      parser.add_argument('--admin-user', default='admin',
@@ -495,6 +500,19 @@ 

          for k in sorted(opts.iterkeys()):

              logger.debug('%s: %s', k, opts[k])

  

+         if not opts['root_instance'] and opts['instance'] == 'root':

+             raise ValueError('Instance name "root" is reserved')

+ 

+         if opts['root_instance']:

+             opts['instance'] = 'root'

+             opts['instanceurl'] = ''

+             opts['needs_mount'] = '# '

+             opts['script_alias'] = '/'

+         else:

+             opts['instanceurl'] = '/%s' % opts['instance']

+             opts['needs_mount'] = ''

+             opts['script_alias'] = opts['instanceurl']

+ 

          if 'uninstall' in opts and opts['uninstall'] is True:

              if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):

                  logger.info('Instance %s could not be found' % opts['instance'])

file modified
+1 -1
@@ -80,7 +80,7 @@ 

              '/cache': {'tools.staticdir.on': True,

                         'tools.staticdir.dir': 'cache'}}

      cherrypy.quickstart(Root('default', template_env),

-                         cherrypy.config['base.mount'], conf)

+                         cherrypy.config.get('base.mount') or '/', conf)

  

  else:

      cherrypy.config['environment'] = 'embedded'

file modified
+2 -2
@@ -85,7 +85,7 @@ 

  LoadModule intercept_form_submit_module modules/mod_intercept_form_submit.so

  LoadModule authnz_pam_module modules/mod_authnz_pam.so

  

- <Location /${instance}/login/form>

+ <Location ${instanceurl}/login/form>

    InterceptFormPAMService ${service}

    InterceptFormLogin login_name

    InterceptFormPassword login_password
@@ -113,7 +113,7 @@ 

          if opts['form'] != 'yes':

              return

  

-         confopts = {'instance': opts['instance'],

+         confopts = {'instanceurl': opts['instanceurl'],

                      'service': opts['form_service']}

  

          tmpl = Template(CONF_TEMPLATE)

file modified
+4 -4
@@ -86,7 +86,7 @@ 

  

  CONF_TEMPLATE = """

  

- <Location /${instance}/login/gssapi/negotiate>

+ <Location ${instanceurl}/login/gssapi/negotiate>

    AuthName "GSSAPI Single Sign On Login"

    <IfModule mod_auth_gssapi.c>

      GssapiCredStore keytab:$keytab
@@ -102,8 +102,8 @@ 

    </IfModule>

    Require valid-user

  

-   ErrorDocument 401 /${instance}/login/gssapi/unauthorized

-   ErrorDocument 500 /${instance}/login/gssapi/failed

+   ErrorDocument 401 ${instanceurl}/login/gssapi/unauthorized

+   ErrorDocument 500 ${instanceurl}/login/gssapi/failed

  </Location>

  """

  
@@ -126,7 +126,7 @@ 

          if opts['gssapi'] != 'yes':

              return

  

-         confopts = {'instance': opts['instance']}

+         confopts = {'instanceurl': opts['instanceurl']}

  

          if os.path.exists(opts['gssapi_httpd_keytab']):

              confopts['keytab'] = opts['gssapi_httpd_keytab']

@@ -246,8 +246,8 @@ 

              m.write(keyset.export())

  

          proto = 'https'

-         url = '%s://%s/%s/openidc/' % (

-             proto, opts['hostname'], opts['instance'])

+         url = '%s://%s%s/openidc/' % (

+             proto, opts['hostname'], opts['instanceurl'])

  

          subject_salt = uuid.uuid4().hex

          if opts['openidc_subject_salt']:

file modified
+2 -2
@@ -158,8 +158,8 @@ 

          proto = 'https'

          if opts['secure'].lower() == 'no':

              proto = 'http'

-         url = '%s://%s/%s/openid/' % (

-             proto, opts['hostname'], opts['instance'])

+         url = '%s://%s%s/openid/' % (

+             proto, opts['hostname'], opts['instanceurl'])

  

          # Add configuration data to database

          po = PluginObject(*self.pargs)

@@ -101,8 +101,9 @@ 

          for c in key.n[4:]:

              key_n = (key_n*256) + ord(c)

          wellknown = dict()

-         wellknown['authentication'] = '/%s/persona/SignIn/' % opts['instance']

-         wellknown['provisioning'] = '/%s/persona/' % opts['instance']

+         wellknown['authentication'] = ('%s/persona/SignIn/'

+                                        % opts['instanceurl'])

+         wellknown['provisioning'] = '%s/persona/' % opts['instanceurl']

          wellknown['public-key'] = {'algorithm': 'RS',

                                     'e': str(exponent),

                                     'n': str(key_n)}

@@ -518,7 +518,7 @@ 

          proto = 'https'

          if opts['secure'].lower() == 'no':

              proto = 'http'

-         url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])

+         url = '%s://%s%s' % (proto, opts['hostname'], opts['instanceurl'])

          validity = int(opts['saml2_metadata_validity'])

          meta = IdpMetadataGenerator(url, cert,

                                      timedelta(validity))

file modified
+6 -2
@@ -76,7 +76,11 @@ 

          conf_template = f.read()

      t = Template(conf_template)

      text = t.substitute({'debugging': 'True',

-                          'instance': 'idp',

+                          'root_instance': True,

+                          'needs_mount': '# ',

+                          'instance': 'root',

+                          'instanceurl': '',

+                          'script_alias': '/',

                           'staticdir': os.getcwd(),

                           'datadir': workdir,

                           'cachedir': cachedir,
@@ -99,7 +103,7 @@ 

      # Initialize SAML2, since this is quite tricky to get right

      cert = Certificate(os.path.join(workdir, 'saml2'))

      cert.generate('certificate', 'ipsilon-quickrun')

-     url = 'http://localhost:8080/idp'

+     url = 'http://localhost:8080/'

      validity = 365 * 5

      meta = IdpMetadataGenerator(url, cert,

                                  timedelta(validity))

file modified
+5 -5
@@ -1,9 +1,9 @@ 

- Alias /${instance}/ui ${staticdir}/ui

+ Alias ${instanceurl}/ui ${staticdir}/ui

  Alias /.well-known ${wellknowndir}

- Alias /${instance}/cache /var/cache/ipsilon

- Redirect /${instance}/.well-known/webfinger /${instance}/webfinger

+ Alias ${instanceurl}/cache /var/cache/ipsilon

+ Redirect ${instanceurl}/.well-known/webfinger ${instanceurl}/webfinger

  

- WSGIScriptAlias /${instance} ${ipsilondir}/ipsilon

+ WSGIScriptAlias ${script_alias} ${ipsilondir}/ipsilon

  WSGIDaemonProcess ${instance} user=${sysuser} group=${sysuser} home=${datadir} display-name=ipsilon-${instance}

  # This header is required to be passed for OIDC client_secret_basic

  WSGIPassAuthorization On
@@ -12,7 +12,7 @@ 

  WSGIApplicationGroup %{GLOBAL}

  ${wsgi_socket}

  

- <Location /${instance}>

+ <Location ${script_alias}>

      WSGIProcessGroup ${instance}

      ${ssl_require}<IfModule mod_nss.c>

      ${ssl_require}    NSSRequireSSL

@@ -7,7 +7,7 @@ 

  db.conn.log = False

  db.echo = False

  

- base.mount = "/${instance}"

+ ${needs_mount}base.mount = "${instanceurl}"

  base.dir = "${staticdir}"

  admin.config.db = "${admindb}"

  user.prefs.db = "${usersdb}"
@@ -17,7 +17,7 @@ 

  tools.sessions.name = "${instance}_ipsilon_session_id"

  tools.sessions.storage_type = "${sesstype}"

  tools.sessions.storage_${sessopt} = "${sessval}"

- tools.sessions.path = "/${instance}"

+ tools.sessions.path = "${instanceurl}"

  tools.sessions.timeout = 60

  tools.sessions.httponly = ${secure}

  tools.sessions.secure = ${secure}

file modified
+30 -19
@@ -80,6 +80,12 @@ 

  

          raise ValueError("Unknown URL: %s" % url)

  

+     def get_idp_uri(self, idp):

+         if idp == 'root':

+             return ''

+         else:

+             return '/%s' % idp

+ 

      def get(self, url, krb=False, **kwargs):

          session = self.get_session(url)

          allow_redirects = False
@@ -336,7 +342,7 @@ 

      def auth_to_idp(self, idp, krb=False, rule=None, expected=None):

  

          srv = self.servers[idp]

-         target_url = '%s/%s/' % (srv['baseuri'], idp)

+         target_url = '%s%s/' % (srv['baseuri'], self.get_idp_uri(idp))

  

          r = self.access('get', target_url, krb=krb)

          if r.status_code != 200:
@@ -359,7 +365,7 @@ 

      def logout_from_idp(self, idp):

  

          srv = self.servers[idp]

-         target_url = '%s/%s/logout' % (srv['baseuri'], idp)

+         target_url = '%s%s/logout' % (srv['baseuri'], self.get_idp_uri(idp))

  

          r = self.access('get', target_url)

          if r.status_code != 200:
@@ -378,7 +384,8 @@ 

          expected_status = 200

          idpsrv = self.servers[idp]

          idpuri = idpsrv['baseuri']

-         url = '%s/%s/admin/providers/saml2/admin/new' % (idpuri, idp)

+         url = '%s%s/admin/providers/saml2/admin/new' % (idpuri,

+                                                         self.get_idp_uri(idp))

          headers = {'referer': url}

          if rest:

              expected_status = 201
@@ -390,7 +397,9 @@ 

                  'splink': 'https://test.example.com/secret/',

              }

              headers['content-type'] = 'application/x-www-form-urlencoded'

-             url = '%s/%s/rest/providers/saml2/SPS/%s' % (idpuri, idp, desc)

+             url = '%s%s/rest/providers/saml2/SPS/%s' % (idpuri,

+                                                         self.get_idp_uri(idp),

+                                                         desc)

              r = self.post(url, headers=headers, data=urlencode(payload))

          else:

              metafile = {'metafile': m}
@@ -410,7 +419,8 @@ 

          """

          idpsrv = self.servers[idp]

          idpuri = idpsrv['baseuri']

-         url = '%s/%s/admin/providers/saml2/admin/sp/%s' % (idpuri, idp, sp)

+         url = '%s%s/admin/providers/saml2/admin/sp/%s' % (

+             idpuri, self.get_idp_uri(idp), sp)

          headers = {'referer': url}

          headers['content-type'] = 'application/x-www-form-urlencoded'

          payload = {'submit': 'Submit',
@@ -439,8 +449,8 @@ 

          idpsrv = self.servers[idp]

          idpuri = idpsrv['baseuri']

          if spname:  # per-SP setting

-             url = '%s/%s/admin/providers/saml2/admin/sp/%s' % (

-                 idpuri, idp, spname)

+             url = '%s%s/admin/providers/saml2/admin/sp/%s' % (

+                 idpuri, self.get_idp_uri(idp), spname)

              mapname = 'Attribute Mapping'

              attrname = 'Allowed Attributes'

          else:  # global default
@@ -476,9 +486,9 @@ 

          idpsrv = self.servers[idp]

          idpuri = idpsrv['baseuri']

  

-         url = '%s/%s/admin/loginstack/%s/enable/%s' % (

-             idpuri, idp, plugtype, plugin)

-         rurl = '%s/%s/admin/loginstack' % (idpuri, idp)

+         url = '%s%s/admin/loginstack/%s/enable/%s' % (

+             idpuri, self.get_idp_uri(idp), plugtype, plugin)

+         rurl = '%s%s/admin/loginstack' % (idpuri, self.get_idp_uri(idp))

          headers = {'referer': rurl}

          r = idpsrv['session'].get(url, headers=headers)

          if r.status_code != 200:
@@ -495,9 +505,9 @@ 

          idpsrv = self.servers[idp]

          idpuri = idpsrv['baseuri']

  

-         url = '%s/%s/admin/loginstack/%s/disable/%s' % (

-             idpuri, idp, plugtype, plugin)

-         rurl = '%s/%s/admin/loginstack' % (idpuri, idp)

+         url = '%s%s/admin/loginstack/%s/disable/%s' % (

+             idpuri, self.get_idp_uri(idp), plugtype, plugin)

+         rurl = '%s%s/admin/loginstack' % (idpuri, self.get_idp_uri(idp))

          headers = {'referer': rurl}

          r = idpsrv['session'].get(url, headers=headers)

          if r.status_code != 200:
@@ -514,8 +524,8 @@ 

          idpsrv = self.servers[idp]

          idpuri = idpsrv['baseuri']

  

-         url = '%s/%s/admin/loginstack/%s/order' % (

-             idpuri, idp, plugtype)

+         url = '%s%s/admin/loginstack/%s/order' % (

+             idpuri, self.get_idp_uri(idp), plugtype)

          headers = {'referer': url}

          headers['content-type'] = 'application/x-www-form-urlencoded'

          payload = {'order': ','.join(order)}
@@ -530,8 +540,8 @@ 

          idpsrv = self.servers[idp]

          idpuri = idpsrv['baseuri']

  

-         url = '%s/%s/admin/providers/openidc/admin/client/%s/delete' % (

-             idpuri, idp, client_id)

+         url = '%s%s/admin/providers/openidc/admin/client/%s/delete' % (

+             idpuri, self.get_idp_uri(idp), client_id)

          headers = {'referer': url}

          headers['content-type'] = 'application/x-www-form-urlencoded'

          r = idpsrv['session'].get(url, headers=headers)
@@ -560,8 +570,9 @@ 

  

      def get_rest_sp(self, idpname, spname=None):

          if spname is None:

-             uri = '/%s/rest/providers/saml2/SPS/' % idpname

+             uri = '%s/rest/providers/saml2/SPS/' % self.get_idp_uri(idpname)

          else:

-             uri = '/%s/rest/providers/saml2/SPS/%s' % (idpname, spname)

+             uri = '%s/rest/providers/saml2/SPS/%s' % (

+                 self.get_idp_uri(idpname), spname)

  

          return self.fetch_rest_page(idpname, uri)

file added
+196
@@ -0,0 +1,196 @@ 

+ #!/usr/bin/python

+ #

+ # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING

+ 

+ from helpers.common import IpsilonTestBase  # pylint: disable=relative-import

+ from helpers.http import HttpSessions  # pylint: disable=relative-import

+ import os

+ import pwd

+ import sys

+ from string import Template

+ 

+ idp_g = {'TEMPLATES': '${TESTDIR}/templates/install',

+          'CONFDIR': '${TESTDIR}/etc',

+          'DATADIR': '${TESTDIR}/lib',

+          'CACHEDIR': '${TESTDIR}/cache',

+          'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

+          'STATICDIR': '${ROOTDIR}',

+          'BINDIR': '${ROOTDIR}/ipsilon',

+          'WSGI_SOCKET_PREFIX': '${TESTDIR}/${NAME}/logs/wsgi'}

+ 

+ 

+ idp_a = {'hostname': '${ADDRESS}:${PORT}',

+          'admin_user': '${TEST_USER}',

+          'system_user': '${TEST_USER}',

+          'root_instance': True,

+          'testauth': 'yes',

+          'pam': 'no',

+          'gssapi': 'no',

+          'ipa': 'no',

+          'server_debugging': 'True'}

+ 

+ 

+ sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

+         'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

+ 

+ 

+ sp_a = {'hostname': '${ADDRESS}',

+         'saml_idp_metadata': 'https://127.0.0.10:45080/saml2/metadata',

+         'saml_auth': '/sp',

+         'httpd_user': '${TEST_USER}'}

+ 

+ sp2_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

+          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

+          'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+          'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

+ 

+ sp2_a = {'hostname': '${ADDRESS}',

+          'saml_idp_url': 'https://127.0.0.10:45080/',

+          'admin_user': '${TEST_USER}',

+          'admin_password': '${TESTDIR}/pw.txt',

+          'saml_sp_name': 'sp2-test.example.com',

+          'saml_auth': '/sp',

+          'httpd_user': '${TEST_USER}'}

+ 

+ keyless_metadata = """<?xml version='1.0' encoding='UTF-8'?>

+ <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"

+     xmlns:ds="http://www.w3.org/2000/09/xmldsig#" cacheDuration="P7D"

+     entityID="http://keyless-sp">

+   <md:SPSSODescriptor

+         protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">

+     <md:AssertionConsumerService

+         Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"

+         Location="http://keyless-sp/postResponse" index="0"/>

+     <md:NameIDFormat>

+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>

+   </md:SPSSODescriptor>

+ </md:EntityDescriptor>

+ """

+ 

+ 

+ def fixup_sp_httpd(httpdir):

+     location = """

+ 

+ Alias /sp ${HTTPDIR}/sp

+ 

+ <Directory ${HTTPDIR}/sp>

+     <IfModule mod_authz_core.c>

+         Require all granted

+     </IfModule>

+     <IfModule !mod_authz_core.c>

+         Order Allow,Deny

+         Allow from All

+     </IfModule>

+ </Directory>

+ """

+     index = """WORKS!"""

+ 

+     t = Template(location)

+     text = t.substitute({'HTTPDIR': httpdir})

+     with open(httpdir + '/conf.d/ipsilon-saml.conf', 'a') as f:

+         f.write(text)

+ 

+     os.mkdir(httpdir + '/sp')

+     with open(httpdir + '/sp/index.html', 'w') as f:

+         f.write(index)

+ 

+ 

+ class IpsilonTest(IpsilonTestBase):

+ 

+     def __init__(self):

+         super(IpsilonTest, self).__init__('testroot', __file__)

+ 

+     def setup_servers(self, env=None):

+         print "Installing IDP server"

+         name = 'root'

+         addr = '127.0.0.10'

+         port = '45080'

+         idp = self.generate_profile(idp_g, idp_a, name, addr, port)

+         conf = self.setup_idp_server(idp, name, addr, port, env)

+ 

+         print "Starting IDP's httpd server"

+         self.start_http_server(conf, env)

+ 

+         print "Installing first SP server"

+         name = 'sp1'

+         addr = '127.0.0.11'

+         port = '45081'

+         sp = self.generate_profile(sp_g, sp_a, name, addr, port)

+         conf = self.setup_sp_server(sp, name, addr, port, env)

+         fixup_sp_httpd(os.path.dirname(conf))

+ 

+         print "Starting first SP's httpd server"

+         self.start_http_server(conf, env)

+ 

+         print "Installing second SP server"

+         name = 'sp2-test.example.com'

+         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 = 'root'

+     sp1name = 'sp1'

+     sp2name = 'sp2-test.example.com'

+     user = pwd.getpwuid(os.getuid())[0]

+ 

+     sess = HttpSessions()

+     sess.add_server(idpname, 'https://127.0.0.10:45080', user, 'ipsilon')

+     sess.add_server(sp1name, 'https://127.0.0.11:45081')

+     sess.add_server(sp2name, 'https://127.0.0.11:45082')

+ 

+     print "testroot: Authenticate to IDP ...",

+     try:

+         sess.auth_to_idp(idpname)

+     except Exception, e:  # pylint: disable=broad-except

+         print >> sys.stderr, " ERROR: %s" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "testroot: Add first SP Metadata to IDP ...",

+     try:

+         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 "testroot: Access first SP Protected Area ...",

+     try:

+         page = sess.fetch_page(idpname, 'https://127.0.0.11:45081/sp/')

+         page.expected_value('text()', 'WORKS!')

+     except ValueError, e:

+         print >> sys.stderr, " ERROR: %s" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "testroot: Access second SP Protected Area ...",

+     try:

+         page = sess.fetch_page(idpname, 'https://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 "testroot: Try authentication failure ...",

+     newsess = HttpSessions()

+     newsess.add_server(idpname, 'https://127.0.0.10:45080', user, 'wrong')

+     try:

+         newsess.auth_to_idp(idpname)

+         print >> sys.stderr, " ERROR: Authentication should have failed"

+         sys.exit(1)

+     except Exception, e:  # pylint: disable=broad-except

+         print " SUCCESS"