#99 Add a plugin-based authorization system for SP user sessions
Merged 7 years ago by merlinthp. Opened 7 years ago by merlinthp.
merlinthp/ipsilon authz  into  master

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

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

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

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

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

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

  

  test: lp-test unittests tests

@@ -0,0 +1,10 @@ 

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

+ 

+ from ipsilon.admin.loginstack import LoginStackPlugins

+ from ipsilon.authz.common import FACILITY

+ 

+ 

+ class AuthzPlugins(LoginStackPlugins):

+     def __init__(self, site, parent):

+         super(AuthzPlugins, self).__init__('authz', site, parent, FACILITY)

+         self.title = 'Authorization Plugins'

empty or binary file added
@@ -0,0 +1,43 @@ 

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

+ 

+ from ipsilon.authz.common import AuthzProviderBase

+ from ipsilon.authz.common import AuthzProviderInstaller

+ from ipsilon.util.plugin import PluginObject

+ 

+ 

+ class AuthzProvider(AuthzProviderBase):

+     def __init__(self, *pargs):

+         super(AuthzProvider, self).__init__(*pargs)

+         self.name = 'allow'

+         self.description = """

+ Authorization plugin to allow all requests. """

+         self.new_config(self.name)

+ 

+     def authorize_user(self, provplugname, provinfo, user, attributes):

+         return True

+ 

+ 

+ class Installer(AuthzProviderInstaller):

+     def __init__(self, *pargs):

+         super(Installer, self).__init__()

+         self.name = 'allow'

+         self.pargs = pargs

+ 

+     def install_args(self, group):

+         group.add_argument('--authorization-allow', choices=['yes', 'no'],

+                            default='yes', dest='authz_allow',

+                            help='Use the allow authorization provider')

+ 

+     def configure(self, opts, changes):

+         if opts['authz_allow'] != 'yes':

+             return

+ 

+         # Add configuration data to database

+         po = PluginObject(*self.pargs)

+         po.name = 'allow'

+         po.wipe_data()

+         po.wipe_config_values()

+ 

+         # Update global config to add allow plugin

+         po.is_enabled = True

+         po.save_enabled_state()

@@ -0,0 +1,91 @@ 

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

+ 

+ from ipsilon.util.plugin import PluginObject, PluginInstaller, PluginLoader

+ from ipsilon.util.config import ConfigHelper

+ from ipsilon.util.log import Log

+ 

+ 

+ class AuthzProviderBase(ConfigHelper, PluginObject):

+     def __init__(self, *pargs):

+         ConfigHelper.__init__(self)

+         PluginObject.__init__(self, *pargs)

+ 

+     def authorize_user(self, provplugname, provinfo, user, attributes):

+         raise NotImplementedError

+ 

+ 

+ FACILITY = 'authz_config'

+ 

+ 

+ class Authz(Log):

+     def __init__(self, site):

+         self._site = site

+ 

+         plugins = PluginLoader(Authz, FACILITY, 'AuthzProvider')

+         plugins.get_plugin_data()

+         self._site[FACILITY] = plugins

+ 

+         available = plugins.available.keys()

+         self.debug('Available authorization providers: %s' % str(available))

+ 

+         for item in plugins.enabled:

+             self.debug('Authorization plugin in enabled list: %s' % item)

+             if item not in plugins.available:

+                 self.debug('Authorization plugin %s not found' % item)

+                 continue

+             try:

+                 plugins.available[item].enable()

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

+                 while item in plugins.enabled:

+                     plugins.enabled.remove(item)

+                 self.debug("Authorization plugin %s couldn't be enabled: %s" %

+                            (item, str(e)))

+ 

+     def authorize_user(self, provplugname, provinfo, user, attributes):

+         plugins = self._site[FACILITY]

+ 

+         authorized = None

+ 

+         for name in plugins.enabled:

+             p = plugins.available[name]

+             self.debug('Calling authorization provider %s' % p.name)

+             result = p.authorize_user(provplugname, provinfo, user,

+                                       attributes)

+             self.debug('Authorization provider %s returned %s' % (p.name,

+                                                                   str(result)))

+             if result is not None:

+                 authorized = result

+                 break

+ 

+         if authorized is None:

+             self.debug('All authorization providers declined to authorize, '

+                        'denying the request')

+             authorized = False

+ 

+         return authorized

+ 

+ 

+ class AuthzProviderInstaller(object):

+     def __init__(self):

+         self.facility = FACILITY

+         self.ptype = 'authz'

+         self.name = None

+ 

+     def unconfigure(self, opts, changes):

+         return

+ 

+     def install_args(self, group):

+         raise NotImplementedError

+ 

+     def validate_args(self, args):

+         return

+ 

+     def configure(self, opts, changes):

+         raise NotImplementedError

+ 

+ 

+ class AuthzProviderInstall(object):

+ 

+     def __init__(self):

+         pi = PluginInstaller(AuthzProviderInstall, FACILITY)

+         self.plugins = pi.get_plugins()

@@ -0,0 +1,43 @@ 

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

+ 

+ from ipsilon.authz.common import AuthzProviderBase

+ from ipsilon.authz.common import AuthzProviderInstaller

+ from ipsilon.util.plugin import PluginObject

+ 

+ 

+ class AuthzProvider(AuthzProviderBase):

+     def __init__(self, *pargs):

+         super(AuthzProvider, self).__init__(*pargs)

+         self.name = 'deny'

+         self.description = """

+ Authorization plugin to deny all requests. """

+         self.new_config(self.name)

+ 

+     def authorize_user(self, provplugname, provinfo, user, attributes):

+         return False

+ 

+ 

+ class Installer(AuthzProviderInstaller):

+     def __init__(self, *pargs):

+         super(Installer, self).__init__()

+         self.name = 'deny'

+         self.pargs = pargs

+ 

+     def install_args(self, group):

+         group.add_argument('--authorization-deny', choices=['yes', 'no'],

+                            default='no', dest='authz_deny',

+                            help='Use the deny authorization provider')

+ 

+     def configure(self, opts, changes):

+         if opts['authz_deny'] != 'yes':

+             return

+ 

+         # Add configuration data to database

+         po = PluginObject(*self.pargs)

+         po.name = 'deny'

+         po.wipe_data()

+         po.wipe_config_values()

+ 

+         # Update global config to add deny plugin

+         po.is_enabled = True

+         po.save_enabled_state()

@@ -0,0 +1,94 @@ 

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

+ 

+ from ipsilon.authz.common import AuthzProviderBase

+ from ipsilon.authz.common import AuthzProviderInstaller

+ from ipsilon.util.plugin import PluginObject

+ from ipsilon.util import config as pconfig

+ 

+ 

+ class AuthzProvider(AuthzProviderBase):

+     def __init__(self, *pargs):

+         super(AuthzProvider, self).__init__(*pargs)

+         self.name = 'spgroup'

+         self.description = """

+ Authorization plugin that allows access based on user groups.

+ This plugin will decline to authorize users not in the prerequisite group,

+ rather than reject them outright."""

