From f174f6169e368c58ab35e5e415de0883bc0ff3a5 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Nov 16 2017 13:18:35 +0000 Subject: [PATCH 1/3] Kill the Persona IdP plugin Signed-off-by: Patrick Uiterwijk --- diff --git a/contrib/fedora/ipsilon.spec b/contrib/fedora/ipsilon.spec index d4ce8c1..5e99f74 100644 --- a/contrib/fedora/ipsilon.spec +++ b/contrib/fedora/ipsilon.spec @@ -153,19 +153,6 @@ Provides an OpenID Connect and OAuth2 provider plugin for the Ipsilon identity Provider -%package persona -Summary: Persona provider plugin -Group: System Environment/Base -License: GPLv3+ -Provides: ipsilon-provider = %{version}-%{release} -Requires: %{name} = %{version}-%{release} -Requires: m2crypto -BuildArch: noarch - -%description persona -Provides a Persona provider plugin for the Ipsilon identity Provider - - %package authfas Summary: Fedora Authentication System login plugin Group: System Environment/Base @@ -394,10 +381,6 @@ fi %{python2_sitelib}/ipsilon/providers/openidc/ %{_datadir}/ipsilon/templates/openidc/ -%files persona -%{python2_sitelib}/ipsilon/providers/persona* -%{_datadir}/ipsilon/templates/persona - %files authfas %{python2_sitelib}/ipsilon/login/authfas* diff --git a/ipsilon/providers/persona/__init__.py b/ipsilon/providers/persona/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/ipsilon/providers/persona/__init__.py +++ /dev/null diff --git a/ipsilon/providers/persona/auth.py b/ipsilon/providers/persona/auth.py deleted file mode 100644 index daa64f6..0000000 --- a/ipsilon/providers/persona/auth.py +++ /dev/null @@ -1,146 +0,0 @@ -# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING - -from ipsilon.providers.common import ProviderPageBase -from ipsilon.util.user import UserSession -from ipsilon.util.endpoint import allow_iframe - -import base64 -import cherrypy -import time -import json -import M2Crypto - - -class AuthenticateRequest(ProviderPageBase): - - def __init__(self, site, provider, *args, **kwargs): - super(AuthenticateRequest, self).__init__(site, provider) - self.trans = None - - def _preop(self, *args, **kwargs): - self.trans = self.get_valid_transaction('persona', **kwargs) - - def pre_GET(self, *args, **kwargs): - self._preop(*args, **kwargs) - - def pre_POST(self, *args, **kwargs): - self._preop(*args, **kwargs) - - -class Sign(AuthenticateRequest): - - def _base64_url_decode(self, inp): - inp += '=' * (4 - (len(inp) % 4)) - return base64.urlsafe_b64decode(inp) - - def _base64_url_encode(self, inp): - return base64.urlsafe_b64encode(inp).replace('=', '') - - def _persona_sign(self, email, publicKey, certDuration): - self.debug('Signing for %s with duration of %s' % (email, - certDuration)) - header = {'alg': 'RS256'} - header = json.dumps(header) - header = self._base64_url_encode(header) - - claim = {} - # Valid from 10 seconds before now to account for clock skew - claim['iat'] = 1000 * int(time.time() - 10) - # Validity of at most 24 hours - claim['exp'] = 1000 * int(time.time() + - min(certDuration, 24 * 60 * 60)) - - claim['iss'] = self.cfg.issuer_domain - claim['public-key'] = json.loads(publicKey) - claim['principal'] = {'email': email} - - claim = json.dumps(claim) - claim = self._base64_url_encode(claim) - - certificate = '%s.%s' % (header, claim) - digest = M2Crypto.EVP.MessageDigest('sha256') - digest.update(certificate) - signature = self.cfg.key.sign(digest.digest(), 'sha256') - signature = self._base64_url_encode(signature) - signed_certificate = '%s.%s' % (certificate, signature) - - return signed_certificate - - def _willing_to_sign(self, email, username): - for domain in self.cfg.allowed_domains: - if email == ('%s@%s' % (username, domain)): - return True - return False - - @allow_iframe - def POST(self, *args, **kwargs): - if 'email' not in kwargs or 'publicKey' not in kwargs \ - or 'certDuration' not in kwargs or '@' not in kwargs['email']: - cherrypy.response.status = 400 - raise Exception('Invalid request: %s' % kwargs) - - us = UserSession() - user = us.get_user() - - if user.is_anonymous: - raise cherrypy.HTTPError(401, 'Not signed in') - - if not self._willing_to_sign(kwargs['email'], user.name): - self.log('Not willing to sign for %s, logged in as %s' % ( - kwargs['email'], user.name)) - raise cherrypy.HTTPError(403, 'Incorrect user') - - return self._persona_sign(kwargs['email'], kwargs['publicKey'], - kwargs['certDuration']) - - -class SignInResult(AuthenticateRequest): - @allow_iframe - def GET(self, *args, **kwargs): - user = UserSession().get_user() - - return self._template('persona/signin_result.html', - loggedin=not user.is_anonymous) - - -class SignIn(AuthenticateRequest): - def __init__(self, *args, **kwargs): - super(SignIn, self).__init__(*args, **kwargs) - self.result = SignInResult(*args, **kwargs) - self.trans = None - - @allow_iframe - def GET(self, *args, **kwargs): - username = None - domain = None - if 'email' in kwargs: - if '@' in kwargs['email']: - username, domain = kwargs['email'].split('@', 2) - self.debug('Persona SignIn requested for: %s@%s' % (username, - domain)) - - returl = '%s/persona/SignIn/result?%s' % ( - self.basepath, self.trans.get_GET_arg()) - data = {'login_return': returl, - 'login_target': 'Persona', - 'login_username': username} - self.trans.store(data) - redirect = '%s/login?%s' % (self.basepath, - self.trans.get_GET_arg()) - self.debug('Redirecting: %s' % redirect) - raise cherrypy.HTTPRedirect(redirect) - - -class Persona(AuthenticateRequest): - - def __init__(self, *args, **kwargs): - super(Persona, self).__init__(*args, **kwargs) - self.Sign = Sign(*args, **kwargs) - self.SignIn = SignIn(*args, **kwargs) - self.trans = None - - @allow_iframe - def GET(self, *args, **kwargs): - user = UserSession().get_user() - return self._template('persona/provisioning.html', - loggedin=not user.is_anonymous) diff --git a/ipsilon/providers/personaidp.py b/ipsilon/providers/personaidp.py deleted file mode 100644 index 265847a..0000000 --- a/ipsilon/providers/personaidp.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING - -from __future__ import absolute_import - -from ipsilon.providers.common import ProviderBase, ProviderInstaller -from ipsilon.util.plugin import PluginObject -from ipsilon.util import config as pconfig -from ipsilon.info.common import InfoMapping -from ipsilon.providers.persona.auth import Persona -from ipsilon.tools import files - -import json -import M2Crypto -import os - - -class IdpProvider(ProviderBase): - - def __init__(self, *pargs): - super(IdpProvider, self).__init__('persona', 'Persona', 'persona', - *pargs) - self.mapping = InfoMapping() - self.page = None - self.basepath = None - self.key = None - self.key_info = None - self.description = """ -Provides Persona authentication infrastructure. """ - - self.new_config( - self.name, - pconfig.String( - 'issuer domain', - 'The issuer domain of the Persona provider', - 'localhost'), - pconfig.String( - 'idp key file', - 'The key where the Persona key is stored.', - 'persona.key'), - pconfig.List( - 'allowed domains', - 'List of domains this IdP is willing to issue claims for.'), - ) - - @property - def issuer_domain(self): - return self.get_config_value('issuer domain') - - @property - def idp_key_file(self): - return self.get_config_value('idp key file') - - @property - def allowed_domains(self): - return self.get_config_value('allowed domains') - - def get_tree(self, site): - self.page = Persona(site, self) - # self.admin = AdminPage(site, self) - - return self.page - - def init_idp(self): - # Init IDP data - try: - self.key = M2Crypto.RSA.load_key(self.idp_key_file, - lambda *args: None) - except Exception as e: # pylint: disable=broad-except - self.debug('Failed to init Persona provider: %r' % e) - return None - - def on_enable(self): - super(IdpProvider, self).on_enable() - self.init_idp() - - def get_client_display_name(self, clientid): - return clientid - - def consent_to_display(self, consentdata): - return [] - - -class Installer(ProviderInstaller): - - def __init__(self, *pargs): - super(Installer, self).__init__() - self.name = 'persona' - self.pargs = pargs - - def install_args(self, group): - group.add_argument('--persona', choices=['yes', 'no'], default='yes', - help='Configure Persona Provider') - - def configure(self, opts, changes): - if opts['persona'] != 'yes': - return - - # Check storage path is present or create it - path = os.path.join(opts['data_dir'], 'persona') - if not os.path.exists(path): - os.makedirs(path, 0o700) - - keyfile = os.path.join(path, 'persona.key') - exponent = 0x10001 - key = M2Crypto.RSA.gen_key(2048, exponent) - key.save_key(keyfile, cipher=None) - key_n = 0 - for c in key.n[4:]: - key_n = (key_n*256) + ord(c) - wellknown = dict() - wellknown['authentication'] = ('%s/persona/SignIn/' - % opts['instanceurl']) - wellknown['provisioning'] = '%s/persona/' % opts['instanceurl'] - wellknown['public-key'] = {'algorithm': 'RS', - 'e': str(exponent), - 'n': str(key_n)} - with open(os.path.join(opts['wellknown_dir'], 'browserid'), 'w') as f: - f.write(json.dumps(wellknown)) - - # Add configuration data to database - po = PluginObject(*self.pargs) - po.name = 'persona' - po.wipe_data() - po.wipe_config_values() - config = {'issuer domain': opts['hostname'], - 'idp key file': keyfile, - 'allowed domains': opts['hostname']} - po.save_plugin_config(config) - - # Update global config to add login plugin - po.is_enabled = True - po.save_enabled_state() - - # Fixup permissions so only the ipsilon user can read these files - files.fix_user_dirs(path, opts['system_user']) diff --git a/man/ipsilon-server-install.1 b/man/ipsilon-server-install.1 index f91e93d..703067e 100644 --- a/man/ipsilon-server-install.1 +++ b/man/ipsilon-server-install.1 @@ -87,9 +87,6 @@ Configure OpenID Provider \fB\-\-openid\-dburi\fR \fIOPENID_DBURI\fR OpenID database URI (override template) .TP -\fB\-\-persona\fR -Configure Persona Provider -.TP \fB\-\-saml2\fR Configure SAML2 Provider .TP diff --git a/setup.py b/setup.py index ca09217..745854a 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,6 @@ setup( 'ipsilon.providers.openid.extensions', 'ipsilon.providers.openidc', 'ipsilon.providers.openidc.plugins', - 'ipsilon.providers.persona', 'ipsilon.authz', 'ipsilon.user', 'ipsilon.tools', 'ipsilon.helpers', 'tests', 'tests.helpers'], @@ -59,7 +58,6 @@ setup( (DATA+'templates/saml2', glob('templates/saml2/*.html')), (DATA+'templates/openid', glob('templates/openid/*')), (DATA+'templates/openidc', glob('templates/openidc/*')), - (DATA+'templates/persona', glob('templates/persona/*.html')), (DATA+'templates/install', glob('templates/install/*.conf')), (DATA+'templates/install/openidc', glob('templates/install/openidc/*.conf')), diff --git a/templates/persona/provisioning.html b/templates/persona/provisioning.html deleted file mode 100644 index a693cac..0000000 --- a/templates/persona/provisioning.html +++ /dev/null @@ -1,62 +0,0 @@ -{% extends "master.html" %} -{% block main %} -
-
-

