## $Id: authentication.py 9313 2012-10-08 09:16:59Z henrik $ ## ## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann ## 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 2 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, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## """Authentication for Kofa. """ import grok from zope.event import notify from zope.component import getUtility, getUtilitiesFor from zope.component.interfaces import IFactory from zope.interface import Interface from zope.schema import getFields from zope.securitypolicy.interfaces import ( IPrincipalRoleMap, IPrincipalRoleManager) from zope.pluggableauth.factories import Principal from zope.pluggableauth.plugins.session import SessionCredentialsPlugin from zope.pluggableauth.interfaces import ( ICredentialsPlugin, IAuthenticatorPlugin, IAuthenticatedPrincipalFactory, AuthenticatedPrincipalCreated) from zope.publisher.interfaces import IRequest from zope.password.interfaces import IPasswordManager from zope.securitypolicy.principalrole import principalRoleManager from waeup.kofa.interfaces import (ILocalRoleSetEvent, IUserAccount, IAuthPluginUtility, IPasswordValidator, IKofaPrincipal, IKofaPrincipalInfo, IKofaPluggable, IBatchProcessor, IGNORE_MARKER) from waeup.kofa.utils.batching import BatchProcessor def setup_authentication(pau): """Set up plugguble authentication utility. Sets up an IAuthenticatorPlugin and ICredentialsPlugin (for the authentication mechanism) Then looks for any external utilities that want to modify the PAU. """ pau.credentialsPlugins = ('No Challenge if Authenticated', 'credentials') pau.authenticatorPlugins = ('users',) # Give any third-party code and subpackages a chance to modify the PAU auth_plugin_utilities = getUtilitiesFor(IAuthPluginUtility) for name, util in auth_plugin_utilities: util.register(pau) def get_principal_role_manager(): """Get a role manager for principals. If we are currently 'in a site', return the role manager for the portal or the global rolemanager else. """ portal = grok.getSite() if portal is not None: return IPrincipalRoleManager(portal) return principalRoleManager class KofaSessionCredentialsPlugin(grok.GlobalUtility, SessionCredentialsPlugin): grok.provides(ICredentialsPlugin) grok.name('credentials') loginpagename = 'login' loginfield = 'form.login' passwordfield = 'form.password' class KofaPrincipalInfo(object): """An implementation of IKofaPrincipalInfo. A Kofa principal info is created with id, login, title, description, phone, email, public_name and user_type. """ grok.implements(IKofaPrincipalInfo) def __init__(self, id, title, description, email, phone, public_name, user_type): self.id = id self.title = title self.description = description self.email = email self.phone = phone self.public_name = public_name self.user_type = user_type self.credentialsPlugin = None self.authenticatorPlugin = None class KofaPrincipal(Principal): """A portal principal. Kofa principals provide an extra `email`, `phone`, `public_name` and `user_type` attribute extending ordinary principals. """ grok.implements(IKofaPrincipal) def __init__(self, id, title=u'', description=u'', email=u'', phone=None, public_name=u'', user_type=u'', prefix=None): self.id = id if prefix is not None: self.id = '%s.%s' % (prefix, self.id) self.title = title self.description = description self.groups = [] self.email = email self.phone = phone self.public_name = public_name self.user_type = user_type def __repr__(self): return 'KofaPrincipal(%r)' % self.id class AuthenticatedKofaPrincipalFactory(grok.MultiAdapter): """Creates 'authenticated' Kofa principals. Adapts (principal info, request) to a KofaPrincipal instance. This adapter is used by the standard PAU to transform KofaPrincipalInfos into KofaPrincipal instances. """ grok.adapts(IKofaPrincipalInfo, IRequest) grok.implements(IAuthenticatedPrincipalFactory) def __init__(self, info, request): self.info = info self.request = request def __call__(self, authentication): principal = KofaPrincipal( self.info.id, self.info.title, self.info.description, self.info.email, self.info.phone, self.info.public_name, self.info.user_type, authentication.prefix, ) notify( AuthenticatedPrincipalCreated( authentication, principal, self.info, self.request)) return principal class Account(grok.Model): grok.implements(IUserAccount) _local_roles = dict() def __init__(self, name, password, title=None, description=None, email=None, phone=None, public_name=None, roles = []): self.name = name if title is None: title = name self.title = title self.description = description self.email = email self.phone = phone self.public_name = public_name self.setPassword(password) self.setSiteRolesForPrincipal(roles) # We don't want to share this dict with other accounts self._local_roles = dict() def setPassword(self, password): passwordmanager = getUtility(IPasswordManager, 'SSHA') self.password = passwordmanager.encodePassword(password) def checkPassword(self, password): if not isinstance(password, basestring): return False if not self.password: # unset/empty passwords do never match return False passwordmanager = getUtility(IPasswordManager, 'SSHA') return passwordmanager.checkPassword(self.password, password) def getSiteRolesForPrincipal(self): prm = get_principal_role_manager() roles = [x[0] for x in prm.getRolesForPrincipal(self.name) if x[0].startswith('waeup.')] return roles def setSiteRolesForPrincipal(self, roles): prm = get_principal_role_manager() old_roles = self.getSiteRolesForPrincipal() if sorted(old_roles) == sorted(roles): return for role in old_roles: # Remove old roles, not to be set now... if role.startswith('waeup.') and role not in roles: prm.unsetRoleForPrincipal(role, self.name) for role in roles: # Convert role to ASCII string to be in line with the # event handler prm.assignRoleToPrincipal(str(role), self.name) return roles = property(getSiteRolesForPrincipal, setSiteRolesForPrincipal) def getLocalRoles(self): return self._local_roles def notifyLocalRoleChanged(self, obj, role_id, granted=True): objects = self._local_roles.get(role_id, []) if granted and obj not in objects: objects.append(obj) if not granted and obj in objects: objects.remove(obj) self._local_roles[role_id] = objects if len(objects) == 0: del self._local_roles[role_id] self._p_changed = True return class UserAuthenticatorPlugin(grok.GlobalUtility): grok.implements(IAuthenticatorPlugin) grok.provides(IAuthenticatorPlugin) grok.name('users') def authenticateCredentials(self, credentials): if not isinstance(credentials, dict): return None if not ('login' in credentials and 'password' in credentials): return None account = self.getAccount(credentials['login']) if account is None: return None if not account.checkPassword(credentials['password']): return None return KofaPrincipalInfo( id=account.name, title=account.title, description=account.description, email=account.email, phone=account.phone, public_name=account.public_name, user_type=u'user') def principalInfo(self, id): account = self.getAccount(id) if account is None: return None return KofaPrincipalInfo( id=account.name, title=account.title, description=account.description, email=account.email, phone=account.phone, public_name=account.public_name, user_type=u'user') def getAccount(self, login): # ... look up the account object and return it ... userscontainer = self.getUsersContainer() if userscontainer is None: return return userscontainer.get(login, None) def addAccount(self, account): userscontainer = self.getUsersContainer() if userscontainer is None: return # XXX: complain if name already exists... userscontainer.addAccount(account) def addUser(self, name, password, title=None, description=None): userscontainer = self.getUsersContainer() if userscontainer is None: return userscontainer.addUser(name, password, title, description) def getUsersContainer(self): site = grok.getSite() return site['users'] class PasswordValidator(grok.GlobalUtility): grok.implements(IPasswordValidator) def validate_password(self, pw, pw_repeat): errors = [] if len(pw) < 3: errors.append('Password must have at least 3 chars.') if pw != pw_repeat: errors.append('Passwords do not match.') return errors class LocalRoleSetEvent(object): grok.implements(ILocalRoleSetEvent) def __init__(self, object, role_id, principal_id, granted=True): self.object = object self.role_id = role_id self.principal_id = principal_id self.granted = granted @grok.subscribe(IUserAccount, grok.IObjectRemovedEvent) def handle_account_removed(account, event): """When an account is removed, local and global roles might have to be deleted. """ local_roles = account.getLocalRoles() principal = account.name for role_id, object_list in local_roles.items(): for object in object_list: try: role_manager = IPrincipalRoleManager(object) except TypeError: # No Account object, no role manager, no local roles to remove continue role_manager.unsetRoleForPrincipal(role_id, principal) role_manager = IPrincipalRoleManager(grok.getSite()) roles = account.getSiteRolesForPrincipal() for role_id in roles: role_manager.unsetRoleForPrincipal(role_id, principal) return @grok.subscribe(IUserAccount, grok.IObjectAddedEvent) def handle_account_added(account, event): """When an account is added, the local owner role and the global AcademicsOfficer role must be set. """ # We set the local Owner role role_manager_account = IPrincipalRoleManager(account) role_manager_account.assignRoleToPrincipal( 'waeup.local.Owner', account.name) # We set the global AcademicsOfficer role site = grok.getSite() role_manager_site = IPrincipalRoleManager(site) role_manager_site.assignRoleToPrincipal( 'waeup.AcademicsOfficer', account.name) # Finally we have to notify the user account that the local role # of the same object has changed notify(LocalRoleSetEvent( account, 'waeup.local.Owner', account.name, granted=True)) return @grok.subscribe(Interface, ILocalRoleSetEvent) def handle_local_role_changed(obj, event): site = grok.getSite() if site is None: return users = site.get('users', None) if users is None: return if event.principal_id not in users.keys(): return user = users[event.principal_id] user.notifyLocalRoleChanged(event.object, event.role_id, event.granted) return @grok.subscribe(Interface, grok.IObjectRemovedEvent) def handle_local_roles_on_obj_removed(obj, event): try: role_map = IPrincipalRoleMap(obj) except TypeError: # no map, no roles to remove return for local_role, user_name, setting in role_map.getPrincipalsAndRoles(): notify(LocalRoleSetEvent( obj, local_role, user_name, granted=False)) return class UserAccountFactory(grok.GlobalUtility): """A factory for user accounts. This factory is only needed for imports. """ grok.implements(IFactory) grok.name(u'waeup.UserAccount') title = u"Create a user.", description = u"This factory instantiates new user account instances." def __call__(self, *args, **kw): return Account(name=None, password='') def getInterfaces(self): return implementedBy(Account) class UserProcessor(BatchProcessor): """A batch processor for IUserAccount objects. """ grok.implements(IBatchProcessor) grok.provides(IBatchProcessor) grok.context(Interface) util_name = 'userprocessor' grok.name(util_name) name = u'User Processor' iface = IUserAccount location_fields = ['name',] factory_name = 'waeup.UserAccount' mode = None def parentsExist(self, row, site): return 'users' in site.keys() def entryExists(self, row, site): return row['name'] in site['users'].keys() def getParent(self, row, site): return site['users'] def getEntry(self, row, site): if not self.entryExists(row, site): return None parent = self.getParent(row, site) return parent.get(row['name']) def addEntry(self, obj, row, site): parent = self.getParent(row, site) parent.addAccount(obj) return def updateEntry(self, obj, row, site): """Update obj to the values given in row. """ changed = [] for key, value in row.items(): if key == 'roles': # We cannot simply set the roles attribute here because # we can't assure that the name attribute is set before # the roles attribute is set. continue # Skip fields to be ignored. if value == IGNORE_MARKER: continue if not hasattr(obj, key): continue setattr(obj, key, value) changed.append('%s=%s' % (key, value)) roles = row.get('roles', IGNORE_MARKER) if roles not in ('', IGNORE_MARKER): evalvalue = eval(roles) if isinstance(evalvalue, list): setattr(obj, 'roles', evalvalue) changed.append('roles=%s' % roles) # Log actions... items_changed = ', '.join(changed) grok.getSite().logger.info('%s - %s - updated: %s' % (self.name, row['name'], items_changed)) return class UsersPlugin(grok.GlobalUtility): """A plugin that updates users. """ grok.implements(IKofaPluggable) grok.name('users') deprecated_attributes = [] def setup(self, site, name, logger): return def update(self, site, name, logger): users = site['users'] items = getFields(IUserAccount).items() for user in users.values(): # Add new attributes for i in items: if not hasattr(user,i[0]): setattr(user,i[0],i[1].missing_value) logger.info( 'UsersPlugin: %s attribute %s added.' % ( user.name,i[0])) # Remove deprecated attributes for i in self.deprecated_attributes: try: delattr(user,i) logger.info( 'UsersPlugin: %s attribute %s deleted.' % ( user.name,i)) except AttributeError: pass return