+         self.new_config(

+             self.name,

+             pconfig.String(

+                 'prefix',

+                 'Group name prefix',

+                 ''),

+             pconfig.String(

+                 'suffix',

+                 'Group name suffix',

+                 '')

+         )

+ 

+     @property

+     def prefix(self):

+         return self.get_config_value('prefix')

+ 

+     @property

+     def suffix(self):

+         return self.get_config_value('suffix')

+ 

+     def authorize_user(self, provplugname, provinfo, user, attributes):

+         if 'groups' in attributes:

+             groups = attributes['groups']

+         elif '_groups' in attributes:

+             groups = attributes['_groups']

+         else:

+             return None

+ 

+         provname = provinfo.get('name', None)

+         if provname is None:

+             return None

+ 

+         groupname = '%s%s%s' % (self.prefix, provname, self.suffix)

+ 

+         self.debug('Looking for group "%s" in user groups' % groupname)

+ 

+         if groupname in groups:

+             return True

+         else:

+             return None

+ 

+ 

+ class Installer(AuthzProviderInstaller):

+     def __init__(self, *pargs):

+         super(Installer, self).__init__()

+         self.name = 'spgroup'

+         self.pargs = pargs

+ 

+     def install_args(self, group):

+         group.add_argument('--authorization-spgroup', choices=['yes', 'no'],

+                            default='no', dest='authz_spgroup',

+                            help='Use the spgroup authorization provider')

+         group.add_argument('--authorization-spgroup-prefix', action='store',

+                            dest='authz_spgroup_prefix',

+                            help='Group name prefix')

+         group.add_argument('--authorization-spgroup-suffix', action='store',

+                            dest='authz_spgroup_suffix',

+                            help='Group name suffix')

+ 

+     def configure(self, opts, changes):

+         if opts['authz_spgroup'] != 'yes':

+             return

+ 

+         # Add configuration data to database

+         po = PluginObject(*self.pargs)

+         po.name = 'spgroup'

+         po.wipe_data()

+         po.wipe_config_values()

+         config = dict()

+         if 'authz_spgroup_prefix' in opts:

+             config['prefix'] = opts['authz_spgroup_prefix']

+         if 'authz_spgroup_suffix' in opts:

+             config['suffix'] = opts['authz_spgroup_suffix']

+         po.save_plugin_config(config)

+ 

+         # Update global config to add spgroup plugin

+         po.is_enabled = True

+         po.save_enabled_state()

@@ -20,7 +20,8 @@ 

  

  default_sections = ['info_config',

                      'login_config',

-                     'provider_config']

+                     'provider_config',

+                     'authz_config']

  

  

  def sanitize_value(value):

@@ -8,6 +8,7 @@ 

  from ipsilon.info.common import InfoProviderInstall

  from ipsilon.providers.common import ProvidersInstall

  from ipsilon.helpers.common import EnvHelpersInstall

+ from ipsilon.authz.common import AuthzProviderInstall

  from ipsilon.util.data import UserStore

  from ipsilon.tools import files, dbupgrade

  import ConfigParser
@@ -163,7 +164,8 @@ 

      changes = {'env_helper': {},

                 'login_manager': {},

                 'info_provider': {},

-                'auth_provider': {}}

+                'auth_provider': {},

+                'authz_provider': {}}

  

      # Move pre-existing dbs away

      admin_db = cherrypy.config['admin.config.db']
@@ -219,6 +221,19 @@ 

              raise ConfigurationError(msg)

          changes['auth_provider'][plugin_name] = plugin_changes

  

+     logger.info('Configuring Authorization providers')

+     for plugin_name in args['az_order']:

+         try:

+             plugin = plugins['Authz Providers'][plugin_name]

+         except KeyError:

+             sys.exit('Authorization provider %s not installed' % plugin_name)

+         plugin_changes = {}

+         if plugin.configure(args, plugin_changes) == False:

+             msg = 'Configuration of authorization provider %s failed' % \

+                 plugin_name

+             raise ConfigurationError(msg)

+         changes['authz_provider'][plugin_name] = plugin_changes

+ 

      # Save any changes that were made

      install_changes = os.path.join(instance_conf, 'install_changes')

      changes = json.dumps(changes)
@@ -300,6 +315,14 @@ 

          if plugin.unconfigure(args, plugin_changes) == False:

              logger.info('Removal of auth provider %s failed' % plugin_name)

  

+     logger.info('Removing Authorization providers')

+     for plugin_name in plugins.get('Authz Providers', []):

+         plugin = plugins['Authz Providers'][plugin_name]

+         plugin_changes = changes['authz_provider'].get(plugin_name, {})

+         if plugin.unconfigure(args, plugin_changes) == False:

+             logger.info('Removal of authorization provider %s failed' %

+                         plugin_name)

+ 

      logger.info('Removing httpd configuration')

      os.remove(httpd_conf)

      logger.info('Erasing instance configuration')
@@ -317,7 +340,8 @@ 

          'Environment Helpers': EnvHelpersInstall().plugins,

          'Login Managers': LoginMgrsInstall().plugins,

          'Info Provider': InfoProviderInstall().plugins,

-         'Auth Providers': ProvidersInstall().plugins

+         'Auth Providers': ProvidersInstall().plugins,

+         'Authz Providers': AuthzProviderInstall().plugins

      }

      return plugins

  
@@ -355,6 +379,8 @@ 

                          action='version', version='%(prog)s 0.1')

      parser.add_argument('-o', '--login-managers-order', dest='lm_order',

                          help='Comma separated list of login managers')

+     parser.add_argument('--authorization-order', dest='az_order',

+                         help='Comma separated list of authorization plugins')

      parser.add_argument('--hostname',

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

      parser.add_argument('--instance', default='idp',
@@ -391,6 +417,7 @@ 

                               'entries (in minutes, default: 30 minutes)')

  

      lms = []

+     azs = []

  

      for plugin_group in plugins:

          group = parser.add_argument_group(plugin_group)
@@ -398,6 +425,8 @@ 

              plugin = plugins[plugin_group][plugin_name]

              if plugin.ptype == 'login':

                  lms.append(plugin.name)

+             elif plugin.ptype == 'authz':

+                 azs.append(plugin.name)

              plugin.install_args(group)

  

      args = vars(parser.parse_args())
@@ -437,6 +466,17 @@ 

      if len(args['lm_order']) == 0 and args.get('ipa', 'no') != 'yes':

          sys.exit('No login plugins are enabled.')

  

+     if args['az_order'] is None:

+         args['az_order'] = []

+         for name in azs:

+             if args['authz_' + name] == 'yes':

+                 args['az_order'].append(name)

+     else:

+         args['az_order'] = args['az_order'].split(',')

+ 

+     if len(args['az_order']) == 0:

+         sys.exit('No authorization plugins are enabled.')

+ 

      #FIXME: check instance is only alphanums

  

      return args

file modified
+22 -1
@@ -26,6 +26,10 @@ 

                      'email': '%s@example.com' % username,

                      '_groups': [username]

                  }

