From 1b3e1425422ec11370f4821a880e6aa837375002 Mon Sep 17 00:00:00 2001 From: Howard Johnson Date: Jul 23 2016 17:58:30 +0000 Subject: [PATCH 1/6] Add a plugin-based authorization system for SP user sessions 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 denied. The first plugin to not abstain determines the result of the authorization process. Three plugins are included: - "allow" unconditionally allows all sessions, and is enabled by default - "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. Signed-off-by: Howard Johnson --- diff --git a/ipsilon/admin/authz.py b/ipsilon/admin/authz.py new file mode 100644 index 0000000..e9861ca --- /dev/null +++ b/ipsilon/admin/authz.py @@ -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' diff --git a/ipsilon/authz/__init__.py b/ipsilon/authz/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ipsilon/authz/__init__.py diff --git a/ipsilon/authz/allow.py b/ipsilon/authz/allow.py new file mode 100644 index 0000000..2613d3a --- /dev/null +++ b/ipsilon/authz/allow.py @@ -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() diff --git a/ipsilon/authz/common.py b/ipsilon/authz/common.py new file mode 100644 index 0000000..bcea4e9 --- /dev/null +++ b/ipsilon/authz/common.py @@ -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() diff --git a/ipsilon/authz/deny.py b/ipsilon/authz/deny.py new file mode 100644 index 0000000..1ff7d03 --- /dev/null +++ b/ipsilon/authz/deny.py @@ -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() diff --git a/ipsilon/authz/spgroup.py b/ipsilon/authz/spgroup.py new file mode 100644 index 0000000..f920c37 --- /dev/null +++ b/ipsilon/authz/spgroup.py @@ -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() diff --git a/ipsilon/install/ipsilon-db2conf b/ipsilon/install/ipsilon-db2conf index ed52942..8d16427 100755 --- a/ipsilon/install/ipsilon-db2conf +++ b/ipsilon/install/ipsilon-db2conf @@ -20,7 +20,8 @@ logger = logging.getLogger(__name__) default_sections = ['info_config', 'login_config', - 'provider_config'] + 'provider_config', + 'authz_config'] def sanitize_value(value): diff --git a/ipsilon/install/ipsilon-server-install b/ipsilon/install/ipsilon-server-install index 0afbf84..1cc83fc 100755 --- a/ipsilon/install/ipsilon-server-install +++ b/ipsilon/install/ipsilon-server-install @@ -8,6 +8,7 @@ from ipsilon.login.common import LoginMgrsInstall 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 @@ def install(plugins, args): 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 @@ def install(plugins, args): 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 @@ def uninstall(plugins, args): 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 @@ def find_plugins(): '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 @@ def parse_args(plugins): 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 @@ def parse_args(plugins): 'entries (in minutes, default: 30 minutes)') lms = [] + azs = [] for plugin_group in plugins: group = parser.add_argument_group(plugin_group) @@ -398,6 +425,8 @@ def parse_args(plugins): 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 @@ def parse_args(plugins): 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 diff --git a/ipsilon/providers/openid/store.py b/ipsilon/providers/openid/store.py index 85fc728..9f1c06c 100644 --- a/ipsilon/providers/openid/store.py +++ b/ipsilon/providers/openid/store.py @@ -110,5 +110,7 @@ class OpenIDStore(Store, OpenIDStoreInterface): for index in table.indexes: self._db.add_index(index) return 2 + elif old_version == 2: + return 3 else: raise NotImplementedError() diff --git a/ipsilon/root.py b/ipsilon/root.py index 5978265..2df2236 100644 --- a/ipsilon/root.py +++ b/ipsilon/root.py @@ -11,8 +11,10 @@ from ipsilon.admin.loginstack import LoginStack 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 @@ class Root(Page): 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 @@ class Root(Page): 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) diff --git a/ipsilon/tools/dbupgrade.py b/ipsilon/tools/dbupgrade.py index 021acf3..b684bce 100644 --- a/ipsilon/tools/dbupgrade.py +++ b/ipsilon/tools/dbupgrade.py @@ -99,7 +99,8 @@ def execute_upgrade(cfgfile): 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] diff --git a/ipsilon/util/data.py b/ipsilon/util/data.py index ed22540..cf7bf21 100644 --- a/ipsilon/util/data.py +++ b/ipsilon/util/data.py @@ -16,7 +16,7 @@ import logging 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 @@ class AdminStore(Store): 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 @@ class AdminStore(Store): 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 @@ class UserStore(Store): 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 @@ class TranStore(Store): 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 @@ class SAML2SessionStore(Store): for index in table.indexes: self._db.add_index(index) return 2 + elif old_version == 2: + return 3 else: raise NotImplementedError() diff --git a/ipsilon/util/sessions.py b/ipsilon/util/sessions.py index e6d24b5..1b35db0 100644 --- a/ipsilon/util/sessions.py +++ b/ipsilon/util/sessions.py @@ -34,6 +34,8 @@ class SessionStore(Store): for index in table.indexes: self._db.add_index(index) return 2 + elif old_version == 2: + return 3 else: raise NotImplementedError() diff --git a/quickrun.py b/quickrun.py index ce7eb94..c509a3f 100755 --- a/quickrun.py +++ b/quickrun.py @@ -41,6 +41,8 @@ INSERT INTO provider_config VALUES('openidc', 'idp key file', '${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=''' diff --git a/tests/blobs/old_dbs/v2/adminconfig.sqlite.dump b/tests/blobs/old_dbs/v2/adminconfig.sqlite.dump new file mode 100644 index 0000000..1653986 --- /dev/null +++ b/tests/blobs/old_dbs/v2/adminconfig.sqlite.dump @@ -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; diff --git a/tests/blobs/old_dbs/v2/openid.sqlite.dump b/tests/blobs/old_dbs/v2/openid.sqlite.dump new file mode 100644 index 0000000..2965b49 --- /dev/null +++ b/tests/blobs/old_dbs/v2/openid.sqlite.dump @@ -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; diff --git a/tests/blobs/old_dbs/v2/openidc.sqlite.dump b/tests/blobs/old_dbs/v2/openidc.sqlite.dump new file mode 100644 index 0000000..068d737 --- /dev/null +++ b/tests/blobs/old_dbs/v2/openidc.sqlite.dump @@ -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; diff --git a/tests/blobs/old_dbs/v2/saml2.sessions.db.sqlite.dump b/tests/blobs/old_dbs/v2/saml2.sessions.db.sqlite.dump new file mode 100644 index 0000000..3b1bc40 --- /dev/null +++ b/tests/blobs/old_dbs/v2/saml2.sessions.db.sqlite.dump @@ -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; diff --git a/tests/blobs/old_dbs/v2/transactions.sqlite.dump b/tests/blobs/old_dbs/v2/transactions.sqlite.dump new file mode 100644 index 0000000..c812f0c --- /dev/null +++ b/tests/blobs/old_dbs/v2/transactions.sqlite.dump @@ -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; diff --git a/tests/blobs/old_dbs/v2/userprefs.sqlite.dump b/tests/blobs/old_dbs/v2/userprefs.sqlite.dump new file mode 100644 index 0000000..897eea6 --- /dev/null +++ b/tests/blobs/old_dbs/v2/userprefs.sqlite.dump @@ -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; diff --git a/tests/dbupgrades.py b/tests/dbupgrades.py index bfa3788..0d06a57 100755 --- a/tests/dbupgrades.py +++ b/tests/dbupgrades.py @@ -40,6 +40,16 @@ class IpsilonTest(IpsilonTestBase): 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 @@ class IpsilonTest(IpsilonTestBase): 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 @@ class IpsilonTest(IpsilonTestBase): 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) diff --git a/tests/fconf.py b/tests/fconf.py index c2440d4..a5747b1 100755 --- a/tests/fconf.py +++ b/tests/fconf.py @@ -57,6 +57,8 @@ saml2 idp nameid salt = ${IDPSALT} 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', From 9fc5c79a188f3c6c58bc5d9ce8b4fec93a60bab4 Mon Sep 17 00:00:00 2001 From: Howard Johnson Date: Jul 23 2016 17:58:36 +0000 Subject: [PATCH 2/6] Add authorization to the SAML2 provider Signed-off-by: Howard Johnson --- diff --git a/ipsilon/providers/saml2/auth.py b/ipsilon/providers/saml2/auth.py index f89c220..1d16091 100644 --- a/ipsilon/providers/saml2/auth.py +++ b/ipsilon/providers/saml2/auth.py @@ -255,6 +255,21 @@ class AuthenticateRequest(ProviderPageBase): 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 From b24911d08c3ef0dec8241938536d6d37f84f553d Mon Sep 17 00:00:00 2001 From: Howard Johnson Date: Jul 23 2016 17:58:36 +0000 Subject: [PATCH 3/6] Add tests for authz code Signed-off-by: Howard Johnson --- diff --git a/Makefile b/Makefile index e1bfe63..c641cfd 100644 --- a/Makefile +++ b/Makefile @@ -112,6 +112,7 @@ tests: wrappers 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 diff --git a/ipsilon/login/authtest.py b/ipsilon/login/authtest.py index 6b2db5b..9e28de7 100644 --- a/ipsilon/login/authtest.py +++ b/ipsilon/login/authtest.py @@ -26,6 +26,10 @@ class TestAuth(LoginFormBase): '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 @@ Form based TEST login Manager, DO NOT EVER ACTIVATE IN PRODUCTION """ '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 @@ Form based TEST login Manager, DO NOT EVER ACTIVATE IN PRODUCTION """ 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 @@ class Installer(LoginManagerInstaller): 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 @@ class Installer(LoginManagerInstaller): 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 diff --git a/tests/authz.py b/tests/authz.py new file mode 100755 index 0000000..ab8a295 --- /dev/null +++ b/tests/authz.py @@ -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 + + + + Require all granted + + + Order Allow,Deny + Allow from All + + +""" + 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" diff --git a/tests/helpers/http.py b/tests/helpers/http.py index a385353..2bd1dd8 100755 --- a/tests/helpers/http.py +++ b/tests/helpers/http.py @@ -51,6 +51,12 @@ class PageTree(object): 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 @@ class HttpSessions(object): 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 From 49f7f0b9a0867c34f6603815c1136f9da29f2c4b Mon Sep 17 00:00:00 2001 From: Howard Johnson Date: Jul 23 2016 17:58:36 +0000 Subject: [PATCH 4/6] Add authorization support (and tests) to OpenID provider Signed-off-by: Howard Johnson --- diff --git a/ipsilon/providers/openid/auth.py b/ipsilon/providers/openid/auth.py index 16fa5fc..d8f02cd 100644 --- a/ipsilon/providers/openid/auth.py +++ b/ipsilon/providers/openid/auth.py @@ -133,6 +133,16 @@ class AuthenticateRequest(ProviderPageBase): 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)) diff --git a/tests/openid.py b/tests/openid.py index a695096..31dd005 100755 --- a/tests/openid.py +++ b/tests/openid.py @@ -120,3 +120,41 @@ if __name__ == '__main__': 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" From f55ec03e94a95593df292a459c5d2f729972b1ea Mon Sep 17 00:00:00 2001 From: Howard Johnson Date: Jul 23 2016 17:58:36 +0000 Subject: [PATCH 5/6] Add authorization support (and tests) to OpenID Connect provider Signed-off-by: Howard Johnson --- diff --git a/ipsilon/providers/openidc/auth.py b/ipsilon/providers/openidc/auth.py index 46a6722..55beeb8 100644 --- a/ipsilon/providers/openidc/auth.py +++ b/ipsilon/providers/openidc/auth.py @@ -129,6 +129,22 @@ class AuthenticateRequest(ProviderPageBase): 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 @@ class Authorization(AuthenticateRequest): 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 @@ class Continue(AuthenticateRequest): '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 diff --git a/tests/openidc.py b/tests/openidc.py index 665140b..05c015a 100755 --- a/tests/openidc.py +++ b/tests/openidc.py @@ -145,6 +145,12 @@ def check_info_results(text, expected): 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 @@ if __name__ == '__main__': 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" From b0951e779b2153895683fb980266a43e77bdd58d Mon Sep 17 00:00:00 2001 From: Howard Johnson Date: Jul 23 2016 17:58:36 +0000 Subject: [PATCH 6/6] Update main admin page SVG for authorization plugin stack Signed-off-by: Howard Johnson --- diff --git a/templates/admin/ipsilon-scheme.svg b/templates/admin/ipsilon-scheme.svg index ef2ae80..7d8587b 100644 --- a/templates/admin/ipsilon-scheme.svg +++ b/templates/admin/ipsilon-scheme.svg @@ -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"> + + + + style="fill-opacity:0;stroke-opacity:0"> + style="fill-opacity:0;stroke-opacity:0"> + style="fill-opacity:0;stroke-opacity:0"> + sodipodi:nodetypes="cc" /> Resource + + + + + + + + AuthorizationPlugins + +