From 8fe18393e0a3ff27e999a069207d48f782cca9f8 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Thu, 20 Mar 2014 17:54:35 -0400 Subject: [PATCH 01/16] Add a default admin user at install time Signed-off-by: Simo Sorce --- ipsilon/install/server.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ipsilon/install/server.py b/ipsilon/install/server.py index 4c0aef7..4ae0c8f 100755 --- a/ipsilon/install/server.py +++ b/ipsilon/install/server.py @@ -19,6 +19,7 @@ from ipsilon.login.common import LoginMgrsInstall from ipsilon.providers.common import ProvidersInstall +from ipsilon.util.data import Store import argparse import cherrypy import logging @@ -98,6 +99,13 @@ def install(plugins, args): if os.path.exists(admin_db): shutil.move(admin_db, '%s.backup.%s' % (admin_db, now)) + # Rebuild user db + users_db = cherrypy.config['user.prefs.db'] + if os.path.exists(users_db): + shutil.move(users_db, '%s.backup.%s' % (users_db, now)) + db = Store() + db.save_user_preferences(args['admin_user'], {'is_admin': 1}) + logger.info('Configuring login managers') for plugin_name in args['lm_order']: plugin = plugins['Login Managers'][plugin_name] @@ -132,6 +140,8 @@ def parse_args(plugins): help="Machine's fully qualified host name") parser.add_argument('--system-user', default='ipsilon', help="User account used to run the server") + parser.add_argument('--admin-user', default='admin', + help="User account that is assigned admin privileges") parser.add_argument('--ipa', choices=['yes', 'no'], default='yes', help='Detect and use an IPA server for authentication') parser.add_argument('--uninstall', action='store_true', -- 2.20.1 From 1e137be617dba1d0f3f85d594f5625926d3f46e9 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Mon, 24 Mar 2014 16:37:15 -0400 Subject: [PATCH 02/16] Implement plugin ordering configuration Allows to change the login plugins order from the admin configuration page. Signed-off-by: Simo Sorce --- ipsilon/admin/common.py | 75 ++++++++++++++++++++++++++++++++ templates/admin/index.html | 7 ++- templates/admin/login_order.html | 25 +++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 templates/admin/login_order.html diff --git a/ipsilon/admin/common.py b/ipsilon/admin/common.py index 6e36669..10171dc 100755 --- a/ipsilon/admin/common.py +++ b/ipsilon/admin/common.py @@ -20,6 +20,7 @@ from ipsilon.util.data import Store from ipsilon.util.page import Page from ipsilon.util.user import UserSession +from ipsilon.util.plugin import PluginObject import cherrypy from ipsilon.login.common import FACILITY as LOGIN_FACILITY @@ -105,6 +106,78 @@ class LoginPluginPage(Page): return op(*args, **kwargs) +class LoginPluginsOrder(Page): + + def __init__(self, site, baseurl): + super(LoginPluginsOrder, self).__init__(site) + self.url = '%s/order' % baseurl + + @admin_protect + def GET(self, *args, **kwargs): + return self._template('admin/login_order.html', + title='login plugins order', + name='admin_login_order_form', + action=self.url, + options=self._site[LOGIN_FACILITY]['enabled']) + + @admin_protect + def POST(self, *args, **kwargs): + message = "Nothing was modified." + message_type = "info" + valid = self._site[LOGIN_FACILITY]['enabled'] + + if 'order' in kwargs: + order = kwargs['order'].split(',') + if len(order) != 0: + new_values = [] + try: + for v in order: + val = v.strip() + if val not in valid: + error = "Invalid plugin name: %s" % val + raise ValueError(error) + new_values.append(val) + if len(new_values) < len(valid): + for val in valid: + if val not in new_values: + new_values.append(val) + + po = PluginObject() + po.name = "global" + globalconf = dict() + globalconf['order'] = ','.join(new_values) + po.set_config(globalconf) + po.save_plugin_config(LOGIN_FACILITY) + + # When all is saved update also live config + self._site[LOGIN_FACILITY]['enabled'] = new_values + + message = "New configuration saved." + message_type = "success" + + except ValueError, e: + message = str(e) + message_type = "error" + + except Exception, e: # pylint: disable=broad-except + message = "Failed to save data!" + message_type = "error" + + return self._template('admin/login_order.html', + message=message, + message_type=message_type, + title='login plugins order', + name='admin_login_order_form', + action=self.url, + options=self._site[LOGIN_FACILITY]['enabled']) + + def root(self, *args, **kwargs): + cherrypy.log.error("method: %s" % cherrypy.request.method) + op = getattr(self, cherrypy.request.method, self.GET) + if callable(op): + return op(*args, **kwargs) + + class LoginPlugins(Page): def __init__(self, site, baseurl): super(LoginPlugins, self).__init__(site) @@ -115,6 +188,8 @@ class LoginPlugins(Page): obj = self._site[LOGIN_FACILITY]['available'][plugin] self.__dict__[plugin] = LoginPluginPage(obj, self._site, self.url) + self.order = LoginPluginsOrder(self._site, self.url) + class Admin(Page): diff --git a/templates/admin/index.html b/templates/admin/index.html index c22d249..1957a7f 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -18,6 +18,9 @@ {% endfor %}

Plugins order

-

[list here and form button to change?]