+                 groups = self.lm.groups

+                 if groups is not None:

+                     self.debug('groups is %s' % repr(groups))

+                     testdata['_groups'].extend(groups)

                  return self.lm.auth_successful(self.trans,

                                                 username, 'password', testdata)

              else:
@@ -71,7 +75,10 @@ 

                  'DISABLE IN PRODUCTION, USE ONLY FOR TEST ' +

                  'Use any username they are all valid, "admin" gives ' +

                  'administrative powers. ' +

-                 'Use the fixed password "ipsilon" for any user')

+                 'Use the fixed password "ipsilon" for any user'),

+             pconfig.List(

+                 'groups',

+                 'Extra groups')

          )

  

      @property
@@ -86,6 +93,10 @@ 

      def password_text(self):

          return self.get_config_value('password text')

  

+     @property

+     def groups(self):

+         return self.get_config_value('groups')

+ 

      def get_tree(self, site):

          self.page = TestAuth(site, self, 'login/testauth')

          return self.page
@@ -101,6 +112,8 @@ 

      def install_args(self, group):

          group.add_argument('--testauth', choices=['yes', 'no'], default='no',

                             help='Configure PAM authentication')

+         group.add_argument('--testauth-groups', action='store',

+                            help='Extra groups for the testauth user')

  

      def configure(self, opts, changes):

          if opts['testauth'] != 'yes':
@@ -111,6 +124,14 @@ 

          po = PluginObject(*self.pargs)

          po.name = 'testauth'

          po.wipe_data()

+         po.wipe_config_values()

+ 

+         config = dict()

+         if opts['testauth_groups'] is not None:

+             cherrypy.log('testauth_groups is %s (%s)' % (

+                 opts['testauth_groups'], type(opts['testauth_groups'])))

+             config['groups'] = opts['testauth_groups']

+         po.save_plugin_config(config)

  

          # Update global config to add login plugin

          po.is_enabled = True

@@ -133,6 +133,16 @@ 

          if request.trust_root in self.cfg.untrusted_roots:

              raise UnauthorizedRequest("Untrusted Relying party")

  

+         # Perform authorization check.

+         provinfo = {

+             'url': kwargs.get('openid.realm', None)

+         }

+         if not self._site['authz'].authorize_user('openid', provinfo,

+                                                   user.name,

+                                                   us.get_user_attrs()):

+             self.error('Authorization denied by authorization provider')

+             raise UnauthorizedRequest('Authorization denied')

+ 

          # if the party is explicitly whitelisted just respond

          if request.trust_root in self.cfg.trusted_roots:

              return self._respond(self._response(request, us))

@@ -110,5 +110,7 @@ 

              for index in table.indexes:

                  self._db.add_index(index)

              return 2

+         elif old_version == 2:

+             return 3

          else:

              raise NotImplementedError()

@@ -129,6 +129,22 @@ 

          return self._respond(request, {'error': error,

                                         'error_description': message})

  

+     def _authz_stack_check(self, request_data, client, username, userattrs):

+         provinfo = client.copy()

+         provinfo['url'] = provinfo.pop('client_uri')

+         if provinfo['ipsilon_internal']['trusted']:

+             # Trusted OpenIDC clients are added by an Ipsilon admin, so we can

+             # safely use the client name

+             provinfo['name'] = provinfo.pop('client_name')

+ 

+         if not self._site['authz'].authorize_user('openidc', provinfo,

+                                                   username, userattrs):

+             self.error('Authorization denied by authorization provider')

+             return self._respond_error(request_data, 'access_denied',

+                                        'authorization denied')

+         else:

+             return None

+ 

  

  class APIError(cherrypy.HTTPError, Log):

  
@@ -594,6 +610,13 @@ 

              self.debug('Redirecting: %s' % redirect)

              raise cherrypy.HTTPRedirect(redirect)

  

+         # Return error if authz check fails

+         authz_check_res = self._authz_stack_check(request_data, client,

+                                                   user.name,

+                                                   us.get_user_attrs())

+         if authz_check_res:

+             return authz_check_res

+ 

          self.trans.store(data)

          # The user was already signed on, and no request to re-assert its

          # identity. Let's forward directly to /Continue/
@@ -739,6 +762,13 @@ 

                                         'unauthorized_client',

                                         'Unknown client ID')

  

+         # Return error if authz check fails

+         authz_check_res = self._authz_stack_check(request_data, client,

+                                                   user.name,

+                                                   us.get_user_attrs())

+         if authz_check_res:

+             return authz_check_res

+ 

          userattrs = self._source_attributes(us)

          if client['ipsilon_internal']['trusted']:

              # No consent needed, approve

@@ -255,6 +255,21 @@ 

  

          self.debug("%s's attributes: %s" % (user.name, attributes))

  

+         # Perform authorization check.

+         # We use the raw userattrs here so that we can make decisions based

+         # on attributes we don't want to send to the SP

+         provinfo = {

+             'name': provider.name,

+             'url': provider.splink,

+             'owner': provider.owner

+         }

+         if not self._site['authz'].authorize_user('saml2', provinfo, user.name,

+                                                   userattrs):

+             self.trans.wipe()

+             self.error('Authorization denied by authorization provider')

+             raise AuthenticationError("Authorization denied",

+                                       lasso.SAML2_STATUS_CODE_AUTHN_FAILED)

+ 

          # TODO: get authentication type fnd name format from session

          # need to save which login manager authenticated and map it to a

          # saml2 authentication context

file modified
+5
@@ -11,8 +11,10 @@ 

  from ipsilon.admin.info import InfoPlugins

  from ipsilon.admin.login import LoginPlugins

  from ipsilon.admin.providers import ProviderPlugins

+ from ipsilon.admin.authz import AuthzPlugins

  from ipsilon.rest.common import Rest

  from ipsilon.rest.providers import RestProviderPlugins

+ from ipsilon.authz.common import Authz

  import cherrypy

  

  sites = dict()
@@ -34,6 +36,8 @@ 

          cherrypy.config['error_page.404'] = errors.Error_404(self._site)

          cherrypy.config['error_page.500'] = errors.Errors(self._site)

  

+         self._site['authz'] = Authz(self._site)

+ 

          # set up WebFinger endpoint

          self.webfinger = WebFinger(self._site)

  
@@ -50,6 +54,7 @@ 

          self.stack = LoginStack(self._site, self.admin)

          LoginPlugins(self._site, self.stack)

          InfoPlugins(self._site, self.stack)

+         AuthzPlugins(self._site, self.stack)

          ProviderPlugins(self._site, self.admin)

          RestProviderPlugins(self._site, self.rest)

  

file modified
+2 -1
@@ -99,7 +99,8 @@ 

      userstore = UserStore()

      for facility in ['provider_config',

                       'login_config',

-                      'info_config']:

+                      'info_config',

