diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index fb3e4b8..099d0de 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -976,29 +976,19 @@ class ToolMixin(BaseMixin): # -------------------------------------------------------------------------- # Authentication-related methods # -------------------------------------------------------------------------- - def _encryptPassword(self, password): - '''Returns the encrypted version of clear p_password.''' - return self.acl_users._encryptPassword(password) + def identifyUser(self, alsoSpecial=False): + '''To identify a user means: get its login and password. There are + several places to look for this information: http authentication, + cookie of credentials coming from the web form. - def getUser(self, authentify=False): - '''Gets the current user. If p_authentify is True, in addition to - finding the logged user and returning it (=identification), we check - if found credentials are valid (=authentification).''' - # I. Identify the user (=find its login and password). If identification - # fails, if we don't need to authentify the user (p_authentify is - # False), we consider that the current user is one of those special - # users: anon (corresponds to an anonymous user) or system (the - # technical user representing the system itself, running at startup or - # in batch mode). + If no user could be identified, and p_alsoSpecial is True, we will + nevertheless identify a "special user": "system", representing the + system itself (running at startup or in batch mode) or "anon", + representing an anonymous user.''' tool = self.appy() req = tool.request - # Try first to return the user that can be cached on the request. In - # this case, we suppose authentification has previously been done, and - # we just return the cached user. - if hasattr(req, 'user'): return req.user login = password = None - isSpecial = False - # Ia. Identify the user from http basic authentication. + # a. Identify the user from http basic authentication. if getattr(req, '_auth', None): # HTTP basic authentication credentials are present (used when # connecting to the ZMI). Decode it. @@ -1009,45 +999,25 @@ class ToolMixin(BaseMixin): login, password = base64.decodestring(creds).split(':', 1) except Exception, e: pass - # Ib. Identify the user from the authentication cookie. + # b. Identify the user from the authentication cookie. if not login: login, password = gutils.readCookie(req) - # Ic. Identify the user from the authentication form. + # c. Identify the user from the authentication form. if not login: login = req.get('__ac_name', None) password = req.get('__ac_password', None) - # Stop the identification process here if we needed to authentify the - # user: this user does not exist. - if not login and authentify: return - # Id. All the identification methods failed. So identify the user as + # Stop identification here if we don't need to return a special user + if not alsoSpecial: return login, password + # d. All the identification methods failed. So identify the user as # "anon" or "system". - if not login and not authentify: + if not login: # If we have a real request object, it is the anonymous user. login = (req.__class__.__name__ == 'Object') and 'system' or 'anon' - isSpecial = True - # Now, get the User instance from a query in the catalog. - user = tool.search1('User', noSecurity=True, login=login) - # It is possible that we find no user here: it happens before users - # "anon" and "system" are created, at first startup. - if not user: return - # Authentify the user if required - if authentify and not isSpecial: - if not user.checkPassword(password): - # Disable the authentication cookie. - req.RESPONSE.expireCookie('_appy_', path='/') - return - # Create an authentication cookie for this user. - gutils.writeCookie(login, password, req) - # Cache the user and some precomputed values, for performance. - req.user = user - req.userRoles = user.getRoles() - req.userLogins = user.getLogins() - req.zopeUser = user.getZopeUser() - return user + return login, password - def _ldapAuthenticate(self, login, password): - '''Performs a LDAP-based authentication. Returns True if authentication - succeeds.''' + def getLdapUser(self, login, password): + '''Returns a local User instance corresponding to a LDAP user if p_login + and p_password correspong to a valid LDAP user.''' # Check if LDAP is configured. cfg = self.getProductConfig(True).ldap if not cfg: return @@ -1069,8 +1039,8 @@ class ToolMixin(BaseMixin): # The password is correct. We can create/update our local user # corresponding to this LDAP user. userParams = cfg.getUserParams(ldapData) - user = self.search1('User', noSecurity=True, login=login) - tool = self + tool = self.appy() + user = tool.search1('User', noSecurity=True, login=login) if user: # Update the user with fresh info about him from the LDAP for name, value in userParams.iteritems(): @@ -1081,6 +1051,47 @@ class ToolMixin(BaseMixin): user = tool.create('users', login=login, source='ldap',**userParams) return user + def getUser(self, authentify=False, source='zodb'): + '''Gets the current user. If p_authentify is True, in addition to + finding the logged user and returning it (=identification), we check + if found credentials are valid (=authentification). + + If p_authentify is True and p_source is "zodb", authentication is + performed locally. Else (p_source is "ldap"), authentication is + performed on a LDAP (if a LDAP configuration is found).''' + tool = self.appy() + req = tool.request + # Try first to return the user that can be cached on the request. In + # this case, we suppose authentication has previously been done, and we + # just return the cached user. + if hasattr(req, 'user'): return req.user + # Identify the user (=find its login and password). If we don't need + # to authentify the user, we ask to identify a user or, if impossible, + # a special user. + login, password = self.identifyUser(alsoSpecial=not authentify) + # Stop here if no user was found and authentication was required. + if authentify and not login: return + # Now, get the User instance. + if source == 'zodb': + user = tool.search1('User', noSecurity=True, login=login) + elif source == 'ldap': + user = self.getLdapUser(login, password) + if not user: return + # Authentify the user if required. + if authentify: + if not user.checkPassword(password): + # Disable the authentication cookie. + req.RESPONSE.expireCookie('_appy_', path='/') + return + # Create an authentication cookie for this user. + gutils.writeCookie(login, password, req) + # Cache the user and some precomputed values, for performance. + req.user = user + req.userRoles = user.getRoles() + req.userLogins = user.getLogins() + req.zopeUser = user.getZopeUser() + return user + def performLogin(self): '''Logs the user in.''' rq = self.REQUEST @@ -1092,7 +1103,8 @@ class ToolMixin(BaseMixin): return self.goto(urlBack, msg) # Authenticate the user. login = rq.get('__ac_name', None) - if self.getUser(authentify=True): + if self.getUser(authentify=True) or \ + self.getUser(authentify=True, source='ldap'): msg = self.translate('login_ok') logMsg = 'User "%s" logged in.' % login else: @@ -1107,7 +1119,7 @@ class ToolMixin(BaseMixin): userId = self.getUser().login # Perform the logout in acl_users rq.RESPONSE.expireCookie('_appy_', path='/') - # Invalidate session. + # Invalidate the user session. try: sdm = self.session_data_manager except AttributeError, ae: diff --git a/gen/wrappers/UserWrapper.py b/gen/wrappers/UserWrapper.py index 1bdbfa3..713b250 100644 --- a/gen/wrappers/UserWrapper.py +++ b/gen/wrappers/UserWrapper.py @@ -35,8 +35,9 @@ class UserWrapper(AbstractWrapper): # can potentially be changed. if not self.login or (login != self.login): # A new p_login is requested. Check if it is valid and free. - # Firstly, the login can't be the id of the whole site or "admin". - if login == 'admin': return self.translate('login_reserved') + # Some logins are not allowed. + if login in ('admin', 'anon', 'system'): + return self.translate('login_reserved') # Check that no user or group already uses this login. if self.count('User', noSecurity=True, login=login) or \ self.count('Group', noSecurity=True, login=login): @@ -58,6 +59,10 @@ class UserWrapper(AbstractWrapper): # also own a User instance) wants to edit information about himself. if self.user.login == self.login: return 'edit' + def encryptPassword(self, clearPassword): + '''Returns p_clearPassword, encrypted.''' + return self.o.getTool().acl_users._encryptPassword(clearPassword) + def setPassword(self, newPassword=None): '''Sets a p_newPassword for self. If p_newPassword is not given, we generate one. This method returns the generated password (or simply @@ -70,7 +75,7 @@ class UserWrapper(AbstractWrapper): login = self.login zopeUser = self.getZopeUser() tool = self.tool.o - zopeUser.__ = tool._encryptPassword(newPassword) + zopeUser.__ = self.encryptPassword(newPassword) if self.user.login == login: # The user for which we change the password is the currently logged # user. So update the authentication cookie, too. @@ -91,7 +96,7 @@ class UserWrapper(AbstractWrapper): self.login = newLogin # Update the corresponding Zope-level user aclUsers = self.o.acl_users - zopeUser = aclUsers.getUser(oldLogin) + zopeUser = aclUsers.data[oldLogin] zopeUser.name = newLogin del aclUsers.data[oldLogin] aclUsers.data[newLogin] = zopeUser @@ -150,20 +155,25 @@ class UserWrapper(AbstractWrapper): self.roles = roles def onEdit(self, created): - self.updateTitle() - self.ensureAdminIsManager() - aclUsers = self.o.acl_users + '''Triggered when a User is created or updated.''' login = self.login + # Is it a local User or a LDAP User? + isLocal = self.source == 'zodb' + # Ensure correctness of some infos about this user. + if isLocal: + self.updateTitle() + self.ensureAdminIsManager() if created: - # Create the corresponding Zope user - aclUsers._doAddUser(login, self.password1, self.roles, ()) - zopeUser = aclUsers.getUser(login) + # Create the corresponding Zope user. + from AccessControl.User import User as ZopeUser + password = self.encryptPassword(self.password1) + zopeUser = ZopeUser(login, password, self.roles, ()) + # Add it in acl_users if it is a local user. + if isLocal: self.o.acl_users.data[login] = zopeUser + # Add it in self.o._zopeUser if it is a LDAP user + else: self.o._zopeUser = zopeUser # Remove our own password copies self.password1 = self.password2 = '' - from persistent.mapping import PersistentMapping - # The following dict will store, for every group, global roles - # granted to it. - zopeUser.groups = PersistentMapping() else: # Update the login itself if the user has changed it. oldLogin = self.o._oldLogin @@ -188,16 +198,20 @@ class UserWrapper(AbstractWrapper): return self._callCustom('onEdit', created) def mayEdit(self): - '''No one can edit users "system" and "anon".''' + '''No one can edit users "system" and "anon"; no one can edit non-zodb + users.''' if self.o.id in ('system', 'anon'): return + if self.source != 'zodb': return # Call custom "mayEdit" when present. custom = self._getCustomMethod('mayEdit') if custom: return self._callCustom('mayEdit') return True def mayDelete(self): - '''No one can delete users "system", "anon" and "admin".''' + '''No one can delete users "system", "anon" and "admin"; no one can + delete non-zodb users.''' if self.o.id in ('system', 'anon', 'admin'): return + if self.source != 'zodb': return # Call custom "mayDelete" when present. custom = self._getCustomMethod('mayDelete') if custom: return self._callCustom('mayDelete') @@ -205,11 +219,14 @@ class UserWrapper(AbstractWrapper): def getZopeUser(self): '''Gets the Zope user corresponding to this user.''' - return self.o.acl_users.getUser(self.login) + if self.source == 'zodb': + return self.o.acl_users.data.get(self.login, None) + return self.o._zopeUser def onDelete(self): - '''Before deleting myself, I must delete the corresponding Zope user.''' - self.o.acl_users._doDelUsers([self.login]) + '''Before deleting myself, I must delete the corresponding Zope user + (for local users only).''' + if self.source == 'zodb': del self.o.acl_users.data[self.login] self.log('User "%s" deleted.' % self.login) # Call a custom "onDelete" if any. return self._callCustom('onDelete')