| |
@@ -0,0 +1,253 @@
|
| |
+ # Copyright (C) 2016 Ipsilon project Contributors, for license see COPYING
|
| |
+
|
| |
+ from ipsilon.login.common import LoginFormBase, LoginManagerBase, \
|
| |
+ LoginManagerInstaller
|
| |
+ from ipsilon.util.plugin import PluginObject
|
| |
+ from ipsilon.util.policy import Policy
|
| |
+ from ipsilon.util import config as pconfig
|
| |
+ from ipsilon.providers.openid.extensions.ax import AP_MAP
|
| |
+ from ipsilon.providers.openid.store import OpenIDStore
|
| |
+ import cherrypy
|
| |
+ import logging
|
| |
+ import pickle
|
| |
+
|
| |
+ from openid.consumer import consumer
|
| |
+ from openid.extensions import pape, sreg, ax
|
| |
+
|
| |
+ # These are type URIs where we allow multiple results
|
| |
+ multival_type_uris = ['http://fedoauth.org/openid/schema/SSH/key']
|
| |
+
|
| |
+ openid_mapping = [
|
| |
+ ['fullname', 'fullname'],
|
| |
+ ['email', 'email'],
|
| |
+ ['lastname', 'surname'],
|
| |
+ ['nickname', 'nickname'],
|
| |
+ ['nickname', 'nickname'],
|
| |
+ ]
|
| |
+
|
| |
+
|
| |
+ class OpenID(LoginFormBase):
|
| |
+
|
| |
+ def __init__(self, site, mgr, page, forced_provider=None):
|
| |
+ super(OpenID, self).__init__(site, mgr, page)
|
| |
+ self.mapper = Policy(openid_mapping)
|
| |
+ self.forced_provider = forced_provider
|
| |
+
|
| |
+ def root(self, *args, **kwargs):
|
| |
+ self.trans = self.get_valid_transaction('login', **kwargs)
|
| |
+ context = self.create_tmpl_context()
|
| |
+ if self.forced_provider or cherrypy.request.method == 'POST':
|
| |
+ if self.forced_provider:
|
| |
+ provider = self.forced_provider
|
| |
+ else:
|
| |
+ provider = kwargs['login_name']
|
| |
+
|
| |
+ context['username'] = provider
|
| |
+ openid_ses = {}
|
| |
+ oidconsumer = consumer.Consumer(openid_ses, self.lm.datastore)
|
| |
+ try:
|
| |
+ request = oidconsumer.begin(provider)
|
| |
+ except consumer.DiscoveryFailure as exc:
|
| |
+ cherrypy.log.error('Failed discovery for %s: %s' %
|
| |
+ (provider, exc),
|
| |
+ severity=logging.WARN)
|
| |
+ return self._template('login/openid.html',
|
| |
+ error_username=True,
|
| |
+ error='Unable to discover OpenID',
|
| |
+ forced_provider=self.forced_provider,
|
| |
+ **context)
|
| |
+
|
| |
+ if request is None:
|
| |
+ cherrypy.log.error('No OpenID request generated')
|
| |
+ return self._template('login/openid.html',
|
| |
+ error_username=True,
|
| |
+ error='Unable to initiate OpenID',
|
| |
+ forced_provider=self.forced_provider,
|
| |
+ **context)
|
| |
+
|
| |
+ request.addExtension(sreg.SRegRequest(
|
| |
+ optional=['nickname', 'fullname', 'email', 'timezone']))
|
| |
+ request.addExtension(pape.Request([]))
|
| |
+
|
| |
+ ax_req = ax.FetchRequest()
|
| |
+ for prop in AP_MAP.keys():
|
| |
+ count = 1
|
| |
+ if prop in multival_type_uris:
|
| |
+ count = 'unlimited'
|
| |
+ ax_req.add(ax.AttrInfo(type_uri=prop, count=count))
|
| |
+ request.addExtension(ax_req)
|
| |
+
|
| |
+ trust_root = cherrypy.url('/')
|
| |
+ return_to = '%s/login/openid/response/?%s' % (
|
| |
+ cherrypy.url('/'), self.trans.get_GET_arg())
|
| |
+
|
| |
+ # Store the OpenID Session in the transaction
|
| |
+ self.trans.store({'openid_ses': pickle.dumps(openid_ses)})
|
| |
+
|
| |
+ if request.shouldSendRedirect():
|
| |
+ redirect_url = request.redirectURL(trust_root, return_to,
|
| |
+ False)
|
| |
+ raise cherrypy.HTTPRedirect(redirect_url)
|
| |
+ else:
|
| |
+ return request.htmlMarkup(
|
| |
+ trust_root, return_to,
|
| |
+ form_tag_attrs={'id': 'openid_mesage'}, immediate=False)
|
| |
+
|
| |
+ context['username'] = self.forced_provider or ''
|
| |
+ return self._template('login/openid.html',
|
| |
+ forced_provider=self.forced_provider,
|
| |
+ **context)
|
| |
+
|
| |
+ def response(self, *args, **kwargs):
|
| |
+ context = self.create_tmpl_context()
|
| |
+
|
| |
+ self.trans = self.get_valid_transaction('login', **kwargs)
|
| |
+ openid_ses = pickle.loads(self.trans.retrieve().get('openid_ses'))
|
| |
+ oidconsumer = consumer.Consumer(openid_ses, self.lm.datastore)
|
| |
+ return_to = '%s/login/openid/response/?%s' % (
|
| |
+ cherrypy.url('/'), self.trans.get_GET_arg())
|
| |
+ # We need to make sure the transaction ID does not get passed on
|
| |
+ info = oidconsumer.complete(kwargs, return_to)
|
| |
+ display_identifier = info.getDisplayIdentifier()
|
| |
+
|
| |
+ if info.status == consumer.FAILURE and display_identifier:
|
| |
+ cherrypy.log.error("OpenID auth failure: %s for %s" %
|
| |
+ (info.message, display_identifier),
|
| |
+ severity=logging.INFO)
|
| |
+ return self._template('login/openid.html',
|
| |
+ error='Unable to verify OpenID %s' %
|
| |
+ display_identifier,
|
| |
+ **context)
|
| |
+ elif info.status == consumer.CANCEL:
|
| |
+ cherrypy.log.error("OpenID auth cancelled", severity=logging.INFO)
|
| |
+ return self._template('login/openid.html',
|
| |
+ error='Login cancelled',
|
| |
+ **context)
|
| |
+ elif info.status == consumer.FAILURE:
|
| |
+ cherrypy.log.error("OpenID auth failure: %s" % info.message,
|
| |
+ severity=logging.INFO)
|
| |
+ return self._template('login/openid.html',
|
| |
+ error='Unable to verify OpenID',
|
| |
+ **context)
|
| |
+ elif info.status == consumer.SUCCESS:
|
| |
+ if self.forced_provider:
|
| |
+ if self.forced_provider != info.endpoint.server_url:
|
| |
+ cherrypy.log.error('Claim received for invalid provider:'
|
| |
+ ' %s' % info.endpoint.server_url)
|
| |
+ return self._template('login/openid.html',
|
| |
+ error='Invalid provider!',
|
| |
+ error_username=True,
|
| |
+ **context)
|
| |
+
|
| |
+ userdata = self.make_userdata(info)
|
| |
+ return self.lm.auth_successful(self.trans,
|
| |
+ info.getDisplayIdentifier(),
|
| |
+ userdata=userdata)
|
| |
+ else:
|
| |
+ return 'Strange state: %s' % info.status
|
| |
+ response.exposed = True
|
| |
+
|
| |
+ def make_userdata(self, openid_response):
|
| |
+ sreg_resp = sreg.SRegResponse.fromSuccessResponse(openid_response)
|
| |
+ ax_resp = ax.FetchResponse.fromSuccessResponse(openid_response)
|
| |
+
|
| |
+ user_info = {}
|
| |
+
|
| |
+ if sreg_resp:
|
| |
+ user_info['username'] = sreg_resp.get('nickname')
|
| |
+ user_info['fullname'] = sreg_resp.get('fullname')
|
| |
+ user_info['email'] = sreg_resp.get('email')
|
| |
+ user_info['timezone'] = sreg_resp.get('timezone')
|
| |
+
|
| |
+ if ax_resp:
|
| |
+ for type_uri in AP_MAP:
|
| |
+ value = ax_resp.get(type_uri)
|
| |
+ if value:
|
| |
+ key = AP_MAP[type_uri]
|
| |
+ user_info[key] = value
|
| |
+
|
| |
+ userattrs, extras = self.mapper.map_attributes(user_info)
|
| |
+ userattrs['_extras'] = {'openid': extras}
|
| |
+
|
| |
+ return userattrs
|
| |
+
|
| |
+
|
| |
+ class LoginManager(LoginManagerBase):
|
| |
+
|
| |
+ def __init__(self, *args, **kwargs):
|
| |
+ super(LoginManager, self).__init__(*args, **kwargs)
|
| |
+ self.name = 'openid'
|
| |
+ self.path = 'openid'
|
| |
+ self.page = None
|
| |
+ self.service_name = 'openid'
|
| |
+ self.datastore = None
|
| |
+ self.description = """
|
| |
+ Login provider using OpenID
|
| |
+ """
|
| |
+ self.new_config(
|
| |
+ self.name,
|
| |
+ pconfig.String(
|
| |
+ 'database url',
|
| |
+ 'Database URL for OpenID temp storage',
|
| |
+ 'openid.sqlite'),
|
| |
+ pconfig.String(
|
| |
+ 'username text',
|
| |
+ 'Text used to ask for the identity URL at login time.',
|
| |
+ 'OpenID Identity'),
|
| |
+ pconfig.String(
|
| |
+ 'help text',
|
| |
+ 'Text used to guide the user at login time.',
|
| |
+ 'Enter your OpenID identity')
|
| |
+ )
|
| |
+
|
| |
+ def used_datastores(self):
|
| |
+ return [self.datastore]
|
| |
+
|
| |
+ @property
|
| |
+ def help_text(self):
|
| |
+ return self.get_config_value('help text')
|
| |
+
|
| |
+ @property
|
| |
+ def username_text(self):
|
| |
+ return self.get_config_value('username text')
|
| |
+
|
| |
+ def get_tree(self, site):
|
| |
+ self.page = OpenID(site, self, 'login/openid')
|
| |
+ return self.page
|
| |
+
|
| |
+ def on_enable(self):
|
| |
+ super(LoginManager, self).on_enable()
|
| |
+ self.datastore = OpenIDStore(self.get_config_value('database url'))
|
| |
+
|
| |
+
|
| |
+ class Installer(LoginManagerInstaller):
|
| |
+
|
| |
+ def __init__(self, *pargs):
|
| |
+ super(Installer, self).__init__()
|
| |
+ self.name = 'openid'
|
| |
+ self.pargs = pargs
|
| |
+
|
| |
+ def install_args(self, group):
|
| |
+ group.add_argument('--login-openid', choices=['yes', 'no'],
|
| |
+ default='no', help='Configure OpenID login')
|
| |
+
|
| |
+ def configure(self, opts, changes):
|
| |
+ if opts['login_openid'] != 'yes':
|
| |
+ return
|
| |
+
|
| |
+ # Add configuration data to database
|
| |
+ po = PluginObject(*self.pargs)
|
| |
+ po.name = 'openid'
|
| |
+ po.wipe_data()
|
| |
+ po.wipe_config_values()
|
| |
+ # We reuse the same database used by the openid provider side of
|
| |
+ # things. This will not cause any issues, and will result in less
|
| |
+ # files on the disk.
|
| |
+ config = {'database url': opts['openid_dburi'] or
|
| |
+ opts['database_url'] % {
|
| |
+ 'datadir': opts['data_dir'], 'dbname': 'openid'}}
|
| |
+ po.save_plugin_config(config)
|
| |
+
|
| |
+ # Update global config to add login plugin
|
| |
+ po.is_enabled = True
|
| |
+ po.save_enabled_state()
|
| |
twice the nickname?