+                      'authz_config']:

          for plugin in root._site[facility].enabled:

              logger.debug('Handling plugin %s', plugin)

              plugin = root._site[facility].available[plugin]

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

  import time

  

  

- CURRENT_SCHEMA_VERSION = 2

+ CURRENT_SCHEMA_VERSION = 3

  OPTIONS_TABLE = {'columns': ['name', 'option', 'value'],

                   'primary_key': ('name', 'option'),

                   'indexes': [('name',)]
@@ -672,7 +672,8 @@ 

          for table in ['config',

                        'info_config',

                        'login_config',

-                       'provider_config']:

+                       'provider_config',

+                       'authz_config']:

              q = self._query(self._db, table, OPTIONS_TABLE, trans=False)

              q.create()

              q._con.close()  # pylint: disable=protected-access
@@ -691,6 +692,13 @@ 

                  for index in table.indexes:

                      self._db.add_index(index)

              return 2

+         elif old_version == 2:

+             # Version 3 adds the authz config table

+             q = self._query(self._db, 'authz_config', OPTIONS_TABLE,

+                             trans=False)

+             q.create()

+             q._con.close()  # pylint: disable=protected-access

+             return 3

          else:

              raise NotImplementedError()

  
@@ -736,6 +744,8 @@ 

              for index in table.indexes:

                  self._db.add_index(index)

              return 2

+         elif old_version == 2:

+             return 3

          else:

              raise NotImplementedError()

  
@@ -770,6 +780,8 @@ 

              for index in table.indexes:

                  self._db.add_index(index)

              return 2

+         elif old_version == 2:

+             return 3

          else:

              raise NotImplementedError()

  
@@ -889,5 +901,7 @@ 

              for index in table.indexes:

                  self._db.add_index(index)

              return 2

+         elif old_version == 2:

+             return 3

          else:

              raise NotImplementedError()

@@ -34,6 +34,8 @@ 

              for index in table.indexes:

                  self._db.add_index(index)

              return 2

+         elif old_version == 2:

+             return 3

          else:

              raise NotImplementedError()

  

file modified
+2
@@ -41,6 +41,8 @@ 

                                     '${workdir}/openidc.key');

  INSERT INTO provider_config VALUES('openidc', 'idp sig key id',

                                     'quickstart');

+ CREATE TABLE authz_config (name TEXT,option TEXT,value TEXT);

+ INSERT INTO authz_config VALUES('global', 'enabled', 'allow');

  '''

  

  USERS_TEMPLATE='''

@@ -11,12 +11,15 @@ 

     xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"

     xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"

     sodipodi:docname="ipsilon-scheme.svg"

-    inkscape:version="0.48.4 r9939"

+    inkscape:version="0.91 r13725"

     version="1.1"

     id="svg6015"

     height="100%"

     width="100%"

-    viewBox="0 0 1004 609">

+    viewBox="0 0 1004 609"

+    inkscape:export-filename="/home/merlin/Desktop/ipsilon-scheme-authz.png"

+    inkscape:export-xdpi="90"

+    inkscape:export-ydpi="90">

    <defs

       id="defs6017">

      <linearGradient
@@ -229,6 +232,20 @@ 

         x2="679.04138"

         y2="264.31851"

         gradientUnits="userSpaceOnUse" />

+     <marker

+        inkscape:stockid="Arrow1Mend"

+        orient="auto"

+        refY="0"

+        refX="0"

+        id="Arrow1Mend-36"

+        style="overflow:visible">

+       <path

+          id="path7303-7"

+          d="M 0,0 5,-5 -12.5,0 5,5 0,0 Z"

+          style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"

+          transform="matrix(-0.4,0,0,-0.4,-4,0)"

+          inkscape:connector-curvature="0" />

+     </marker>

    </defs>

    <sodipodi:namedview

       inkscape:document-units="cm"
@@ -237,17 +254,17 @@ 

       borderopacity="1.0"

       inkscape:pageopacity="0.0"

       inkscape:pageshadow="2"

-      inkscape:zoom="0.79"

-      inkscape:cx="565.54868"

-      inkscape:cy="238.31961"

+      inkscape:zoom="1.182266"

+      inkscape:cx="560.82442"

+      inkscape:cy="304.5"

       inkscape:current-layer="layer1"

       id="namedview6019"

       showgrid="false"

-      inkscape:window-width="1206"

-      inkscape:window-height="801"

-      inkscape:window-x="512"

-      inkscape:window-y="96"

-      inkscape:window-maximized="0"

+      inkscape:window-width="1680"

+      inkscape:window-height="951"

+      inkscape:window-x="0"

+      inkscape:window-y="28"

+      inkscape:window-maximized="1"

       showguides="true"

       inkscape:guide-bbox="true"

       units="pc"
@@ -542,9 +559,9 @@ 

         sodipodi:nodetypes="cscccsccc" />

      <a

         xlink:href="{{ loginstack_url }}"

-        style="fill-opacity:0;stroke-opacity:0"

+        id="info_plugins"

         target="_parent"

-        id="info_plugins">

+        style="fill-opacity:0;stroke-opacity:0">

        <text

           sodipodi:linespacing="125%"

           id="text7243-3"
@@ -646,9 +663,9 @@ 

      </a>

      <a

         xlink:href="{{ loginstack_url }}"

-        style="fill-opacity:0;stroke-opacity:0"

+        id="login_plugins"

         target="_parent"

-        id="login_plugins">

+        style="fill-opacity:0;stroke-opacity:0">

        <g

           id="g26678"

           transform="matrix(1.5529471,0,0,1.3015327,279.56471,189.48728)"
@@ -710,9 +727,9 @@ 

      </a>

      <a

         xlink:href="{{ providers_url }}"

-        style="fill-opacity:0;stroke-opacity:0"

+        id="identity_providers"

         target="_parent"

-        id="identity_providers">

+        style="fill-opacity:0;stroke-opacity:0">

        <text

           sodipodi:linespacing="125%"

           id="text7243-0"
@@ -802,11 +819,11 @@ 

         inkscape:connector-curvature="0"

         sodipodi:nodetypes="czc" />

      <path

-        style="fill:none;stroke:#000000;stroke-width:1.77165353;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:5.31496063, 5.31496063;stroke-dashoffset:0;marker-end:url(#Arrow1Mend)"

-        d="m 358.28105,563.9826 c 0,0 157.70528,55.76087 243.03797,-108.86076 85.33269,-164.62163 356.96204,-36.70886 356.96204,-36.70886"

+        style="fill:none;stroke:#000000;stroke-width:1.77165353;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:5.31496063, 5.31496063;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#Arrow1Mend)"

+        d="m 358.28105,563.9826 c 79.27689,50.14243 209.13438,52.4621 292.11668,6.68038"

         id="path7288-8-7"

         inkscape:connector-curvature="0"

-        sodipodi:nodetypes="czc" />

+        sodipodi:nodetypes="cc" />

      <g

         style="display:inline"

         id="webpage"
