[gen] More work on LDAP authentication.

This commit is contained in:
Gaetan Delannay 2013-09-09 15:54:06 +02:00
parent 79d89aca2b
commit e51308b277
2 changed files with 101 additions and 72 deletions

View file

@ -976,29 +976,19 @@ class ToolMixin(BaseMixin):
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Authentication-related methods # Authentication-related methods
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
def _encryptPassword(self, password): def identifyUser(self, alsoSpecial=False):
'''Returns the encrypted version of clear p_password.''' '''To identify a user means: get its login and password. There are
return self.acl_users._encryptPassword(password) several places to look for this information: http authentication,
cookie of credentials coming from the web form.
def getUser(self, authentify=False): If no user could be identified, and p_alsoSpecial is True, we will
'''Gets the current user. If p_authentify is True, in addition to nevertheless identify a "special user": "system", representing the
finding the logged user and returning it (=identification), we check system itself (running at startup or in batch mode) or "anon",
if found credentials are valid (=authentification).''' representing an anonymous user.'''
# 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).
tool = self.appy() tool = self.appy()
req = tool.request 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 login = password = None
isSpecial = False # a. Identify the user from http basic authentication.
# Ia. Identify the user from http basic authentication.
if getattr(req, '_auth', None): if getattr(req, '_auth', None):
# HTTP basic authentication credentials are present (used when # HTTP basic authentication credentials are present (used when
# connecting to the ZMI). Decode it. # connecting to the ZMI). Decode it.
@ -1009,45 +999,25 @@ class ToolMixin(BaseMixin):
login, password = base64.decodestring(creds).split(':', 1) login, password = base64.decodestring(creds).split(':', 1)
except Exception, e: except Exception, e:
pass pass
# Ib. Identify the user from the authentication cookie. # b. Identify the user from the authentication cookie.
if not login: if not login:
login, password = gutils.readCookie(req) login, password = gutils.readCookie(req)
# Ic. Identify the user from the authentication form. # c. Identify the user from the authentication form.
if not login: if not login:
login = req.get('__ac_name', None) login = req.get('__ac_name', None)
password = req.get('__ac_password', None) password = req.get('__ac_password', None)
# Stop the identification process here if we needed to authentify the # Stop identification here if we don't need to return a special user
# user: this user does not exist. if not alsoSpecial: return login, password
if not login and authentify: return # d. All the identification methods failed. So identify the user as
# Id. All the identification methods failed. So identify the user as
# "anon" or "system". # "anon" or "system".
if not login and not authentify: if not login:
# If we have a real request object, it is the anonymous user. # If we have a real request object, it is the anonymous user.
login = (req.__class__.__name__ == 'Object') and 'system' or 'anon' login = (req.__class__.__name__ == 'Object') and 'system' or 'anon'
isSpecial = True return login, password
# 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
def _ldapAuthenticate(self, login, password): def getLdapUser(self, login, password):
'''Performs a LDAP-based authentication. Returns True if authentication '''Returns a local User instance corresponding to a LDAP user if p_login
succeeds.''' and p_password correspong to a valid LDAP user.'''
# Check if LDAP is configured. # Check if LDAP is configured.
cfg = self.getProductConfig(True).ldap cfg = self.getProductConfig(True).ldap
if not cfg: return if not cfg: return
@ -1069,8 +1039,8 @@ class ToolMixin(BaseMixin):
# The password is correct. We can create/update our local user # The password is correct. We can create/update our local user
# corresponding to this LDAP user. # corresponding to this LDAP user.
userParams = cfg.getUserParams(ldapData) userParams = cfg.getUserParams(ldapData)
user = self.search1('User', noSecurity=True, login=login) tool = self.appy()
tool = self user = tool.search1('User', noSecurity=True, login=login)
if user: if user:
# Update the user with fresh info about him from the LDAP # Update the user with fresh info about him from the LDAP
for name, value in userParams.iteritems(): for name, value in userParams.iteritems():
@ -1081,6 +1051,47 @@ class ToolMixin(BaseMixin):
user = tool.create('users', login=login, source='ldap',**userParams) user = tool.create('users', login=login, source='ldap',**userParams)
return user 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): def performLogin(self):
'''Logs the user in.''' '''Logs the user in.'''
rq = self.REQUEST rq = self.REQUEST
@ -1092,7 +1103,8 @@ class ToolMixin(BaseMixin):
return self.goto(urlBack, msg) return self.goto(urlBack, msg)
# Authenticate the user. # Authenticate the user.
login = rq.get('__ac_name', None) 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') msg = self.translate('login_ok')
logMsg = 'User "%s" logged in.' % login logMsg = 'User "%s" logged in.' % login
else: else:
@ -1107,7 +1119,7 @@ class ToolMixin(BaseMixin):
userId = self.getUser().login userId = self.getUser().login
# Perform the logout in acl_users # Perform the logout in acl_users
rq.RESPONSE.expireCookie('_appy_', path='/') rq.RESPONSE.expireCookie('_appy_', path='/')
# Invalidate session. # Invalidate the user session.
try: try:
sdm = self.session_data_manager sdm = self.session_data_manager
except AttributeError, ae: except AttributeError, ae:

View file

@ -35,8 +35,9 @@ class UserWrapper(AbstractWrapper):
# can potentially be changed. # can potentially be changed.
if not self.login or (login != self.login): if not self.login or (login != self.login):
# A new p_login is requested. Check if it is valid and free. # 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". # Some logins are not allowed.
if login == 'admin': return self.translate('login_reserved') if login in ('admin', 'anon', 'system'):
return self.translate('login_reserved')
# Check that no user or group already uses this login. # Check that no user or group already uses this login.
if self.count('User', noSecurity=True, login=login) or \ if self.count('User', noSecurity=True, login=login) or \
self.count('Group', noSecurity=True, login=login): 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. # also own a User instance) wants to edit information about himself.
if self.user.login == self.login: return 'edit' 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): def setPassword(self, newPassword=None):
'''Sets a p_newPassword for self. If p_newPassword is not given, we '''Sets a p_newPassword for self. If p_newPassword is not given, we
generate one. This method returns the generated password (or simply generate one. This method returns the generated password (or simply
@ -70,7 +75,7 @@ class UserWrapper(AbstractWrapper):
login = self.login login = self.login
zopeUser = self.getZopeUser() zopeUser = self.getZopeUser()
tool = self.tool.o tool = self.tool.o
zopeUser.__ = tool._encryptPassword(newPassword) zopeUser.__ = self.encryptPassword(newPassword)
if self.user.login == login: if self.user.login == login:
# The user for which we change the password is the currently logged # The user for which we change the password is the currently logged
# user. So update the authentication cookie, too. # user. So update the authentication cookie, too.
@ -91,7 +96,7 @@ class UserWrapper(AbstractWrapper):
self.login = newLogin self.login = newLogin
# Update the corresponding Zope-level user # Update the corresponding Zope-level user
aclUsers = self.o.acl_users aclUsers = self.o.acl_users
zopeUser = aclUsers.getUser(oldLogin) zopeUser = aclUsers.data[oldLogin]
zopeUser.name = newLogin zopeUser.name = newLogin
del aclUsers.data[oldLogin] del aclUsers.data[oldLogin]
aclUsers.data[newLogin] = zopeUser aclUsers.data[newLogin] = zopeUser
@ -150,20 +155,25 @@ class UserWrapper(AbstractWrapper):
self.roles = roles self.roles = roles
def onEdit(self, created): def onEdit(self, created):
'''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.updateTitle()
self.ensureAdminIsManager() self.ensureAdminIsManager()
aclUsers = self.o.acl_users
login = self.login
if created: if created:
# Create the corresponding Zope user # Create the corresponding Zope user.
aclUsers._doAddUser(login, self.password1, self.roles, ()) from AccessControl.User import User as ZopeUser
zopeUser = aclUsers.getUser(login) 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 # Remove our own password copies
self.password1 = self.password2 = '' 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: else:
# Update the login itself if the user has changed it. # Update the login itself if the user has changed it.
oldLogin = self.o._oldLogin oldLogin = self.o._oldLogin
@ -188,16 +198,20 @@ class UserWrapper(AbstractWrapper):
return self._callCustom('onEdit', created) return self._callCustom('onEdit', created)
def mayEdit(self): 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.o.id in ('system', 'anon'): return
if self.source != 'zodb': return
# Call custom "mayEdit" when present. # Call custom "mayEdit" when present.
custom = self._getCustomMethod('mayEdit') custom = self._getCustomMethod('mayEdit')
if custom: return self._callCustom('mayEdit') if custom: return self._callCustom('mayEdit')
return True return True
def mayDelete(self): 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.o.id in ('system', 'anon', 'admin'): return
if self.source != 'zodb': return
# Call custom "mayDelete" when present. # Call custom "mayDelete" when present.
custom = self._getCustomMethod('mayDelete') custom = self._getCustomMethod('mayDelete')
if custom: return self._callCustom('mayDelete') if custom: return self._callCustom('mayDelete')
@ -205,11 +219,14 @@ class UserWrapper(AbstractWrapper):
def getZopeUser(self): def getZopeUser(self):
'''Gets the Zope user corresponding to this user.''' '''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): def onDelete(self):
'''Before deleting myself, I must delete the corresponding Zope user.''' '''Before deleting myself, I must delete the corresponding Zope user
self.o.acl_users._doDelUsers([self.login]) (for local users only).'''
if self.source == 'zodb': del self.o.acl_users.data[self.login]
self.log('User "%s" deleted.' % self.login) self.log('User "%s" deleted.' % self.login)
# Call a custom "onDelete" if any. # Call a custom "onDelete" if any.
return self._callCustom('onDelete') return self._callCustom('onDelete')