This page is used internally

-
-
- - - -{% endblock %} diff --git a/templates/persona/signin_result.html b/templates/persona/signin_result.html deleted file mode 100644 index cda130d..0000000 --- a/templates/persona/signin_result.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "master.html" %} -{% block main %} -
-
-

This page is used internally

-
-
- - - -{% endblock %} From 6d4b6a631c1fc1aa20dc23d62f253ed72f9b6cea Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Nov 16 2017 13:58:28 +0000 Subject: [PATCH 2/3] Handle root gracefully on uninstalled plugin Signed-off-by: Patrick Uiterwijk --- diff --git a/ipsilon/root.py b/ipsilon/root.py index f2b3b2b..9d2f819 100644 --- a/ipsilon/root.py +++ b/ipsilon/root.py @@ -5,7 +5,7 @@ from ipsilon.util.webfinger import WebFinger from ipsilon.util import errors from ipsilon.login.common import Login from ipsilon.login.common import Logout -from ipsilon.admin.common import Admin +from ipsilon.admin.common import Admin, AdminError from ipsilon.providers.common import LoadProviders from ipsilon.admin.loginstack import LoginStack from ipsilon.admin.info import InfoPlugins @@ -65,8 +65,11 @@ class Root(Page): providers = [] for plugin in self._site['provider_config'].enabled: # pylint: disable=no-member,protected-access - obj = self.admin.providers._get_plugin_obj(plugin) - providers.extend(obj.get_providers()) + try: + obj = self.admin.providers._get_plugin_obj(plugin) + providers.extend(obj.get_providers()) + except AdminError: + self.error('Failed to load plugin %s' % plugin) providers = sorted(providers, key=lambda provider: provider.name) return self._template('index.html', title='Ipsilon', providers=providers, heads=self.html_heads) From 06985c530e03fee1f19d996c46b06583e6c5c8cb Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Nov 16 2017 14:00:55 +0000 Subject: [PATCH 3/3] Let dbupgrades handle missing plugins gracefully Signed-off-by: Patrick Uiterwijk --- diff --git a/ipsilon/tools/dbupgrade.py b/ipsilon/tools/dbupgrade.py index 46cdfcd..a4fbf96 100644 --- a/ipsilon/tools/dbupgrade.py +++ b/ipsilon/tools/dbupgrade.py @@ -6,6 +6,7 @@ import cherrypy import os from jinja2 import Environment, FileSystemLoader import ipsilon.util.sessions +from ipsilon.admin.common import AdminError from ipsilon.util.data import AdminStore, Store, UserStore, TranStore from ipsilon.util.sessions import SqlSession, EtcdSession from ipsilon.root import Root @@ -115,6 +116,9 @@ def execute_upgrade(cfgfile): 'authz_config']: for plugin in root._site[facility].enabled: logger.info('Handling plugin %s', plugin) + if not plugin in root._site[facility].available: + logger.error('Plugin was unavailable') + continue plugin = root._site[facility].available[plugin] logger.info('Creating plugin AdminStore table') adminstore.create_plugin_data_table(plugin.name)