@@ -893,5 +910,61 @@ 

           y="495.29861"

           id="tspan9095-0-7"

           style="font-size:18px">Resource</tspan></text>

+     <path

+        style="fill:none;stroke:#000000;stroke-width:1.77165353;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:5.31496063, 5.31496063;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#Arrow1Mend-36)"

+        d="m 717.27806,514.68673 c 89.0557,-71.85507 165.78853,-72.16041 243.90417,-69.44463"

+        id="path7288-8-7-5"

+        inkscape:connector-curvature="0"

+        sodipodi:nodetypes="cc" />

+     <a

+        xlink:href="{{ loginstack_url }}"

+        target="_parent"

+        id="authz_plugins"

+        style="fill-opacity:0;stroke-opacity:0">

+         <g

+            id="g3"

+            transform="matrix(1.6994694,0,0,1.6994694,660.36243,514.38775)"

+            style="fill-opacity:1;display:inline">

+           <path

+              style="fill:#58595b"

+              inkscape:connector-curvature="0"

+              d="m 10.45,0 c 0,0 2.007,2.856 4.861,3.895 2.854,1.039 4.404,0.372 7.108,1.356 2.705,0.984 4.256,2.779 7.109,3.818 2.855,1.039 4.861,-0.356 4.861,-0.356 l 7.057,9.481 c -2.631,0.595 -4.391,2.78 -4.391,6.007 0,2.048 0.738,4.204 1.9,6.127 1.268,2.265 2.037,4.807 2.037,7.144 0,2.873 -1.094,5.099 -2.893,6.439 -0.693,0.515 -3.85,2.484 -4.791,2.889 -1.984,0.852 -4.51,0.875 -7.262,-0.127 L 25.8,46.584 C 22.804,45.493 20.221,46.292 19.048,48.37 17.874,45.438 15.243,42.741 12.246,41.65 L 12.001,41.561 C 5.625,39.24 0.453,32.291 0.453,26.041 c 0,-2.337 0.77,-4.318 2.038,-5.66 C 3.652,19.302 4.39,17.684 4.39,15.635 4.39,12.407 2.631,8.942 0,6.432 L 10.45,0 l 0,0 z"

+              id="path5" />

+           <path

+              style="fill:#a7a9ac"

+              inkscape:connector-curvature="0"

+              d="m 37.1,19.004 -6.012,-8.08 c 0.871,0.126 1.775,0.101 2.723,-0.141 l 4.943,6.75 c -0.611,0.421 -1.17,0.911 -1.654,1.471 l 0,0 z M 18.714,5.808 c 4.625,0.062 7.934,4.342 12.143,5.079 -0.5,0.287 -2.309,1.149 -4.723,0.271 C 23.28,10.119 21.73,8.325 19.026,7.34 16.321,6.356 14.771,7.022 11.917,5.983 10.44,5.446 9.191,4.422 8.329,3.56 l 2.073,-0.939 c 1.902,2.077 3.619,3.124 8.312,3.187 l 0,0 z m 16.958,15.641 c -0.291,0.818 -0.451,1.719 -0.451,2.699 0,2.486 0.877,4.973 2.154,7.104 1.139,2.034 1.781,4.241 1.781,6.166 0,2.641 -0.738,4.096 -1.924,4.847 0.24,-0.82 0.367,-1.724 0.367,-2.704 0,-2.337 -0.77,-4.878 -2.037,-7.143 -1.162,-1.925 -1.9,-4.08 -1.9,-6.128 0,-2.111 0.752,-3.776 2.01,-4.841 l 0,0 z"

+              id="path7" />

+           <path

+              style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd"

+              inkscape:connector-curvature="0"

+              d="m 23.521,35.676 -5.046,-5.014 -5.004,1.329 c 1.425,1.527 3.166,2.753 5.044,3.438 1.862,0.677 3.589,0.723 5.006,0.247 l 0,0 z m -10.219,-3.868 1.45,-5.206 -4.602,-5.51 c -0.246,0.717 -0.378,1.525 -0.378,2.412 0,2.875 1.388,5.931 3.53,8.304 l 0,0 z m 5.134,-13.893 c -3.854,-1.369 -7.112,-0.026 -8.24,3.05 l 5.979,1.778 2.261,-4.828 0,0 z m 8.427,9.207 -4.666,2.19 1.463,6.314 c 2.18,-0.796 3.598,-2.854 3.598,-5.759 0,-0.905 -0.139,-1.83 -0.395,-2.745 l 0,0 z M 18.515,15.681 c 6.079,2.212 11.004,8.933 11.004,15.01 0,6.078 -4.926,9.211 -11.004,6.999 C 12.437,35.479 7.51,28.758 7.51,22.681 c 0,-6.078 4.927,-9.212 11.005,-7 l 0,0 z m 8.321,11.346 c -1.133,-3.926 -4.43,-7.668 -8.321,-9.084 l 2.261,6.475 6.06,2.609 0,0 z"

+              id="path9" />

+         </g>

+         <text

+            xml:space="preserve"

+            style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:'Bitstream Vera Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-opacity:0"

+            x="744.44562"

+            y="549.92511"

+            id="text7243-3-5"

+            sodipodi:linespacing="125%"><tspan

+              sodipodi:role="line"

+              id="tspan7245-8-6"

+              x="744.44562"

+              y="549.92511"

+              style="font-size:24px">Authorization</tspan><tspan

+              sodipodi:role="line"

+              x="744.44562"

+              y="579.92511"

+              id="tspan7247-7-2"

+              style="font-size:24px">Plugins</tspan></text>

+         <rect

+            y="509.10779"

+            x="654.2121"

+            height="94.72995"

+            width="230.85495"

+            id="rect5538-0-2"

+            style="fill:#000000;stroke:#585855;stroke-width:1.68737376;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" />

+     </a>

    </g>

  </svg>

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

+ #!/usr/bin/python

+ #

+ # Copyright (C) 2016 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}',

+          'instance': '${NAME}',

+          'testauth': 'yes',

+          'testauth_groups': 'sp1',

+          'authz_allow': 'yes',

+          'authz_deny': 'no',

+          'authz_spgroup': 'no',

+          'pam': 'no',

+          'gssapi': 'no',

+          'ipa': 'no',

+          'server_debugging': 'True'}

+ 

+ 

+ sp1_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'}

+ 

+ 

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

+          'saml_idp_metadata': 'https://127.0.0.10:45080/idp1/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_metadata': 'https://127.0.0.10:45080/idp1/saml2/metadata',

+          'saml_auth': '/sp',

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

+ 

+ 

+ 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__('authz', __file__)

+ 

+     def setup_servers(self, env=None):

+         print "Installing IDP server"

+         name = 'idp1'