+
{{ ', '.join(enabled) }}
+
+ configure +
{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/login_order.html b/templates/admin/login_order.html new file mode 100644 index 0000000..c78fe55 --- /dev/null +++ b/templates/admin/login_order.html @@ -0,0 +1,25 @@ +{% extends "master-admin.html" %} +{% block main %} +

{{ title }}

+ {% if message %} +
+

{{ message }}

+
+ {% endif %} +
+
+ +
+ + +
+ Plugins order + + + Back +
+
+{% endblock %} + -- 2.20.1 From 667901638f082e05b4ac61a14f4ddc07ec987742 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Mon, 24 Mar 2014 17:06:05 -0400 Subject: [PATCH 03/16] Move admin_protect to a more generic module Signed-off-by: Simo Sorce --- ipsilon/admin/common.py | 13 +------------ ipsilon/util/page.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ipsilon/admin/common.py b/ipsilon/admin/common.py index 10171dc..2897237 100755 --- a/ipsilon/admin/common.py +++ b/ipsilon/admin/common.py @@ -19,23 +19,12 @@ from ipsilon.util.data import Store from ipsilon.util.page import Page -from ipsilon.util.user import UserSession +from ipsilon.util.page import admin_protect from ipsilon.util.plugin import PluginObject import cherrypy from ipsilon.login.common import FACILITY as LOGIN_FACILITY -def admin_protect(fn): - - def check(*args, **kwargs): - if UserSession().get_user().is_admin: - return fn(*args, **kwargs) - - raise cherrypy.HTTPError(403) - - return check - - class LoginPluginPage(Page): def __init__(self, obj, site, baseurl): diff --git a/ipsilon/util/page.py b/ipsilon/util/page.py index 56a6463..7dda1d7 100755 --- a/ipsilon/util/page.py +++ b/ipsilon/util/page.py @@ -21,6 +21,17 @@ from ipsilon.util.user import UserSession import cherrypy +def admin_protect(fn): + + def check(*args, **kwargs): + if UserSession().get_user().is_admin: + return fn(*args, **kwargs) + + raise cherrypy.HTTPError(403) + + return check + + def protect(): UserSession().remote_login() -- 2.20.1 From a5be918364744f92d73aa4b6589f3b9b33235d6d Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Mon, 24 Mar 2014 16:59:41 -0400 Subject: [PATCH 04/16] Move login plugin configuration to its own module move also the template, in preparation for handling other configuration data in the main page. Signed-off-by: Simo Sorce --- ipsilon/admin/common.py | 120 +++--------------- ipsilon/admin/login.py | 118 +++++++++++++++++ ipsilon/root.py | 4 +- templates/admin/index.html | 22 +--- templates/admin/login.html | 26 ++++ templates/admin/login_order.html | 2 +- .../{login_plugin.html => plugin_config.html} | 4 +- templates/master-admin.html | 2 +- 8 files changed, 168 insertions(+), 130 deletions(-) create mode 100755 ipsilon/admin/login.py create mode 100644 templates/admin/login.html rename templates/admin/{login_plugin.html => plugin_config.html} (88%) diff --git a/ipsilon/admin/common.py b/ipsilon/admin/common.py index 2897237..0a46797 100755 --- a/ipsilon/admin/common.py +++ b/ipsilon/admin/common.py @@ -17,20 +17,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import cherrypy from ipsilon.util.data import Store from ipsilon.util.page import Page from ipsilon.util.page import admin_protect -from ipsilon.util.plugin import PluginObject -import cherrypy -from ipsilon.login.common import FACILITY as LOGIN_FACILITY -class LoginPluginPage(Page): +class AdminPluginPage(Page): - def __init__(self, obj, site, baseurl): - super(LoginPluginPage, self).__init__(site) + def __init__(self, obj, site, baseurl, facility): + super(AdminPluginPage, self).__init__(site) self._obj = obj self.url = '%s/%s' % (baseurl, obj.name) + self.facility = facility # Get the defaults self.plugin_config = obj.get_config_desc() @@ -43,9 +42,10 @@ class LoginPluginPage(Page): @admin_protect def GET(self, *args, **kwargs): - return self._template('admin/login_plugin.html', + return self._template('admin/plugin_config.html', title='%s plugin' % self._obj.name, - name='admin_login_%s_form' % self._obj.name, + name='admin_%s_%s_form' % (self.facility, + self._obj.name), action=self.url, options=self.plugin_config) @@ -67,7 +67,7 @@ class LoginPluginPage(Page): # First we try to save in the database try: store = Store() - store.save_plugin_config(LOGIN_FACILITY, + store.save_plugin_config(self.facility, self._obj.name, new_values) message = "New configuration saved." message_type = "success" @@ -80,11 +80,12 @@ class LoginPluginPage(Page): self._obj.set_config_value(name, value) self.plugin_config[name][2] = value - return self._template('admin/login_plugin.html', + return self._template('admin/plugin_config.html', message=message, message_type=message_type, title='%s plugin' % self._obj.name, - name='admin_login_%s_form' % self._obj.name, + name='admin_%s_%s_form' % (self.facility, + self._obj.name), action=self.url, options=self.plugin_config) @@ -95,100 +96,11 @@ class LoginPluginPage(Page): return op(*args, **kwargs) -class LoginPluginsOrder(Page): - - def __init__(self, site, baseurl): - super(LoginPluginsOrder, self).__init__(site) - self.url = '%s/order' % baseurl - - @admin_protect - def GET(self, *args, **kwargs): - return self._template('admin/login_order.html', - title='login plugins order', - name='admin_login_order_form', - action=self.url, - options=self._site[LOGIN_FACILITY]['enabled']) - - @admin_protect - def POST(self, *args, **kwargs): - message = "Nothing was modified." - message_type = "info" - valid = self._site[LOGIN_FACILITY]['enabled'] - - if 'order' in kwargs: - order = kwargs['order'].split(',') - if len(order) != 0: - new_values = [] - try: - for v in order: - val = v.strip() - if val not in valid: - error = "Invalid plugin name: %s" % val - raise ValueError(error) - new_values.append(val) - if len(new_values) < len(valid): - for val in valid: - if val not in new_values: - new_values.append(val) - - po = PluginObject() - po.name = "global" - globalconf = dict() - globalconf['order'] = ','.join(new_values) - po.set_config(globalconf) - po.save_plugin_config(LOGIN_FACILITY) - - # When all is saved update also live config - self._site[LOGIN_FACILITY]['enabled'] = new_values - - message = "New configuration saved." - message_type = "success" - - except ValueError, e: - message = str(e) - message_type = "error" - - except Exception, e: # pylint: disable=broad-except - message = "Failed to save data!" - message_type = "error" - - return self._template('admin/login_order.html', - message=message, - message_type=message_type, - title='login plugins order', - name='admin_login_order_form', - action=self.url, - options=self._site[LOGIN_FACILITY]['enabled']) - - def root(self, *args, **kwargs): - cherrypy.log.error("method: %s" % cherrypy.request.method) - op = getattr(self, cherrypy.request.method, self.GET) - if callable(op): - return op(*args, **kwargs) - - -class LoginPlugins(Page): - def __init__(self, site, baseurl): - super(LoginPlugins, self).__init__(site) - self.url = '%s/login' % baseurl - - for plugin in self._site[LOGIN_FACILITY]['available']: - cherrypy.log.error('Admin login plugin: %s' % plugin) - obj = self._site[LOGIN_FACILITY]['available'][plugin] - self.__dict__[plugin] = LoginPluginPage(obj, self._site, self.url) - - self.order = LoginPluginsOrder(self._site, self.url) - - class Admin(Page): - def __init__(self, *args, **kwargs): - super(Admin, self).__init__(*args, **kwargs) - self.url = '%s/admin' % self.basepath - self.login = LoginPlugins(self._site, self.url) + def __init__(self, site, mount): + super(Admin, self).__init__(site) + self.url = '%s/%s' % (self.basepath, mount) def root(self, *args, **kwargs): - login_plugins = self._site[LOGIN_FACILITY] - return self._template('admin/index.html', title='Administration', - available=login_plugins['available'], - enabled=login_plugins['enabled']) + return self._template('admin/index.html', title='Configuration') diff --git a/ipsilon/admin/login.py b/ipsilon/admin/login.py new file mode 100755 index 0000000..1f5e9fa --- /dev/null +++ b/ipsilon/admin/login.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 Simo Sorce +# +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import cherrypy +from ipsilon.util.page import Page +from ipsilon.util.page import admin_protect +from ipsilon.util.plugin import PluginObject +from ipsilon.admin.common import AdminPluginPage +from ipsilon.login.common import FACILITY + + +class LoginPluginsOrder(Page): + + def __init__(self, site, baseurl): + super(LoginPluginsOrder, self).__init__(site) + self.url = '%s/order' % baseurl + + @admin_protect + def GET(self, *args, **kwargs): + return self._template('admin/login_order.html', + title='login plugins order', + name='admin_login_order_form', + action=self.url, + options=self._site[FACILITY]['enabled']) + + @admin_protect + def POST(self, *args, **kwargs): + message = "Nothing was modified." + message_type = "info" + valid = self._site[FACILITY]['enabled'] + + if 'order' in kwargs: + order = kwargs['order'].split(',') + if len(order) != 0: + new_values = [] + try: + for v in order: + val = v.strip() + if val not in valid: + error = "Invalid plugin name: %s" % val + raise ValueError(error) + new_values.append(val) + if len(new_values) < len(valid): + for val in valid: + if val not in new_values: + new_values.append(val) + + po = PluginObject() + po.name = "global" + globalconf = dict() + globalconf['order'] = ','.join(new_values) + po.set_config(globalconf) + po.save_plugin_config(FACILITY) + + # When all is saved update also live config + self._site[FACILITY]['enabled'] = new_values + + message = "New configuration saved." + message_type = "success" + + except ValueError, e: + message = str(e) + message_type = "error" + + except Exception, e: # pylint: disable=broad-except + message = "Failed to save data!" + message_type = "error" + + return self._template('admin/login_order.html', + message=message, + message_type=message_type, + title='login plugins order', + name='admin_login_order_form', + action=self.url, + options=self._site[FACILITY]['enabled']) + + def root(self, *args, **kwargs): + cherrypy.log.error("method: %s" % cherrypy.request.method) + op = getattr(self, cherrypy.request.method, self.GET) + if callable(op): + return op(*args, **kwargs) + + +class LoginPlugins(Page): + def __init__(self, site, parent): + super(LoginPlugins, self).__init__(site) + parent.login = self + self.url = '%s/login' % parent.url + + for plugin in self._site[FACILITY]['available']: + cherrypy.log.error('Admin login plugin: %s' % plugin) + obj = self._site[FACILITY]['available'][plugin] + self.__dict__[plugin] = AdminPluginPage(obj, self._site, + self.url, FACILITY) + + self.order = LoginPluginsOrder(self._site, self.url) + + def root(self, *args, **kwargs): + login_plugins = self._site[FACILITY] + return self._template('admin/login.html', title='Login Plugins', + available=login_plugins['available'], + enabled=login_plugins['enabled']) diff --git a/ipsilon/root.py b/ipsilon/root.py index 413f453..c308c95 100755 --- a/ipsilon/root.py +++ b/ipsilon/root.py @@ -23,6 +23,7 @@ from ipsilon.login.common import Login from ipsilon.login.common import Logout from ipsilon.admin.common import Admin from ipsilon.providers.common import LoadProviders +from ipsilon.admin.login import LoginPlugins import cherrypy sites = dict() @@ -50,7 +51,8 @@ class Root(Page): LoadProviders(self, self._site) # after all plugins are setup we can instantiate the admin pages - self.admin = Admin(self._site) + self.admin = Admin(self._site, 'admin') + LoginPlugins(self._site, self.admin) def root(self): return self._template('index.html', title='Ipsilon') diff --git a/templates/admin/index.html b/templates/admin/index.html index 1957a7f..a0d17fe 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -1,26 +1,6 @@ {% extends "master-admin.html" %} {% block main %} {% if user.is_admin %} -

Login plugins

- - {% for p in available %} -
-
{{ p }}
-
- {% if p in enabled %} - Disable - Configure - {% else %} - Enable - {% endif %} -
-
- {% endfor %} - -

Plugins order

-
{{ ', '.join(enabled) }}
-
- configure -
+

Select an item to configure

{% endif %} {% endblock %} diff --git a/templates/admin/login.html b/templates/admin/login.html new file mode 100644 index 0000000..1957a7f --- /dev/null +++ b/templates/admin/login.html @@ -0,0 +1,26 @@ +{% extends "master-admin.html" %} +{% block main %} +{% if user.is_admin %} +

Login plugins

+ + {% for p in available %} +
+
{{ p }}
+
+ {% if p in enabled %} + Disable + Configure + {% else %} + Enable + {% endif %} +
+
+ {% endfor %} + +

Plugins order

+
{{ ', '.join(enabled) }}
+
+ configure +
+{% endif %} +{% endblock %} diff --git a/templates/admin/login_order.html b/templates/admin/login_order.html index c78fe55..b14c4d9 100644 --- a/templates/admin/login_order.html +++ b/templates/admin/login_order.html @@ -18,7 +18,7 @@ - Back + Back {% endblock %} diff --git a/templates/admin/login_plugin.html b/templates/admin/plugin_config.html similarity index 88% rename from templates/admin/login_plugin.html rename to templates/admin/plugin_config.html index b45b3a4..7c143af 100644 --- a/templates/admin/login_plugin.html +++ b/templates/admin/plugin_config.html @@ -20,7 +20,7 @@ - Back + Back -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/master-admin.html b/templates/master-admin.html index 796c917..43770c1 100644 --- a/templates/master-admin.html +++ b/templates/master-admin.html @@ -42,7 +42,7 @@ -- 2.20.1 From 1e66ada5e35cd69841eb314062266745b0755174 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Wed, 26 Mar 2014 17:31:19 -0400 Subject: [PATCH 07/16] Refactor login plugin enablement code This allows us to finally implement the plugin enable/disable configuration buttons and enable/disable plugins on the fly. Signed-off-by: Simo Sorce --- ipsilon/admin/login.py | 38 ++++++++++++++++++-- ipsilon/login/common.py | 72 +++++++++++++++++++++++++++++++------- templates/admin/login.html | 9 +++-- 3 files changed, 103 insertions(+), 16 deletions(-) diff --git a/ipsilon/admin/login.py b/ipsilon/admin/login.py index b8325c8..d11c1f1 100755 --- a/ipsilon/admin/login.py +++ b/ipsilon/admin/login.py @@ -114,9 +114,43 @@ class LoginPlugins(Page): self.order = LoginPluginsOrder(self._site, self) - def root(self, *args, **kwargs): + def root_with_msg(self, message=None, message_type=None): login_plugins = self._site[FACILITY] + ordered = [] + for p in login_plugins['enabled']: + ordered.append(p.name) return self._template('admin/login.html', title=self.title, + message=message, + message_type=message_type, available=login_plugins['available'], - enabled=login_plugins['enabled'], + enabled=ordered, menu=self._master.menu) + + def root(self, *args, **kwargs): + return self.root_with_msg() + + def enable(self, plugin): + msg = None + plugins = self._site[FACILITY] + if plugin not in plugins['available']: + msg = "Unknown plugin %s" % plugin + return self.root_with_msg(msg, "error") + obj = plugins['available'][plugin] + if obj not in plugins['enabled']: + obj.enable(self._site) + msg = "Plugin %s enabled" % obj.name + return self.root_with_msg(msg, "success") + enable.exposed = True + + def disable(self, plugin): + msg = None + plugins = self._site[FACILITY] + if plugin not in plugins['available']: + msg = "Unknown plugin %s" % plugin + return self.root_with_msg(msg, "error") + obj = plugins['available'][plugin] + if obj in plugins['enabled']: + obj.disable(self._site) + msg = "Plugin %s disabled" % obj.name + return self.root_with_msg(msg, "success") + disable.exposed = True diff --git a/ipsilon/login/common.py b/ipsilon/login/common.py index d290521..f0efebd 100755 --- a/ipsilon/login/common.py +++ b/ipsilon/login/common.py @@ -68,6 +68,61 @@ class LoginManagerBase(PluginObject): raise cherrypy.HTTPRedirect(ref) + def _debug(self, fact): + if cherrypy.config.get('debug', False): + cherrypy.log(fact) + + def get_tree(self, site): + raise NotImplementedError + + def enable(self, site): + plugins = site[FACILITY] + if self in plugins['enabled']: + return + + # configure self + if self.name in plugins['config']: + self.set_config(plugins['config'][self.name]) + + # and add self to the root + root = plugins['root'] + root.add_subtree(self.name, self.get_tree(site)) + + # finally add self in login chain + prev_obj = None + for prev_obj in plugins['enabled']: + if prev_obj.next_login: + break + if prev_obj: + while prev_obj.next_login: + prev_obj = prev_obj.next_login + prev_obj.next_login = self + if not root.first_login: + root.first_login = self + + plugins['enabled'].append(self) + self._debug('Login plugin enabled: %s' % self.name) + + def disable(self, site): + plugins = site[FACILITY] + if self not in plugins['enabled']: + return + + #remove self from chain + root = plugins['root'] + if root.first_login == self: + root.first_login = self.next_login + elif root.first_login: + prev_obj = root.first_login + while prev_obj.next_login != self: + prev_obj = prev_obj.next_login + if prev_obj: + prev_obj.next_login = self.next_login + self.next_login = None + + plugins['enabled'].remove(self) + self._debug('Login plugin disabled: %s' % self.name) + class LoginPageBase(Page): @@ -95,22 +150,15 @@ class Login(Page): available = plugins['available'].keys() self._debug('Available login managers: %s' % str(available)) - prev_obj = None + plugins['root'] = self for item in plugins['whitelist']: self._debug('Login plugin in whitelist: %s' % item) if item not in plugins['available']: continue - self._debug('Login plugin enabled: %s' % item) - plugins['enabled'].append(item) - obj = plugins['available'][item] - if prev_obj: - prev_obj.next_login = obj - else: - self.first_login = obj - prev_obj = obj - if item in plugins['config']: - obj.set_config(plugins['config'][item]) - self.__dict__[item] = obj.get_tree(self._site) + plugins['available'][item].enable(self._site) + + def add_subtree(self, name, page): + self.__dict__[name] = page def root(self, *args, **kwargs): if self.first_login: diff --git a/templates/admin/login.html b/templates/admin/login.html index 1957a7f..9f51d02 100644 --- a/templates/admin/login.html +++ b/templates/admin/login.html @@ -2,16 +2,21 @@ {% block main %} {% if user.is_admin %}

Login plugins

+ {% if message %} +
+

{{ message }}

+
+ {% endif %} {% for p in available %}
{{ p }}
{% if p in enabled %} - Disable + Disable Configure {% else %} - Enable + Enable {% endif %}
-- 2.20.1 From d06318be2430b6863a5695f76811bf43a617bade Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Thu, 27 Mar 2014 11:56:34 -0400 Subject: [PATCH 08/16] Refactor provider plugins enablement This allow to enable/disable Identity Providers directly from the configuration interface. Signed-off-by: Simo Sorce --- ipsilon/providers/common.py | 43 +++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/ipsilon/providers/common.py b/ipsilon/providers/common.py index 3b2072c..8e62cbe 100755 --- a/ipsilon/providers/common.py +++ b/ipsilon/providers/common.py @@ -44,6 +44,41 @@ class ProviderBase(PluginObject): self.name = name self.path = path + def _debug(self, fact): + if cherrypy.config.get('debug', False): + cherrypy.log(fact) + + def get_tree(self, site): + raise NotImplementedError + + def enable(self, site): + plugins = site[FACILITY] + if self in plugins['enabled']: + return + + # configure self + if self.name in plugins['config']: + self.set_config(plugins['config'][self.name]) + + # and add self to the root + root = plugins['root'] + root.add_subtree(self.name, self.get_tree(site)) + + plugins['enabled'].append(self) + self._debug('IdP Provider enabled: %s' % self.name) + + def disable(self, site): + plugins = site[FACILITY] + if self not in plugins['enabled']: + return + + # remove self to the root + root = plugins['root'] + root.del_subtree(self.name) + + plugins['enabled'].remove(self) + self._debug('IdP Provider disabled: %s' % self.name) + class ProviderPageBase(Page): @@ -86,16 +121,12 @@ class LoadProviders(object): available = providers['available'].keys() self._debug('Available providers: %s' % str(available)) + providers['root'] = root for item in providers['whitelist']: self._debug('IdP Provider in whitelist: %s' % item) if item not in providers['available']: continue - self._debug('IdP Provider enabled: %s' % item) - providers['enabled'].append(item) - provider = providers['available'][item] - if item in providers['config']: - provider.set_config(providers['config'][item]) - root.__dict__[item] = provider.get_tree(site) + providers['available'][item].enable(site) def _debug(self, fact): if cherrypy.config.get('debug', False): -- 2.20.1 From 0338db2eb3197ce0024d0192bd981120a58de573 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Wed, 26 Mar 2014 15:20:16 -0400 Subject: [PATCH 09/16] Basic Identity providers plugin configuration Signed-off-by: Simo Sorce --- ipsilon/admin/providers.py | 78 ++++++++++++++++++++++++++++++++++ ipsilon/root.py | 2 + templates/admin/providers.html | 20 +++++++++ 3 files changed, 100 insertions(+) create mode 100755 ipsilon/admin/providers.py create mode 100644 templates/admin/providers.html diff --git a/ipsilon/admin/providers.py b/ipsilon/admin/providers.py new file mode 100755 index 0000000..26e96a7 --- /dev/null +++ b/ipsilon/admin/providers.py @@ -0,0 +1,78 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 Simo Sorce +# +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import cherrypy +from ipsilon.util.page import Page +from ipsilon.providers.common import FACILITY +from ipsilon.admin.common import AdminPluginPage + + +class ProviderPlugins(Page): + def __init__(self, site, parent): + super(ProviderPlugins, self).__init__(site) + self._master = parent + self.title = 'Identity Providers' + self.url = '%s/providers' % parent.url + self.facility = FACILITY + parent.add_subtree('providers', self) + + for plugin in self._site[FACILITY]['available']: + cherrypy.log.error('Admin provider plugin: %s' % plugin) + obj = self._site[FACILITY]['available'][plugin] + self.__dict__[plugin] = AdminPluginPage(obj, self) + + def root_with_msg(self, message=None, message_type=None): + plugins = self._site[FACILITY] + return self._template('admin/providers.html', title=self.title, + baseurl=self.url, + message=message, + message_type=message_type, + available=plugins['available'], + enabled=plugins['enabled'], + menu=self._master.menu) + + def root(self, *args, **kwargs): + return self.root_with_msg() + + def enable(self, plugin): + msg = None + plugins = self._site[FACILITY] + if plugin not in plugins['available']: + msg = "Unknown plugin %s" % plugin + return self.root_with_msg(msg, "error") + obj = plugins['available'][plugin] + if obj not in plugins['enabled']: + obj.enable(self._site) + msg = "Plugin %s enabled" % obj.name + return self.root_with_msg(msg, "success") + enable.exposed = True + + def disable(self, plugin): + msg = None + plugins = self._site[FACILITY] + if plugin not in plugins['available']: + msg = "Unknown plugin %s" % plugin + return self.root_with_msg(msg, "error") + obj = plugins['available'][plugin] + if obj in plugins['enabled']: + obj.disable(self._site) + msg = "Plugin %s disabled" % obj.name + return self.root_with_msg(msg, "success") + disable.exposed = True diff --git a/ipsilon/root.py b/ipsilon/root.py index c308c95..9451d22 100755 --- a/ipsilon/root.py +++ b/ipsilon/root.py @@ -24,6 +24,7 @@ from ipsilon.login.common import Logout from ipsilon.admin.common import Admin from ipsilon.providers.common import LoadProviders from ipsilon.admin.login import LoginPlugins +from ipsilon.admin.providers import ProviderPlugins import cherrypy sites = dict() @@ -53,6 +54,7 @@ class Root(Page): # after all plugins are setup we can instantiate the admin pages self.admin = Admin(self._site, 'admin') LoginPlugins(self._site, self.admin) + ProviderPlugins(self._site, self.admin) def root(self): return self._template('index.html', title='Ipsilon') diff --git a/templates/admin/providers.html b/templates/admin/providers.html new file mode 100644 index 0000000..18445b6 --- /dev/null +++ b/templates/admin/providers.html @@ -0,0 +1,20 @@ +{% extends "master-admin.html" %} +{% block main %} +{% if user.is_admin %} +

Provider plugins

+ + {% for p in available %} +
+
{{ p }}
+
+ {% if available[p] in enabled %} + Disable + Configure + {% else %} + Enable + {% endif %} +
+
+ {% endfor %} +{% endif %} +{% endblock %} -- 2.20.1 From 7b56b1311ba0c730fa884c75ccf15dfbf996ebd8 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Thu, 27 Mar 2014 12:56:28 -0400 Subject: [PATCH 10/16] Add generic support for IdP plugin admin pages Signed-off-by: Simo Sorce --- ipsilon/admin/providers.py | 5 ++++- ipsilon/providers/common.py | 1 + templates/admin/providers.html | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ipsilon/admin/providers.py b/ipsilon/admin/providers.py index 26e96a7..1a2df7c 100755 --- a/ipsilon/admin/providers.py +++ b/ipsilon/admin/providers.py @@ -36,7 +36,10 @@ class ProviderPlugins(Page): for plugin in self._site[FACILITY]['available']: cherrypy.log.error('Admin provider plugin: %s' % plugin) obj = self._site[FACILITY]['available'][plugin] - self.__dict__[plugin] = AdminPluginPage(obj, self) + page = AdminPluginPage(obj, self._site, self) + if hasattr(obj, 'admin'): + obj.admin.mount(page) + self.add_subtree(plugin, page) def root_with_msg(self, message=None, message_type=None): plugins = self._site[FACILITY] diff --git a/ipsilon/providers/common.py b/ipsilon/providers/common.py index 8e62cbe..f9c1311 100755 --- a/ipsilon/providers/common.py +++ b/ipsilon/providers/common.py @@ -43,6 +43,7 @@ class ProviderBase(PluginObject): super(ProviderBase, self).__init__() self.name = name self.path = path + self.admin = None def _debug(self, fact): if cherrypy.config.get('debug', False): diff --git a/templates/admin/providers.html b/templates/admin/providers.html index 18445b6..fbeb54d 100644 --- a/templates/admin/providers.html +++ b/templates/admin/providers.html @@ -10,6 +10,9 @@ {% if available[p] in enabled %} Disable Configure + {% if available[p].admin %} + Administer + {% endif %} {% else %} Enable {% endif %} -- 2.20.1 From 15ef3579e537523ea97714bf80c63f2f8f30d4bd Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Thu, 27 Mar 2014 12:57:19 -0400 Subject: [PATCH 11/16] Saml2 initial admin page Signed-off-by: Simo Sorce --- ipsilon/providers/saml2/admin.py | 49 ++++++++++++++++++++++++++++ ipsilon/providers/saml2idp.py | 2 ++ setup.py | 2 ++ templates/admin/providers/saml2.html | 23 +++++++++++++ 4 files changed, 76 insertions(+) create mode 100755 ipsilon/providers/saml2/admin.py create mode 100644 templates/admin/providers/saml2.html diff --git a/ipsilon/providers/saml2/admin.py b/ipsilon/providers/saml2/admin.py new file mode 100755 index 0000000..1e1ddb7 --- /dev/null +++ b/ipsilon/providers/saml2/admin.py @@ -0,0 +1,49 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 Simo Sorce +# +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from ipsilon.util.page import Page +from ipsilon.providers.saml2.provider import ServiceProvider + + +class AdminPage(Page): + def __init__(self, site, config): + super(AdminPage, self).__init__(site) + self.name = 'admin' + self.cfg = config + self.providers = [] + self.menu = [] + self.url = None + + def mount(self, page): + self.menu = page.menu + self.url = '%s/%s' % (page.url, self.name) + for p in self.cfg.idp.get_providers(): + try: + sp = ServiceProvider(self.cfg, p) + self.providers.append(sp) + except Exception, e: # pylint: disable=broad-except + self._debug("Failed to find provider %s: %s" % (p, str(e))) + page.add_subtree(self.name, self) + + def root(self, *args, **kwargs): + return self._template('admin/providers/saml2.html', + title='SAML2 Administration', + providers=self.providers, + baseurl=self.url, + menu=self.menu) diff --git a/ipsilon/providers/saml2idp.py b/ipsilon/providers/saml2idp.py index b8d1851..c1e31dc 100755 --- a/ipsilon/providers/saml2idp.py +++ b/ipsilon/providers/saml2idp.py @@ -20,6 +20,7 @@ from ipsilon.providers.common import ProviderBase, ProviderPageBase from ipsilon.providers.common import FACILITY from ipsilon.providers.saml2.auth import AuthenticateRequest +from ipsilon.providers.saml2.admin import AdminPage from ipsilon.providers.saml2.certs import Certificate from ipsilon.providers.saml2 import metadata from ipsilon.util.user import UserSession @@ -222,6 +223,7 @@ Provides SAML 2.0 authentication infrastructure. """ def get_tree(self, site): self.page = SAML2(site, self) + self.admin = AdminPage(site, self) return self.page diff --git a/setup.py b/setup.py index ecda06a..7dd021d 100755 --- a/setup.py +++ b/setup.py @@ -41,6 +41,8 @@ setup( (DATA+'templates/login', glob('templates/login/*.html')), (DATA+'templates/saml2', glob('templates/saml2/*.html')), (DATA+'templates/install', glob('templates/install/*.conf')), + (DATA+'templates/admin/providers', + glob('templates/admin/providers/*.html')), ] ) diff --git a/templates/admin/providers/saml2.html b/templates/admin/providers/saml2.html new file mode 100644 index 0000000..0d0a05f --- /dev/null +++ b/templates/admin/providers/saml2.html @@ -0,0 +1,23 @@ +{% extends "master-admin.html" %} +{% block main %} +{% if user.is_admin %} +

Service Providers

+ +
+
+ Add New +
+
+
+ {% for p in providers %} +
+ +
+ {{ p.provider_id }} +
+
+ {% endfor %} +{% endif %} +{% endblock %} -- 2.20.1 From 5a6c713fda1a4052051566984c0b489e286aa502 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Thu, 3 Apr 2014 17:10:18 -0400 Subject: [PATCH 12/16] No need to have a separate certificate file Certificates are already contained in the metadata.xml file Signed-off-by: Simo Sorce --- ipsilon/providers/saml2idp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ipsilon/providers/saml2idp.py b/ipsilon/providers/saml2idp.py index c1e31dc..c70b96a 100755 --- a/ipsilon/providers/saml2idp.py +++ b/ipsilon/providers/saml2idp.py @@ -126,8 +126,7 @@ class SAML2(ProviderPageBase): name = str(idval) try: meta = os.path.join(path, 'metadata.xml') - cert = os.path.join(path, 'certificate.pem') - self.cfg.idp.addProvider(lasso.PROVIDER_ROLE_SP, meta, cert) + self.cfg.idp.addProvider(lasso.PROVIDER_ROLE_SP, meta) self._debug('Added SP %s' % name) except Exception, e: # pylint: disable=broad-except self._debug('Failed to add SP %s: %r' % (name, e)) -- 2.20.1 From c67d1a3583a6eda8c626c6d1d9cb42547d7a5b68 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Fri, 4 Apr 2014 10:34:21 -0400 Subject: [PATCH 13/16] Add racefree way to add a new unique data point Our schema gathers together data related to a service by using an ID column. This column cannot be unique or a primary key as the ID is repeated for each key/value pair in the datum group. Use a unique identifier to make sure we can let dqlite generate a new ID internally and then find out wat it is as race-free as possible. We keep this method in the data module so it can be changed later without affecting application logic. Signed-off-by: Simo Sorce --- ipsilon/util/data.py | 30 ++++++++++++++++++++++++++++++ ipsilon/util/plugin.py | 3 +++ 2 files changed, 33 insertions(+) diff --git a/ipsilon/util/data.py b/ipsilon/util/data.py index ec32b43..52dfa78 100755 --- a/ipsilon/util/data.py +++ b/ipsilon/util/data.py @@ -20,6 +20,8 @@ import os import sqlite3 import cherrypy +from random import randint +import sys class Store(object): @@ -366,6 +368,34 @@ class Store(object): if con: con.close() + def new_datum(self, plugin, datum): + ID = "(SELECT IFNULL(MAX(id), 0) + 1 FROM %s_data)" % plugin + INSERT_NEW = "INSERT INTO %s_data VALUES(%s,?,?)" % (plugin, ID) + INSERT = "INSERT INTO %s_data VALUES(?,?,?)" % plugin + SELECT = "SELECT id FROM %s_data WHERE name=? AND value=?" % plugin + DELETE = "DELETE FROM %s_data WHERE name=? AND value=?" % plugin + con = None + try: + con = sqlite3.connect(self._admin_dbname) + cur = con.cursor() + tmpid = ('new', str(randint(0, sys.maxint))) + cur.execute(INSERT_NEW, tmpid) + cur.execute(SELECT, tmpid) + rows = cur.fetchall() + idval = rows[0][0] + for name in datum: + cur.execute(INSERT, (idval, name, datum[name])) + cur.execute(DELETE, tmpid) + con.commit() + except sqlite3.Error, e: + if con: + con.rollback() + cherrypy.log.error("Failed to store %s data: [%s]" % (plugin, e)) + raise + finally: + if con: + con.close() + def wipe_data(self, plugin): # Try to backup old data first, just in case try: diff --git a/ipsilon/util/plugin.py b/ipsilon/util/plugin.py index 6c329d6..cdf997e 100755 --- a/ipsilon/util/plugin.py +++ b/ipsilon/util/plugin.py @@ -157,6 +157,9 @@ class PluginObject(object): def save_data(self, data): self._data.save_data(self.name, data) + def new_datum(self, datum): + self._data.new_datum(self.name, datum) + def wipe_config_values(self, facility): self._data.wipe_plugin_config(facility, self.name) -- 2.20.1 From ed5ed179806c921036cf811e1890408aac072bef Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Thu, 3 Apr 2014 15:42:35 -0400 Subject: [PATCH 14/16] Add Service and Identity Provider abstraction This commit adds: - helper functions to create new providers - separate IdentityProvider class to represent the IDP. Database changes: The saml2 plugin database now contain the metadata file contents and does not rely anymore on on-disk data. Signed-off-by: Simo Sorce --- ipsilon/providers/saml2/auth.py | 2 +- ipsilon/providers/saml2/provider.py | 59 +++++++++++++++++++++++++++++ ipsilon/providers/saml2idp.py | 29 ++++++-------- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/ipsilon/providers/saml2/auth.py b/ipsilon/providers/saml2/auth.py index ff81af6..bac73a5 100755 --- a/ipsilon/providers/saml2/auth.py +++ b/ipsilon/providers/saml2/auth.py @@ -59,7 +59,7 @@ class AuthenticateRequest(ProviderPageBase): def _parse_request(self, message): - login = lasso.Login(self.cfg.idp) + login = self.cfg.idp.get_login_handler() try: login.processAuthnRequestMsg(message) diff --git a/ipsilon/providers/saml2/provider.py b/ipsilon/providers/saml2/provider.py index d3ed5da..6339450 100755 --- a/ipsilon/providers/saml2/provider.py +++ b/ipsilon/providers/saml2/provider.py @@ -109,3 +109,62 @@ class ServiceProvider(object): if 'strip domain' in self._properties: return username.split('@', 1)[0] return username + + +class ServiceProviderCreator(object): + + def __init__(self, config): + self.cfg = config + + def create_from_buffer(self, name, metabuf): + '''Test and add data''' + + test = lasso.Server() + test.addProviderFromBuffer(lasso.PROVIDER_ROLE_SP, metabuf) + newsps = test.get_providers() + if len(newsps) != 1: + raise InvalidProviderId("Metadata must contain one Provider") + + spid = newsps.keys()[0] + data = self.cfg.get_data(name='id', value=spid) + if len(data) != 0: + raise InvalidProviderId("Provider Already Exists") + datum = {'id': spid, 'name': name, 'type': 'SP', 'metadata': metabuf} + self.cfg.new_datum(datum) + + data = self.cfg.get_data(name='id', value=spid) + if len(data) != 1: + raise InvalidProviderId("Internal Error") + idval = data.keys()[0] + data = self.cfg.get_data(idval=idval) + sp = data[idval] + self.cfg.idp.add_provider(sp) + + return ServiceProvider(self.cfg, spid) + + +class IdentityProvider(object): + def __init__(self, config): + self.server = lasso.Server(config.idp_metadata_file, + config.idp_key_file, + None, + config.idp_certificate_file) + self.server.role = lasso.PROVIDER_ROLE_IDP + + def add_provider(self, sp): + self.server.addProviderFromBuffer(lasso.PROVIDER_ROLE_SP, + sp['metadata']) + self._debug('Added SP %s' % sp['name']) + + def get_login_handler(self, dump=None): + if dump: + return lasso.Login.newFromDump(self.server, dump) + else: + return lasso.Login(self.server) + + def get_providers(self): + return self.server.get_providers() + + def _debug(self, fact): + if cherrypy.config.get('debug', False): + cherrypy.log(fact) diff --git a/ipsilon/providers/saml2idp.py b/ipsilon/providers/saml2idp.py index c70b96a..d8cbe6c 100755 --- a/ipsilon/providers/saml2idp.py +++ b/ipsilon/providers/saml2idp.py @@ -22,6 +22,7 @@ from ipsilon.providers.common import FACILITY from ipsilon.providers.saml2.auth import AuthenticateRequest from ipsilon.providers.saml2.admin import AdminPage from ipsilon.providers.saml2.certs import Certificate +from ipsilon.providers.saml2.provider import IdentityProvider from ipsilon.providers.saml2 import metadata from ipsilon.util.user import UserSession from ipsilon.util.plugin import PluginObject @@ -76,7 +77,7 @@ class Continue(AuthenticateRequest): raise cherrypy.HTTPError(400) try: - login = lasso.Login.newFromDump(self.cfg.idp, dump) + login = self.cfg.idp.get_login_handler(dump) except Exception, e: # pylint: disable=broad-except self._debug('Failed to load status from dump: %r' % e) @@ -104,32 +105,23 @@ class SAML2(ProviderPageBase): # Init IDP data try: - self.cfg.idp = lasso.Server(self.cfg.idp_metadata_file, - self.cfg.idp_key_file, - None, - self.cfg.idp_certificate_file) - self.cfg.idp.role = lasso.PROVIDER_ROLE_IDP + self.cfg.idp = IdentityProvider(self.cfg) except Exception, e: # pylint: disable=broad-except - self._debug('Failed to enable SAML2 provider: %r' % e) + self._debug('Failed to init SAML2 provider: %r' % e) return # Import all known applications data = self.cfg.get_data() for idval in data: - if 'type' not in data[idval] or data[idval]['type'] != 'SP': - continue - path = os.path.join(self.cfg.idp_storage_path, str(idval)) sp = data[idval] - if 'name' in sp: - name = sp['name'] - else: - name = str(idval) + if 'type' not in sp or sp['type'] != 'SP': + continue + if 'name' not in sp or 'metadata' not in sp: + continue try: - meta = os.path.join(path, 'metadata.xml') - self.cfg.idp.addProvider(lasso.PROVIDER_ROLE_SP, meta) - self._debug('Added SP %s' % name) + self.cfg.idp.add_provider(sp) except Exception, e: # pylint: disable=broad-except - self._debug('Failed to add SP %s: %r' % (name, e)) + self._debug('Failed to add SP %s: %r' % (sp['name'], e)) self.SSO = SSO(*args, **kwargs) @@ -139,6 +131,7 @@ class IdpProvider(ProviderBase): def __init__(self): super(IdpProvider, self).__init__('saml2', 'saml2') self.page = None + self.idp = None self.description = """ Provides SAML 2.0 authentication infrastructure. """ -- 2.20.1 From 671c9261307a23daaeafdaf3263accc836ba7b70 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Thu, 3 Apr 2014 15:42:35 -0400 Subject: [PATCH 15/16] Providers can save properties back to the database This way a provider class can be used in admin pages as well and remain consistent. Signed-off-by: Simo Sorce --- ipsilon/providers/saml2/provider.py | 47 ++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/ipsilon/providers/saml2/provider.py b/ipsilon/providers/saml2/provider.py index 6339450..7975500 100755 --- a/ipsilon/providers/saml2/provider.py +++ b/ipsilon/providers/saml2/provider.py @@ -64,6 +64,7 @@ class ServiceProvider(object): idval = data.keys()[0] data = self.cfg.get_data(idval=idval) self._properties = data[idval] + self._staging = dict() @property def provider_id(self): @@ -73,13 +74,35 @@ class ServiceProvider(object): def name(self): return self._properties['name'] + @name.setter + def name(self, value): + self._staging['name'] = value + + @property + def owner(self): + if 'owner' in self._properties: + return self._properties['owner'] + else: + return '' + + @owner.setter + def owner(self, value): + self._staging['owner'] = value + @property - def allowed_namedids(self): - if 'allowed nameid' in self._properties: - return self._properties['allowed nameid'] + def allowed_nameids(self): + if 'allowed nameids' in self._properties: + allowed = self._properties['allowed nameids'] + return [x.strip() for x in allowed.split(',')] else: return self.cfg.default_allowed_nameids + @allowed_nameids.setter + def allowed_nameids(self, value): + if type(value) is not list: + raise ValueError("Must be a list") + self._staging['allowed nameids'] = ','.join(value) + @property def default_nameid(self): if 'default nameid' in self._properties: @@ -87,6 +110,22 @@ class ServiceProvider(object): else: return self.cfg.default_nameid + @default_nameid.setter + def default_nameid(self, value): + self._staging['default nameid'] = value + + def save_properties(self): + data = self.cfg.get_data(name='id', value=self.provider_id) + if len(data) != 1: + raise InvalidProviderId('Could not find SP data') + idval = data.keys()[0] + data = dict() + data[idval] = self._staging + self.cfg.save_data(data) + data = self.cfg.get_data(idval=idval) + self._properties = data[idval] + self._staging = dict() + def get_valid_nameid(self, nip): self._debug('Requested NameId [%s]' % (nip.format,)) if nip.format is None: @@ -94,7 +133,7 @@ class ServiceProvider(object): elif nip.format == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED: return NAMEID_MAP[self.default_nameid] else: - allowed = self.allowed_namedids + allowed = self.allowed_nameids self._debug('Allowed NameIds %s' % (repr(allowed))) for nameid in allowed: if nip.format == NAMEID_MAP[nameid]: -- 2.20.1 From 8cdf10beebc47e1dfa095d052a2f7ed317e905a0 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Fri, 4 Apr 2014 13:07:19 -0400 Subject: [PATCH 16/16] Admin classes to change SP properties Signed-off-by: Simo Sorce --- ipsilon/providers/saml2/admin.py | 104 +++++++++++++++++++++++- templates/admin/providers/saml2.html | 23 ++---- templates/admin/providers/saml2_sp.html | 61 ++++++++++++++ 3 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 templates/admin/providers/saml2_sp.html diff --git a/ipsilon/providers/saml2/admin.py b/ipsilon/providers/saml2/admin.py index 1e1ddb7..c8d26b8 100755 --- a/ipsilon/providers/saml2/admin.py +++ b/ipsilon/providers/saml2/admin.py @@ -17,10 +17,105 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import cherrypy from ipsilon.util.page import Page from ipsilon.providers.saml2.provider import ServiceProvider +class SPAdminPage(Page): + + def __init__(self, sp, site, parent): + super(SPAdminPage, self).__init__(site) + self.sp = sp + self.title = sp.name + self.backurl = parent.url + self.url = '%s/sp/%s' % (parent.url, sp.name) + + def form_standard(self, message=None, message_type=None): + return self._template('admin/providers/saml2_sp.html', + message=message, + message_type=message_type, + title=self.title, + name='saml2_sp_%s_form' % self.sp.name, + backurl=self.backurl, action=self.url, + data=self.sp) + + def GET(self, *args, **kwargs): + return self.form_standard() + + def POST(self, *args, **kwargs): + + message = "Nothing was modified." + message_type = "info" + save = False + + for key, value in kwargs.iteritems(): + if key == 'name': + if value != self.sp.name: + if self.user.is_admin or self.user.name == self.sp.owner: + self._debug("Replacing %s: %s -> %s" % + (key, self.sp.name, value)) + self.sp.name = value + save = True + else: + message = "Unauthorized to rename object" + message_type = "error" + return self.form_standard(message, message_type) + + elif key == 'owner': + if value != self.sp.owner: + if self.user.is_admin: + self._debug("Replacing %s: %s -> %s" % + (key, self.sp.owner, value)) + self.sp.owner = value + save = True + else: + message = "Unauthorized to set owner value" + message_type = "error" + return self.form_standard(message, message_type) + + elif key == 'default_nameid': + if value != self.sp.default_nameid: + if self.user.is_admin: + self._debug("Replacing %s: %s -> %s" % + (key, self.sp.default_nameid, value)) + self.sp.default_nameid = value + save = True + else: + message = "Unauthorized to set default nameid value" + message_type = "error" + return self.form_standard(message, message_type) + + elif key == 'allowed_nameids': + v = set([x.strip() for x in value.split(',')]) + if v != set(self.sp.allowed_nameids): + if self.user.is_admin: + self._debug("Replacing %s: %s -> %s" % + (key, self.sp.allowed_nameids, list(v))) + self.sp.allowed_nameids = list(v) + save = True + else: + message = "Unauthorized to set allowed nameids value" + message_type = "error" + return self.form_standard(message, message_type) + + if save: + try: + self.sp.save_properties() + message = "Properties succssfully changed" + message_type = "success" + except Exception: # pylint: disable=broad-except + message = "Failed to save data!" + message_type = "error" + + return self.form_standard(message, message_type) + + def root(self, *args, **kwargs): + op = getattr(self, cherrypy.request.method, self.GET) + if callable(op): + return op(*args, **kwargs) + + class AdminPage(Page): def __init__(self, site, config): super(AdminPage, self).__init__(site) @@ -29,6 +124,13 @@ class AdminPage(Page): self.providers = [] self.menu = [] self.url = None + self.sp = Page(self._site) + + def add_sp(self, name, sp): + page = SPAdminPage(sp, self._site, self) + self.sp.add_subtree(name, page) + self.providers.append(sp) + return page def mount(self, page): self.menu = page.menu @@ -36,7 +138,7 @@ class AdminPage(Page): for p in self.cfg.idp.get_providers(): try: sp = ServiceProvider(self.cfg, p) - self.providers.append(sp) + self.add_sp(sp.name, sp) except Exception, e: # pylint: disable=broad-except self._debug("Failed to find provider %s: %s" % (p, str(e))) page.add_subtree(self.name, self) diff --git a/templates/admin/providers/saml2.html b/templates/admin/providers/saml2.html index 0d0a05f..5185a6f 100644 --- a/templates/admin/providers/saml2.html +++ b/templates/admin/providers/saml2.html @@ -1,23 +1,16 @@ {% extends "master-admin.html" %} {% block main %} -{% if user.is_admin %} -

Service Providers

+

Service Providers

+
+{% for p in providers %} -
- {% for p in providers %} -
- -
- {{ p.provider_id }} -
+
+ {{ p.provider_id }}
- {% endfor %} -{% endif %} +
+{% endfor %} {% endblock %} diff --git a/templates/admin/providers/saml2_sp.html b/templates/admin/providers/saml2_sp.html new file mode 100644 index 0000000..50d38ed --- /dev/null +++ b/templates/admin/providers/saml2_sp.html @@ -0,0 +1,61 @@ +{% extends "master-admin.html" %} +{% block main %} +

{{ title }}

+ {% if message %} +
+

{{ message }}

+
+ {% endif %} +
+
+ +
+ + {{ data.provider_id }} +
+ +
+ + {% if user.name == data.owner or user.is_admin %} + + {% else %} + {{ data.name }} + {% endif %} +
+ +
+ + {% if user.is_admin -%} + + {%- endif %} +
+ +
+ + {% if user.is_admin -%} + + {%- endif %} +
+ + {% if user.is_admin %} +
+ + +
+ {% endif %} + + + Back +
+
+{% endblock %} -- 2.20.1