class ZopeStore(OpenIDStore): """Zope OpenID store. This class implements an OpenID store which uses the ZODB. """ def __init__(self): self.associations=OOBTree() self.handles=OOBTree() self.nonces=OITreeSet() self.noncetimeline=PersistentList() self.assoctimeline=PersistentList() def getAssociationKey(self, server_url, handle): """Generate a key used to identify an association in our storage. """ if handle is None: return self.handles[server_url][0] return (server_url, handle) def storeAssociation(self, server_url, association): key=self.getAssociationKey(server_url, association.handle) self.associations[key]=association.serialize() now=time.time() def getKey(item): return self.getAssociation(item[0], item[1], remove=False).getExpiresIn(now) lst=self.handles.get(server_url, []) lst.append(key) lst.sort(key=getKey) self.handles[server_url]=lst self.assoctimeline.append((association.issued+association.lifetime, key)) def getAssociation(self, server_url, handle=None, remove=True): try: key=self.getAssociationKey(server_url, handle) assoc=Association.deserialize(self.associations[key]) except KeyError: return None if remove and assoc.getExpiresIn()==0: self.removeAssociation(server_url, handle) return None return assoc def removeAssociation(self, server_url, handle): key=self.getAssociationKey(server_url, handle) try: assoc=Association.deserialize(self.associations[key]) del self.associations[key] lst=self.handles[server_url] lst.remove(key) self.handles[server_url]=lst self.assoctimeline.remove((assoc.issued+assoc.lifetime, key)) return True except KeyError: return False def useNonce(self, server_url, timestamp, salt): nonce = (salt, server_url) if nonce in self.nonces: return False self.nonces.insert(nonce) if not hasattr(self, "noncetimeline"): # BBB for store instances from before 1.0b2 self.noncetimeline=PersistentList() self.noncetimeline.append((timestamp, nonce)) return True def cleanupNonces(self): if not hasattr(self, "noncetimeline"): return 0 cutoff=time.time()+SKEW count=0 for (timestamp,nonce) in self.noncetimeline: if timestamp<cutoff: self.noncetimeline.remove((timestamp,nonce)) self.nonces.remove(nonce) count+=1 return count def cleanupAssociations(self): if not hasattr(self, "assoctimeline"): return 0 now=time.time() count=0 expired=(key for (timestamp,key) in self.assoctimeline if timestamp<=now) for key in expired: self.removeAssociation(*key) count+=1 return count
class ZopeStore(OpenIDStore): """Zope OpenID store. This class implements an OpenID store which uses the ZODB. """ def __init__(self): self.associations = OOBTree() self.handles = OOBTree() self.nonces = OITreeSet() self.noncetimeline = PersistentList() self.assoctimeline = PersistentList() def getAssociationKey(self, server_url, handle): """Generate a key used to identify an association in our storage. """ if handle is None: return self.handles[server_url][0] return (server_url, handle) def storeAssociation(self, server_url, association): key = self.getAssociationKey(server_url, association.handle) self.associations[key] = association.serialize() now = time.time() def getKey(item): return self.getAssociation(item[0], item[1], remove=False).getExpiresIn(now) lst = self.handles.get(server_url, []) lst.append(key) lst.sort(key=getKey) self.handles[server_url] = lst if not hasattr(self, "assoctimeline"): # BBB for versions < 1.0b2 self.assoctimeline = PersistentList() self.assoctimeline.append( (association.issued + association.lifetime, key)) def getAssociation(self, server_url, handle=None, remove=True): try: key = self.getAssociationKey(server_url, handle) assoc = Association.deserialize(self.associations[key]) except KeyError: return None if remove and assoc.getExpiresIn() == 0: self.removeAssociation(server_url, handle) return None return assoc def removeAssociation(self, server_url, handle): key = self.getAssociationKey(server_url, handle) try: assoc = Association.deserialize(self.associations[key]) del self.associations[key] lst = self.handles[server_url] lst.remove(key) self.handles[server_url] = lst self.assoctimeline.remove((assoc.issued + assoc.lifetime, key)) return True except KeyError: return False def useNonce(self, server_url, timestamp, salt): nonce = (salt, server_url) if nonce in self.nonces: return False self.nonces.insert(nonce) if not hasattr(self, "noncetimeline"): # BBB for store instances from before 1.0b2 self.noncetimeline = PersistentList() self.noncetimeline.append((timestamp, nonce)) return True def cleanupNonces(self): if not hasattr(self, "noncetimeline"): return 0 cutoff = time.time() + SKEW count = 0 for (timestamp, nonce) in self.noncetimeline: if timestamp < cutoff: self.noncetimeline.remove((timestamp, nonce)) self.nonces.remove(nonce) count += 1 return count def cleanupAssociations(self): if not hasattr(self, "assoctimeline"): return 0 now = time.time() count = 0 expired = (key for (timestamp, key) in self.assoctimeline if timestamp <= now) for key in expired: self.removeAssociation(*key) count += 1 return count
class SAML2Plugin(BasePlugin): """SAML2 plugin. """ implements(IRolesPlugin, IUserEnumerationPlugin) meta_type = "collective.saml2auth plugin" security = ClassSecurityInfo() # ZMI tab for configuration page manage_options = (( { 'label': 'Configuration', 'action': 'manage_config' }, { 'label': 'Users', 'action': 'manage_users' }, ) + BasePlugin.manage_options + Cacheable.manage_options) security.declareProtected(ManagePortal, 'manage_config') manage_config = PageTemplateFile('www/config', globals(), __name__='manage_config') security.declareProtected(ManageUsers, 'manage_users') manage_users = PageTemplateFile('www/manage_users', globals(), __name__='manage_users') def __init__(self, id, title=None): self._setId(id) self.title = title self._roles = () self._logins = OITreeSet() def addUser(self, userid): if userid in self._logins: return self._logins.insert(userid) def removeUser(self, userid): if userid not in self._logins: return self._logins.remove(userid) def listUserInfo(self): """ -> ( {}, ...{} ) o Return one mapping per user, with the following keys: - 'user_id' - 'login_name' """ return [{'user_id': x, 'login_name': x} for x in self._logins] security.declareProtected(ManageUsers, 'manage_addUser') @csrf_only @postonly def manage_addUser(self, user_id, RESPONSE=None, REQUEST=None): """ Add a user via the ZMI. """ self.addUser(user_id) message = 'User+added' if RESPONSE is not None: RESPONSE.redirect('{}/manage_users?manage_tabs_message={}'.format( self.absolute_url(), message)) security.declareProtected(ManageUsers, 'manage_removeUsers') @csrf_only @postonly def manage_removeUsers(self, user_ids, RESPONSE=None, REQUEST=None): """ Remove one or more users via the ZMI. """ user_ids = filter(None, user_ids) if not user_ids: message = 'no+users+selected' else: for user_id in user_ids: self.removeUser(user_id) message = 'Users+removed' if RESPONSE is not None: RESPONSE.redirect('{}/manage_users?manage_tabs_message={}'.format( self.absolute_url(), message)) # IUserEnumerationPlugin implementation def enumerateUsers(self, id=None, login=None, exact_match=False, sort_by=None, max_results=None, **kw): key = id and id or login user_infos = [] pluginid = self.getId() # We do not provide search for additional keywords if kw: return () if not key: # Return all users for login in self._logins: user_infos.append({ "id": login, "login": login, "pluginid": pluginid, }) elif key in self._logins: # User does exists user_infos.append({ "id": key, "login": key, "pluginid": pluginid, }) else: # User does not exists return () if max_results is not None and max_results >= 0: user_infos = user_infos[:max_results] return tuple(user_infos) # IRolesPlugin def getRolesForPrincipal(self, principal, request=None): # Return a list of roles for the given principal (a user or group). if principal.getId() in self._logins: return self._roles return () security.declareProtected(ManagePortal, 'manage_updateConfig') @postonly def manage_updateConfig(self, REQUEST): """Update configuration of SAML2 plugin. """ response = REQUEST.response roles = REQUEST.form.get('roles') self._roles = tuple([role.strip() for role in roles.split(',')]) response.redirect('%s/manage_config?manage_tabs_message=%s' % (self.absolute_url(), 'Configuration+updated.')) def roles(self): """Accessor for config form""" return ','.join(self._roles)
class Saml2WebSSOPlugin(BasePlugin): """Saml2 Web SSO authentication plugin using HTTP POST binding. """ implements(IAuthenticationPlugin, IExtractionPlugin, IRolesPlugin, IUserEnumerationPlugin) meta_type = "Saml2 Web SSO plugin" security = ClassSecurityInfo() # ZMI tab for configuration page manage_options = (({ 'label': 'Configuration', 'action': 'manage_config' }, ) + BasePlugin.manage_options + Cacheable.manage_options) security.declareProtected(ManagePortal, 'manage_config') manage_config = PageTemplateFile('www/config', globals(), __name__='manage_config') def __init__(self, id, title=None): self._setId(id) self.title = title self.idp_url = None self.sp_url = None self.signing_cert = None self.issuer_id = None self.clock_skew = 60 self.authn_context = AUTHN_CONTEXT self.internal_network = '' self.internal_authn_context = INTERNAL_AUTHN_CONTEXT self._roles = () self._logins = OITreeSet() # IExtractionPlugin implementation def extractCredentials(self, request): creds = {} if 'SAMLResponse' in request.form: doc = request.form['SAMLResponse'] try: doc = base64.b64decode(doc) except TypeError: return {} try: resp = CreateFromDocument( doc, context=self._signature_context(), ) except VerifyError, e: logger.warning('Signature verification error: %s' % str(e)) return {} if not resp.Destination.startswith(self.sp_url): logger.warning('Wrong destination in SAML response.') return {} status = resp.Status.StatusCode.Value if status != STATUS_SUCCESS: # Status code may contain a second-level status code. if resp.Status.StatusCode.StatusCode: status += ': ' + resp.Status.StatusCode.StatusCode.Value logger.warning('Failed SAML2 request with status code: %s.' % status) return {} # Verfiy issue time of response. now = datetime.utcnow() issue_instant = resp.IssueInstant.astimezone(tz=pytz.utc).replace( tzinfo=None) delta = timedelta(seconds=self.clock_skew) if (now + delta) < issue_instant or (now - delta) > issue_instant: logger.warning('Clock skew too great.') return {} # We expect the subject and attributes in the first assertion if len(resp.Assertion) > 0: assertion = resp.Assertion[0] subject = assertion.Subject.NameID.value().encode('utf8') attributes = self._extract_attributes(assertion) creds['subject'] = subject creds['attributes'] = attributes self._logins.insert(subject) else: logger.warning('Missing assertion') return {} return creds
class OIDCPlugin(BasePlugin): """OIDC authentication plugin. """ implements(IRolesPlugin, IUserEnumerationPlugin, IChallengePlugin) meta_type = "ftw.oidcauth plugin" security = ClassSecurityInfo() # ZMI tab for configuration page manage_options = (( { 'label': 'Configuration', 'action': 'manage_config' }, { 'label': 'Users', 'action': 'manage_users' }, ) + BasePlugin.manage_options + Cacheable.manage_options) security.declareProtected(ManagePortal, 'manage_config') manage_config = PageTemplateFile('www/config', globals(), __name__='manage_config') security.declareProtected(ManageUsers, 'manage_users') manage_users = PageTemplateFile('www/manage_users', globals(), __name__='manage_users') def __init__(self, id, title=None): self._setId(id) self.title = title self._roles = () self.client_id = None self.client_secret = None self.scope = u'openid email profile' self.sign_algorithm = u'RS256' self.username_attribute = u'sub' self.authentication_endpoint = None self.token_endpoint = None self.user_endpoint = None self.jwks_endpoint = None self._auto_provisioning_enabled = True self.properties_mapping = json.dumps({ "userid": "sub", "fullname": "name", "email": "email" }) self.logins = OITreeSet() security.declarePrivate('challenge') # Initiate a challenge to the user to provide credentials. def challenge(self, request, response, **kw): request.response.setCookie('oidc_next', request['ACTUAL_URL']) uri = '{}?response_type=code&scope={}&client_id={}&redirect_uri={}'.format( self.authentication_endpoint, self.scope, self.client_id, get_oidc_request_url(quote_=True)) response.redirect(uri, lock=True, status=302) return True def addUser(self, userid): if userid in self.logins: return self.logins.insert(userid) def removeUser(self, userid): if userid not in self.logins: return self.logins.remove(userid) def listUserInfo(self): """ -> ( {}, ...{} ) o Return one mapping per user, with the following keys: - 'user_id' - 'login_name' """ return [{'user_id': x, 'login_name': x} for x in self.logins] security.declareProtected(ManageUsers, 'manage_addUser') @csrf_only @postonly def manage_addUser(self, user_id, RESPONSE=None, REQUEST=None): """ Add a user via the ZMI. """ self.addUser(user_id) message = 'User+added' if RESPONSE is not None: RESPONSE.redirect('{}/manage_users?manage_tabs_message={}'.format( self.absolute_url(), message)) security.declareProtected(ManageUsers, 'manage_removeUsers') @csrf_only @postonly def manage_removeUsers(self, user_ids, RESPONSE=None, REQUEST=None): """ Remove one or more users via the ZMI. """ user_ids = filter(None, user_ids) if not user_ids: message = 'no+users+selected' else: for user_id in user_ids: self.removeUser(user_id) message = 'Users+removed' if RESPONSE is not None: RESPONSE.redirect('{}/manage_users?manage_tabs_message={}'.format( self.absolute_url(), message)) # IUserEnumerationPlugin implementation def enumerateUsers(self, id=None, login=None, exact_match=False, sort_by=None, max_results=None, **kw): key = id and id or login user_infos = [] pluginid = self.getId() # We do not provide search for additional keywords if kw: return () if not key: # Return all users for login in self.logins: user_infos.append({ "id": login, "login": login, "pluginid": pluginid, }) elif key in self.logins: # User does exists user_infos.append({ "id": key, "login": key, "pluginid": pluginid, }) else: # User does not exists return () if max_results is not None and max_results >= 0: user_infos = user_infos[:max_results] return tuple(user_infos) # IRolesPlugin def getRolesForPrincipal(self, principal, request=None): # Return a list of roles for the given principal (a user or group). if principal.getId() in self.logins: return self._roles return () security.declareProtected(ManagePortal, 'manage_updateConfig') @postonly def manage_updateConfig(self, REQUEST): """Update configuration of OIDC plugin. """ response = REQUEST.response self.client_id = REQUEST.form.get('client-id') self.client_secret = REQUEST.form.get('client-secret') self.scope = REQUEST.form.get('scope') self.sign_algorithm = REQUEST.form.get('sign-algorithm') self.authentication_endpoint = REQUEST.form.get( 'authentication-endpoint') self.token_endpoint = REQUEST.form.get('token-endpoint') self.user_endpoint = REQUEST.form.get('user-endpoint') self.jwks_endpoint = REQUEST.form.get('jwks-endpoint') self._auto_provisioning_enabled = REQUEST.form.get( 'auto-provisioning-enabled') roles = REQUEST.form.get('roles') self._roles = tuple( [role.strip() for role in roles.split(',') if role]) # only update props if json is valid props = REQUEST.form.get('properties-mapping') props_data = self.get_valid_json(props) if not props_data: response.redirect( '%s/manage_config?manage_tabs_message=%s' % (self.absolute_url(), 'Please make sure the json is valid!')) return self.properties_mapping = props_data response.redirect('%s/manage_config?manage_tabs_message=%s' % (self.absolute_url(), 'Configuration+updated.')) def auto_provisioning_enabled(self): return True if self._auto_provisioning_enabled else False def roles(self): return ','.join(self._roles) @staticmethod def get_valid_json(props): try: data = ast.literal_eval(props) except ValueError as e: return False return data