+         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(sp1_g, sp1_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'

+         addr = '127.0.0.12'

+         port = '45082'

+         sp = self.generate_profile(sp2_g, sp2_a, name, addr, port)

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

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

+     sp1name = 'sp1'

+     sp2name = 'sp2'

+     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.12:45082')

+ 

+     print "authz: 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 "authz: Add SP1 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 "authz: Add SP2 Metadata to IDP ...",

+     try:

+         sess.add_sp_metadata(idpname, sp2name)

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

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

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "authz: Access SP1 when authz stack set to allow ...",

+     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 "authz: Set IDP authz stack to deny ...",

+     try:

+         sess.disable_plugin(idpname, 'authz', 'allow')

+         sess.enable_plugin(idpname, 'authz', 'deny')

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

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

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     sess2 = HttpSessions()

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

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

+ 

+     print "authz: Fail access SP1 when authz stack set to deny, with " \

+         "pre-auth ...",

+     try:

+         sess2.auth_to_idp(idpname)

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

+         page.expected_status(401)

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

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

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     sess3 = HttpSessions()

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

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

+ 

+     print "authz: Fail access SP1 when authz stack set to deny, without " \

+         "pre-auth ...",

+     try:

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

+         page.expected_status(401)

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

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

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "authz: Set IDP authz stack to spgroup ...",

+     try:

+         sess.disable_plugin(idpname, 'authz', 'deny')

+         sess.enable_plugin(idpname, 'authz', 'spgroup')

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

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

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     sess4 = HttpSessions()

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

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

+     sess4.add_server(sp2name, 'https://127.0.0.12:45082')

+ 

+     print "authz: Access SP1 when authz stack set to spgroup ...",

+     try:

+         sess4.auth_to_idp(idpname)

+         page = sess4.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 "authz: Fail to access SP2 when authz stack set to spgroup ...",

+     try:

+         page = sess4.fetch_page(idpname, 'https://127.0.0.12:45082/sp/')

+         page.expected_status(401)

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

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

+         sys.exit(1)

+     print " SUCCESS"

@@ -0,0 +1,69 @@ 

+ PRAGMA foreign_keys=OFF;

+ BEGIN TRANSACTION;

+ CREATE TABLE dbinfo (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT

+ );

+ INSERT INTO "dbinfo" VALUES('AdminStore_schema','version','2');

+ CREATE TABLE config (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT

+ );

+ CREATE TABLE info_config (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT

+ );

+ CREATE TABLE login_config (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT

+ );

+ INSERT INTO "login_config" VALUES('global','enabled','testauth');

+ CREATE TABLE provider_config (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT

+ );

+ INSERT INTO "provider_config" VALUES('openid','endpoint url','http://127.0.0.11:45081/idp_v1/openid/');

+ INSERT INTO "provider_config" VALUES('openid','database url','openid.sqlite');

+ INSERT INTO "provider_config" VALUES('openid','identity url template','http://127.0.0.11:45081/idp_v1/openid/id/%(username)s');

+ INSERT INTO "provider_config" VALUES('openid','enabled extensions','');

+ INSERT INTO "provider_config" VALUES('global','enabled','openid,persona,saml2');

+ INSERT INTO "provider_config" VALUES('persona','allowed domains','127.0.0.11:45081');

+ INSERT INTO "provider_config" VALUES('persona','issuer domain','127.0.0.11:45081');

+ INSERT INTO "provider_config" VALUES('persona','idp key file','persona/persona.key');

+ INSERT INTO "provider_config" VALUES('saml2','idp nameid salt','6c78ae3b33db4fe4886edb1679490821');

+ INSERT INTO "provider_config" VALUES('saml2','idp metadata validity','1825');

+ INSERT INTO "provider_config" VALUES('saml2','idp certificate file','saml2/idp.pem');

+ INSERT INTO "provider_config" VALUES('saml2','idp key file','saml2/idp.key');

+ INSERT INTO "provider_config" VALUES('saml2','session database url','saml2.sessions.db.sqlite');

+ INSERT INTO "provider_config" VALUES('saml2','idp metadata file','metadata.xml');

+ INSERT INTO "provider_config" VALUES('saml2','idp storage path','saml2');

+ CREATE TABLE testauth_data (

+ 	uuid TEXT NOT NULL, 

+ 	name TEXT NOT NULL, 

+ 	value TEXT

+ );

+ CREATE TABLE openid_data (

+ 	uuid TEXT NOT NULL, 

+ 	name TEXT NOT NULL, 

+ 	value TEXT

+ );

+ CREATE TABLE persona_data (

+ 	uuid TEXT NOT NULL, 

+ 	name TEXT NOT NULL, 

+ 	value TEXT

+ );

+ CREATE TABLE saml2_data (

+ 	uuid TEXT NOT NULL, 

+ 	name TEXT NOT NULL, 

+ 	value TEXT

+ );

+ CREATE INDEX idx_config_name ON config (name);

+ CREATE INDEX idx_info_config_name ON info_config (name);

+ CREATE INDEX idx_login_config_name ON login_config (name);

+ CREATE INDEX idx_provider_config_name ON provider_config (name);

+ COMMIT;

@@ -0,0 +1,21 @@ 

+ PRAGMA foreign_keys=OFF;

+ BEGIN TRANSACTION;

+ CREATE TABLE dbinfo (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT

+ );

+ INSERT INTO "dbinfo" VALUES('OpenIDStore_schema','version','2');

+ CREATE TABLE association (

+ 	uuid TEXT NOT NULL, 

+ 	name TEXT NOT NULL, 

+ 	value TEXT

+ );

+ CREATE TABLE openid_extensions (

+     name TEXT NOT NULL,

+     option TEXT NOT NULL,

+     value TEXT

+ );

+ CREATE INDEX idx_association_uuid ON association (uuid);

+ CREATE INDEX idx_openid_extensions_name ON openid_extensions (name);

+ COMMIT;

@@ -0,0 +1,32 @@ 

+ PRAGMA foreign_keys=OFF;

+ BEGIN TRANSACTION;

+ CREATE TABLE dbinfo (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT, 

+ 	PRIMARY KEY (name, option)

+ );

+ INSERT INTO "dbinfo" VALUES('OpenIDCStore_schema','version','2');

+ CREATE TABLE client (

+ 	uuid TEXT NOT NULL, 

+ 	name TEXT NOT NULL, 

+ 	value TEXT, 

+ 	PRIMARY KEY (uuid, name)

+ );

+ CREATE TABLE token (

+ 	uuid TEXT NOT NULL, 

+ 	name TEXT NOT NULL, 

+ 	value TEXT, 

+ 	PRIMARY KEY (uuid, name)

+ );

+ CREATE TABLE userinfo (

+ 	uuid TEXT NOT NULL, 

+ 	name TEXT NOT NULL, 

+ 	value TEXT, 

+ 	PRIMARY KEY (uuid, name)

+ );

+ CREATE INDEX idx_dbinfo_name ON dbinfo (name);

+ CREATE INDEX idx_client_uuid ON client (uuid);

+ CREATE INDEX idx_token_uuid ON token (uuid);

+ CREATE INDEX idx_userinfo_uuid ON userinfo (uuid);

+ COMMIT;

@@ -0,0 +1,15 @@ 

+ PRAGMA foreign_keys=OFF;

+ BEGIN TRANSACTION;

+ CREATE TABLE saml2_sessions (

+ 	uuid TEXT NOT NULL, 

+ 	name TEXT NOT NULL, 

+ 	value TEXT

+ );

+ CREATE TABLE dbinfo (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT

+ );

+ INSERT INTO "dbinfo" VALUES('SAML2SessionStore_schema','version','2');

+ CREATE INDEX idx_saml2_sessions_uuid ON saml2_sessions (uuid);

+ COMMIT;

@@ -0,0 +1,15 @@ 

+ PRAGMA foreign_keys=OFF;

+ BEGIN TRANSACTION;

+ CREATE TABLE dbinfo (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT

+ );

+ INSERT INTO "dbinfo" VALUES('TranStore_schema','version','2');

+ CREATE TABLE transactions (

+ 	uuid TEXT NOT NULL, 

+ 	name TEXT NOT NULL, 

+ 	value TEXT

+ );

+ CREATE INDEX idx_transactions_uuid ON transactions (uuid);

+ COMMIT;

@@ -0,0 +1,43 @@ 

+ PRAGMA foreign_keys=OFF;

+ BEGIN TRANSACTION;

+ CREATE TABLE dbinfo (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT

+ );

+ INSERT INTO "dbinfo" VALUES('UserStore_schema','version','2');

+ CREATE TABLE users (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT

+ );

+ CREATE TABLE openid_data (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT, 

+ 	PRIMARY KEY (name, option)

+ );

+ CREATE TABLE persona_data (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT, 

+ 	PRIMARY KEY (name, option)

+ );

+ CREATE TABLE saml2_data (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT, 

+ 	PRIMARY KEY (name, option)

+ );

+ CREATE TABLE testauth_data (

+ 	name TEXT NOT NULL, 

+ 	option TEXT NOT NULL, 

+ 	value TEXT, 

+ 	PRIMARY KEY (name, option)

+ );

+ CREATE INDEX idx_users_name ON users (name);

+ CREATE INDEX idx_openid_data_name ON openid_data (name);

+ CREATE INDEX idx_persona_data_name ON persona_data (name);

+ CREATE INDEX idx_saml2_data_name ON saml2_data (name);

+ CREATE INDEX idx_testauth_data_name ON testauth_data (name);

+ COMMIT;

file modified
+20 -14
@@ -40,6 +40,16 @@ 

      def setup_servers(self, env=None):

          pass

  

+     def dump_admin_config_db(self, db_outdir):

+         test_db = os.path.join(db_outdir, 'adminconfig.sqlite')

+         p = subprocess.Popen(['/usr/bin/sqlite3', test_db, '.dump'],

+                              stdout=subprocess.PIPE)

+         output, _ = p.communicate()

+         if p.returncode:

+             print 'Sqlite dump failed'

+             sys.exit(1)

+         return output

+ 

      def test_upgrade_from(self, env, old_version):

          # Setup IDP Server

          print "Installing IDP server to test upgrade from %i" % old_version
@@ -79,13 +89,7 @@ 

          if old_version == 0:

              # Check all features in a newly created database

              # Let's verify if at least one index was created

-             test_db = os.path.join(db_outdir, 'adminconfig.sqlite')

-             p = subprocess.Popen(['/usr/bin/sqlite3', test_db, '.dump'],

-                                  stdout=subprocess.PIPE)

-             output, _ = p.communicate()

-             if p.returncode:

-                 print 'Sqlite dump failed'

-                 sys.exit(1)

+             output = self.dump_admin_config_db(db_outdir)

              if 'CREATE INDEX' not in output:

                  raise Exception('Database upgrade did not introduce index')

              if 'PRIMARY KEY' not in output:
@@ -94,17 +98,19 @@ 

          elif old_version == 1:

              # In 1 -> 2, we added indexes and primary keys

              # Let's verify if at least one index was created

-             test_db = os.path.join(db_outdir, 'adminconfig.sqlite')

-             p = subprocess.Popen(['/usr/bin/sqlite3', test_db, '.dump'],

-                                  stdout=subprocess.PIPE)

-             output, _ = p.communicate()

-             if p.returncode:

-                 print 'Sqlite dump failed'

-                 sys.exit(1)

+             output = self.dump_admin_config_db(db_outdir)

              if 'CREATE INDEX' not in output:

                  raise Exception('Database upgrade did not introduce index')

              # SQLite did not support creating primary keys, so we can't test

  

+         elif old_version == 2:

+             # Version 3 added the authz_config table

+             # Make sure it exists

+             output = self.dump_admin_config_db(db_outdir)

+             if 'TABLE authz_config' not in output:

+                 raise Exception('Database upgrade did not introduce ' +

+                                 'authz_config table')

+ 

          # Start the httpd server

          http_server = self.start_http_server(conf, env)

  

file modified
+2
@@ -57,6 +57,8 @@ 

  811d0231-9362-46c9-a105-a01a64818904 type = SP

  811d0231-9362-46c9-a105-a01a64818904 name = ${SPNAME}

  811d0231-9362-46c9-a105-a01a64818904 metadata = ${SPMETA}

+ [authz_config]

+ global enabled = allow

  """

  

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

file modified
+64
@@ -51,6 +51,12 @@ 

          if value != expected:

              raise ValueError("Expected [%s], got [%s]" % (expected, value))

  

+     def expected_status(self, expected):

+         status = self.result.status_code

+         if status != expected:

+             raise ValueError("Expected HTTP status [%d], got [%d]" %

+                              (expected, status))

+ 

  

  class HttpSessions(object):

  
@@ -459,6 +465,64 @@ 

          if r.status_code != 200:

              raise ValueError('Failed to post IDP data [%s]' % repr(r))

  

+     def enable_plugin(self, idp, plugtype, plugin):

+         """

+         Enable a login stack plugin.

+ 

+         plugtype must be one of 'login', 'info', or 'authz'

+ 

+         plugin must be the name of the plugin to enable

+         """

+         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)

+         headers = {'referer': rurl}

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

+         if r.status_code != 200:

+             raise ValueError('Failed to enable plugin [%s]' % repr(r))

+ 

+     def disable_plugin(self, idp, plugtype, plugin):

+         """

+         Disable a login stack plugin.

+ 

+         plugtype must be one of 'login', 'info', or 'authz'

+ 

+         plugin must be the name of the plugin to enable

+         """

+         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)

+         headers = {'referer': rurl}

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

+         if r.status_code != 200:

+             raise ValueError('Failed to disable plugin [%s]' % repr(r))

+ 

+     def set_plugin_order(self, idp, plugtype, order=[]):

+         """

+         Set the order of the specified login stack plugin type.

+ 

+         plugtype must be one of 'login', 'info', or 'authz'

+ 

+         order must be a list of zero or more plugin names in order

+         """

+         idpsrv = self.servers[idp]

+         idpuri = idpsrv['baseuri']

+ 

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

+             idpuri, idp, plugtype)

+         headers = {'referer': url}

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

+         payload = {'order': ','.join(order)}

+         r = idpsrv['session'].post(url, headers=headers, data=payload)

+         if r.status_code != 200:

+             raise ValueError('Failed to post IDP data [%s]' % repr(r))

+ 

      def fetch_rest_page(self, idpname, uri):

          """

          idpname - the name of the IDP to fetch the page from

file modified
+38
@@ -120,3 +120,41 @@ 

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

          sys.exit(1)

      print " SUCCESS"

+ 

+     print "openid: Set IDP authz stack to deny ...",

+     try:

+         sess.disable_plugin(idpname, 'authz', 'allow')

+         sess.enable_plugin(idpname, 'authz', 'deny')

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

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

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     sess2 = HttpSessions()

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

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

+ 

+     print "openid: Run OpenID Protocol with IDP deny, with pre-auth ...",

+     try:

+         sess2.auth_to_idp(idpname)

+         page = sess2.fetch_page(idpname,

+                                 'https://127.0.0.11:45081/?extensions=NO')

+         page.expected_value('text()', 'ERROR: Cancelled')

+     except ValueError as e:

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

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     sess3 = HttpSessions()

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

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

+ 

+     print "openid: Run OpenID Protocol with IDP deny, without pre-auth ...",

+     try:

+         page = sess3.fetch_page(idpname,

+                                 'https://127.0.0.11:45081/?extensions=NO')

+         page.expected_value('text()', 'ERROR: Cancelled')

+     except ValueError as e:

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

+         sys.exit(1)

+     print " SUCCESS"

file modified
+46
@@ -145,6 +145,12 @@ 

      return toreturn

  

  

+ def check_text_results(text, expected):

+     if expected not in text:

+         raise ValueError("Expected text '%s' not found, got '%s'" %

+                          (expected, text))

+ 

+ 

  class IpsilonTest(IpsilonTestBase):

  

      def __init__(self):
@@ -347,3 +353,43 @@ 

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

          sys.exit(1)

      print " SUCCESS"

+ 

+     print "openidc: Set IDP authz stack to deny",

+     try:

+         sess.disable_plugin(idpname, 'authz', 'allow')

+         sess.enable_plugin(idpname, 'authz', 'deny')

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

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

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     sess2 = HttpSessions()

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

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

+ 

+     print "openidc: Access first SP Protected Area with IDP deny, with " \

+         "pre-auth ...",

+     try:

+         sess2.auth_to_idp(idpname)

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

+         check_text_results(page.text,

+                            'OpenID Connect Provider error: access_denied')

+     except ValueError, e:

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

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     sess3 = HttpSessions()

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

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

+ 

+     print "openidc: Access first SP Protected Area with IDP deny, without " \

+         "pre-auth ...",

+     try:

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

+         check_text_results(page.text,

+                            'OpenID Connect Provider error: access_denied')

+     except ValueError, e:

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

+         sys.exit(1)

+     print " SUCCESS"

This system allows SP authentication requests to be authorized in
Ipsilon based on SP and user data. Authorization takes places after the
user has been authenticated, and before a response is sent back to the
SP.

The authorization plugin execution order is defined by via the
loginstack admin page. Each plugin has the option to permit or deny the
user session, or abstain from making a decision. If all configured
plugins abstain, or there are no configured plugins, the session is
permitted. The first plugin to not abstain determines the result of the
authorization process.

Three plugins are included:
- "permit" unconditionally permits all sessions, and exists primarily
for testing purposes
- "deny" unconditionally denies all sessions, and can be used both for
testing, and as a final configured plugin to deny sessions not
explicitly permitted by other plugins
- "spgroup" requires a user to be a member of a group that matches the
name of the SP

As a new database table is added to the adminconfig database, the
database format version has been bumped to version 3. The database
upgrade test suite has been updated to test upgrades to v3.

Authorization support is currently limited to SAML2 SP sessions, but
should be added to other providers.

Signed-off-by: Howard Johnson merlin@merlinthp.org

A first go at authorization support. Currently only supports authz of SAML2 sessions. No test suite yet.

Instead of escaping the ', you could just use " ".

I would personally prefer to just deny by default if nothing allowed, and then add the Allow module by default.
That way, the allowing is at least explicit.

Perhaps name this Allow? My mind finds that easier to interpret.

One of my coding idiosyncrasies - not mixing quoting styles. I think it has something to do with the years I spent writing perl, where "" triggers the string interpolation. I'll switch this to "".

rebased

7 years ago

would it be an idea to also pass the provider plugin name, so that plugins can for example allow a user access to the OpenIDC "intranet" site, while not allowing access to the SAML2 "intranet" site?

rebased

7 years ago

rebased

7 years ago

Updated with Patrick's suggestions.

rebased

7 years ago

And now with the test suite working again.

1 new commit added

  • Add tests for authz code
7 years ago

The test suite patch includes changes to some other supporting code (the login authtest provider, and the http test helper classes). Happy to split those out into separate commits.

This is a hold-over from when the authz code defaulted to accepting sessions if all plugins abstain. Now we deny by default, so I'm not sure when we'd need it, and I'm inclined to remove the option.

3 new commits added

  • Add tests for authz code
  • Add authorization to the SAML2 provider
  • Add a plugin-based authorization system for SP user sessions
7 years ago

Well, I think that the fact that the option exists makes clear what it does if the user is not in the group: that's up to the admin. If the option were not there, it would need more reading of documentation. Perhaps explain instead in the help that if this is disabled, it will fall over to the next module?

Perhaps change this to "Authorization Plugins"? Not everyone might know what authz stands for :)

rebased

7 years ago

rebased

7 years ago

OK, I think I'm happy with this version ;)

You want to use plugins.get('Authz Providers'. []). The problem otherwise is that current installs won't have this in their install.changes file.

rebased

7 years ago

1 new commit added

  • Add authorization support (and test) to OpenID provider
7 years ago

5 new commits added

  • Add authorization support (and tests) to OpenID Connect provider
  • Add authorization support (and tests) to OpenID provider
  • Add tests for authz code
  • Add authorization to the SAML2 provider
  • Add a plugin-based authorization system for SP user sessions
7 years ago

As said on irc, check the "client" object. It has lots and lots more provider info available.

5 new commits added

  • Add authorization support (and tests) to OpenID Connect provider
  • Add authorization support (and tests) to OpenID provider
  • Add tests for authz code
  • Add authorization to the SAML2 provider
  • Add a plugin-based authorization system for SP user sessions
7 years ago

1 new commit added

  • Update main admin page SVG for authorization plugin stack
7 years ago

rebased

7 years ago

This last occurrence need to have the names reversed of provider and provplugname.

rebased

7 years ago

Commit bd67f89 fixes this pull-request

Pull-Request has been merged by merlin@merlinthp.org

7 years ago
Metadata