## ## authentication.py ## Login : ## Started on Fri Sep 2 15:22:47 2011 Uli Fouquet ## $Id$ ## ## Copyright (C) 2011 Uli Fouquet ## 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 ## """ Authenticate students. """ import grok from zope.component import getUtility from zope.password.interfaces import IPasswordManager from zope.pluggableauth.interfaces import ( IAuthenticatorPlugin, ICredentialsPlugin) from zope.pluggableauth.plugins.session import ( SessionCredentialsPlugin, SessionCredentials) from zope.publisher.interfaces.http import IHTTPRequest from zope.session.interfaces import ISession from waeup.sirp.authentication import PrincipalInfo, get_principal_role_manager from waeup.sirp.interfaces import IAuthPluginUtility, IUserAccount from waeup.sirp.students.interfaces import IStudent class StudentAccount(grok.Adapter): """An adapter to turn student objects into accounts on-the-fly. """ grok.context(IStudent) grok.implements(IUserAccount) @property def name(self): return self.context.student_id @property def password(self): return getattr(self.context, 'password', None) @property def title(self): return self.context.name @property def description(self): return self.title def _get_roles(self): prm = get_principal_role_manager() roles = [x[0] for x in prm.getRolesForPrincipal(self.name) if x[0].startswith('waeup.')] return roles def _set_roles(self, roles): """Set roles for principal denoted by this account. """ prm = get_principal_role_manager() old_roles = self.roles 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: prm.assignRoleToPrincipal(role, self.name) return roles = property(_get_roles, _set_roles) def setPassword(self, password): """Set a password (LDAP-compatible) SSHA encoded. We do not store passwords in plaintext. Encrypted password is stored as unicode string. """ passwordmanager = getUtility(IPasswordManager, 'SSHA') self.context.password = u'%s' % ( passwordmanager.encodePassword(password)) def checkPassword(self, password): """Check whether the given `password` matches the one stored. """ if not isinstance(password, basestring): return False passwordmanager = getUtility(IPasswordManager, 'SSHA') return passwordmanager.checkPassword( self.context.password.encode('utf-8'), # turn unicode into bytes password) class StudentsAuthenticatorPlugin(grok.GlobalUtility): grok.implements(IAuthenticatorPlugin) grok.provides(IAuthenticatorPlugin) grok.name('students') def authenticateCredentials(self, credentials): """Authenticate `credentials`. `credentials` is a tuple (login, password). We look up students to find out whether a respective student exists, then check the password and return the resulting `PrincipalInfo` or ``None`` if no such student can be found. """ 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 PrincipalInfo(id=account.name, title=account.title, description=account.description) def principalInfo(self, id): """Get a principal identified by `id`. This one is required by IAuthenticatorPlugin. """ account = self.getAccount(id) if account is None: return None return PrincipalInfo(id=account.name, title=account.title, description=account.description) def getAccount(self, login): """Look up a student identified by `login`. Returns an account. Currently, we simply look up the key under which the student is stored in the portal. That means we hit if login name and name under which the student is stored match. Returns not a student but an account object adapted from any student found. If no such student exists, ``None`` is returned. """ site = grok.getSite() if site is None: return None studentscontainer = site.get('students', None) if studentscontainer is None: return None student = studentscontainer.get(login, None) if student is None: return None return IUserAccount(student) class PasswordChangeCredentialsPlugin(grok.GlobalUtility, SessionCredentialsPlugin): """A session credentials plugin that handles the case of a user changing his/her own password. When users change their own password they might find themselves logged out on next request. To avoid this, we support to use a 'change password' page a bit like a regular login page. That means, on each request we lookup the sent data for a login field called 'student_id' and a password. If both exist, this means someone sent new credentials. We then look for the old credentials stored in the user session. If the new credentials' login (the student_id) matches the old one's, we set the new credentials in session _but_ we return the old credentials for the authentication plugins to check as for the current request (and for the last time) the old credentials apply. No valid credentials are returned by this plugin if one of the follwing circumstances is true - the sent request is not a regular IHTTPRequest - the credentials to set do not match the old ones - no credentials are sent with the request - no credentials were set before (i.e. the user has no session with credentials set before) Therefore it is mandatory to put this plugin in the line of all credentials plugins _before_ other plugins, so that the regular credentials plugins can drop in as a 'fallback'. This plugin was designed for students to change their passwords, but might be used to allow password resets for other types of accounts as well. """ grok.provides(ICredentialsPlugin) grok.name('student_pw_change') loginpagename = 'login' loginfield = 'student_id' passwordfield = 'form.password' def extractCredentials(self, request): if not IHTTPRequest.providedBy(request): return None login = request.get(self.loginfield, None) password = request.get(self.passwordfield, None) if not login or not password: return None session = ISession(request) sessionData = session.get( 'zope.pluggableauth.browserplugins') old_credentials = sessionData.get('credentials', None) if old_credentials is None: # Password changes for already authenticated users only! return None if old_credentials.getLogin() != login: # Special treatment only for users that change their own pw. return None old_credentials = { 'login': old_credentials.getLogin(), 'password': old_credentials.getPassword()} # Set new credentials in session. These will be active on next request new_credentials = SessionCredentials(login, password) sessionData['credentials'] = new_credentials # Return old credentials for this one request only return old_credentials class StudentsAuthenticatorSetup(grok.GlobalUtility): """Register or unregister student authentication for a PAU. This piece is called when a new site is created. """ grok.implements(IAuthPluginUtility) def register(self, pau): plugins = list(pau.credentialsPlugins) # this plugin must come before the regular credentials plugins plugins.insert(0, 'student_pw_change') pau.credentialsPlugins = tuple(plugins) plugins = list(pau.authenticatorPlugins) plugins.append('students') pau.authenticatorPlugins = tuple(plugins) return pau def unregister(self, pau): plugins = [x for x in pau.authenticatorPlugins if x != 'students'] pau.authenticatorPlugins = tuple(plugins) return pau