From e344ff51e2eb577022c9a0b80698fc5a3661f35d Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Mon, 9 Sep 2013 23:14:50 +0200 Subject: [PATCH] [gen] LDAP bugfixes. --- bin/checkldap.py | 40 +++++++++++++------------------------ gen/__init__.py | 6 ++++-- gen/installer.py | 32 +---------------------------- gen/mixins/ToolMixin.py | 40 ++++++++++++++++++++++++++++++------- gen/templates/config.pyt | 5 ----- gen/wrappers/ToolWrapper.py | 3 +-- gen/wrappers/UserWrapper.py | 16 ++++++++------- shared/ldap_connector.py | 5 ++++- 8 files changed, 66 insertions(+), 81 deletions(-) diff --git a/bin/checkldap.py b/bin/checkldap.py index 4254df1..a517a38 100644 --- a/bin/checkldap.py +++ b/bin/checkldap.py @@ -1,9 +1,10 @@ '''This script allows to check a LDAP connection.''' -import sys, ldap +import sys +from appy.shared.ldap_connector import LdapConnector # ------------------------------------------------------------------------------ class LdapTester: - '''Usage: python checkldap.py ldapUri login password base attrs filter + '''Usage: python checkldap.py ldapUri login password base attrs filter scope ldapUri is, for example, "ldap://127.0.0.1:389" login is the login user DN, ie: "cn=gdy,o=geezteem" @@ -27,34 +28,21 @@ class LdapTester: self.attrs = self.attrs.split(',') self.tentatives = 5 self.timeout = 5 - self.attrList = ['cn'] + self.attributes = ['cn'] self.ssl = False def test(self): # Connect the the LDAP - print('Creating server object for server %s...' % self.uri) - server = ldap.initialize(self.uri) - print('Done. Login with %s...' % self.login) - server.simple_bind(self.login, self.password) - if self.ssl: - server.start_tls_s() - try: - for i in range(self.tentatives): - try: - print('Done. Performing a simple query on %s...'% self.base) - res = server.search_st(self.base, - getattr(ldap, 'SCOPE_%s' % self.scope), - filterstr=self.filter, - attrlist=self.attrs, timeout=5) - print('Got %d entries' % len(res)) - break - except ldap.TIMEOUT: - print('Got timeout.') - except ldap.LDAPError, le: - print('%s %s' % (le.__class__.__name__, str(le))) + print('Connecting to... %s' % self.uri) + connector = LdapConnector(self.uri) + success, msg = connector.connect(self.login, self.password) + if not success: return + # Perform the query. + print ('Querying %s...' % self.base) + res = connector.search(self.base, self.scope, self.filter, + self.attributes) + print('Got %d results' % len(res)) # ------------------------------------------------------------------------------ -if __name__ == '__main__': - LdapTester().test() - +if __name__ == '__main__': LdapTester().test() # ------------------------------------------------------------------------------ diff --git a/gen/__init__.py b/gen/__init__.py index 0a1780f..b519f3e 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -412,7 +412,7 @@ class LdapConfig: def getUserAttributes(self): '''Gets the attributes we want to get from the LDAP for characterizing a user.''' - res = [self.loginAttribute] + res = [] for name in self.ldapAttributes.iterkeys(): if getattr(self, name): res.append(getattr(self, name)) @@ -429,7 +429,9 @@ class LdapConfig: ldapName = getattr(self, name) if not ldapName: continue if ldapData.has_key(ldapName) and ldapData[ldapName]: - res[appyName] = ldapData[ldapName] + value = ldapData[ldapName] + if isinstance(value, list): value = value[0] + res[appyName] = value return res # ------------------------------------------------------------------------------ diff --git a/gen/installer.py b/gen/installer.py index f61c02a..8737cdc 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -2,7 +2,7 @@ Zope product.''' # ------------------------------------------------------------------------------ -import os, os.path, time +import os, os.path import appy import appy.version import appy.gen as gen @@ -16,24 +16,6 @@ from appy.shared.data import languages homePage = '' -# Stuff for tracking user activity --------------------------------------------- -loggedUsers = {} -originalTraverse = None -doNotTrack = ('.jpg','.gif','.png','.js','.css') - -def traverseWrapper(self, path, response=None, validated_hook=None): - '''This function is called every time a users gets a URL, this is used for - tracking user activity. self is a BaseRequest''' - res = originalTraverse(self, path, response, validated_hook) - if os.path.splitext(path)[-1].lower() not in doNotTrack: - # Do nothing when the user gets non-pages. - userId, dummy = gutils.readCookie(self) - if userId: - loggedUsers[userId] = time.time() - # "Touch" the SESSION object. Else, expiration won't occur. - session = self.SESSION - return res - def onDelSession(sessionObject, container): '''This function is called when a session expires.''' rq = container.REQUEST @@ -284,17 +266,6 @@ class ZopeInstaller: else: sessionData.setDelNotificationTarget(None) - def enableUserTracking(self): - '''Enables the machinery allowing to know who is currently logged in. - Information about logged users will be stored in RAM, in the variable - named loggedUsers defined above.''' - global originalTraverse - if not originalTraverse: - # User tracking is not enabled yet. Do it now. - BaseRequest = self.config.BaseRequest - originalTraverse = BaseRequest.traverse - BaseRequest.traverse = traverseWrapper - def installZopeClasses(self): '''Zope-level class registration.''' for klass in self.classes: @@ -358,7 +329,6 @@ class ZopeInstaller: self.installRoles() self.installAppyTypes() self.installZopeClasses() - self.enableUserTracking() self.configureSessions() self.installBaseObjects() # The following line cleans and rebuilds the catalog entirely. diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index 099d0de..ef0694c 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -1038,17 +1038,20 @@ class ToolMixin(BaseMixin): if not success: return # The password is correct. We can create/update our local user # corresponding to this LDAP user. - userParams = cfg.getUserParams(ldapData) + userParams = cfg.getUserParams(ldapData[0][1]) 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(): setattr(user, name, value) + # Update user password + user.setPassword(password, log=False) user.reindex() else: # Create the user - user = tool.create('users', login=login, source='ldap',**userParams) + user = tool.create('users', noSecurity=True, login=login, + password1=password, source='ldap', **userParams) return user def getUser(self, authentify=False, source='zodb'): @@ -1058,7 +1061,9 @@ class ToolMixin(BaseMixin): 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).''' + performed on a LDAP (if a LDAP configuration is found). If p_source + is "any", authentication is performed on the local User object, be it + really local or a copy of a LDAP user.''' tool = self.appy() req = tool.request # Try first to return the user that can be cached on the request. In @@ -1073,9 +1078,14 @@ class ToolMixin(BaseMixin): if authentify and not login: return # Now, get the User instance. if source == 'zodb': + # Get the User object, but only if it is a true local user. user = tool.search1('User', noSecurity=True, login=login) + if user and (user.source != 'zodb'): user = None # Not a local one. elif source == 'ldap': user = self.getLdapUser(login, password) + elif source == 'any': + # Get the user object, be it really local or a copy of a LDAP user. + user = tool.search1('User', noSecurity=True, login=login) if not user: return # Authentify the user if required. if authentify: @@ -1131,22 +1141,35 @@ class ToolMixin(BaseMixin): session.invalidate() self.log('User "%s" has been logged out.' % userId) # Remove user from variable "loggedUsers" - from appy.gen.installer import loggedUsers - if loggedUsers.has_key(userId): del loggedUsers[userId] + if self.loggedUsers.has_key(userId): del self.loggedUsers[userId] return self.goto(self.getApp().absolute_url()) + # This dict stores, for every logged user, the date/time of its last access + loggedUsers = {} + forgetAccessExtensions = ('.jpg', '.gif', '.png', '.js', '.css') + def rememberAccess(self, id, user): + '''Every time there is a hit on the server, this method is called in + order to update global dict loggedUsers (see above).''' + if not id: return + if os.path.splitext(id)[-1].lower() in self.forgetAccessExtensions: + return + self.loggedUsers[user.login] = time.time() + # "Touch" the SESSION object. Else, expiration won't occur. + session = self.REQUEST.SESSION + def validate(self, request, auth='', roles=_noroles): '''This method performs authentication and authorization. It is used as a replacement for Zope's AccessControl.User.BasicUserFolder.validate, that allows to manage cookie-based authentication.''' v = request['PUBLISHED'] # The published object + tool = self.getParentNode().config # v is the object (value) we're validating access to # n is the name used to access the object # a is the object the object was accessed through # c is the physical container of the object a, c, n, v = self._getobcontext(v, request) # Identify and authentify the user - user = self.getParentNode().config.getUser(authentify=True) + user = tool.getUser(authentify=True, source='any') if not user: # Login and/or password incorrect. Try to authorize and return the # anonymous user. @@ -1156,7 +1179,9 @@ class ToolMixin(BaseMixin): return else: # We found a user and his password was correct. Try to authorize him - # against the published object. + # against the published object. By the way, remember its last access + # to this system. + tool.rememberAccess(a.getId(), user) user = user.getZopeUser() if self.authorize(user, a, c, n, v, roles): return user.__of__(self) @@ -1237,6 +1262,7 @@ class ToolMixin(BaseMixin): if ',' in contentType: return () return [f.__dict__ for f in self.getAllAppyTypes(contentType) \ if (f.type == 'Pod') and (f.show == 'result')] + def formatDate(self, aDate, withHour=True): '''Returns aDate formatted as specified by tool.dateFormat. If p_withHour is True, hour is appended, with a format specified diff --git a/gen/templates/config.pyt b/gen/templates/config.pyt index 795c346..950e267 100644 --- a/gen/templates/config.pyt +++ b/gen/templates/config.pyt @@ -8,14 +8,9 @@ import wrappers # The following imports are here for allowing mixin classes to access those # elements without being statically dependent on Zope packages. from persistent.list import PersistentList -from zExceptions import BadRequest -from ZPublisher.HTTPRequest import BaseRequest from OFS.Image import File from ZPublisher.HTTPRequest import FileUpload -from AccessControl import getSecurityManager from DateTime import DateTime -from Products.ExternalMethod.ExternalMethod import ExternalMethod -from Products.Transience.Transience import TransientObjectContainer import appy.gen import logging logger = logging.getLogger('') diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index 4be1719..26ad711 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -4,7 +4,6 @@ import appy from appy.gen.mail import sendMail from appy.shared.utils import executeCommand from appy.gen.wrappers import AbstractWrapper -from appy.gen.installer import loggedUsers from appy.px import Px # ------------------------------------------------------------------------------ @@ -668,7 +667,7 @@ class ToolWrapper(AbstractWrapper): '%s' % \ self.translate('last_user_access') rows = [] - for userId, lastAccess in loggedUsers.items(): + for userId, lastAccess in self.o.loggedUsers.items(): user = self.search1('User', noSecurity=True, login=userId) if not user: continue # Could have been deleted in the meanwhile fmt = '%s (%s)' % (self.dateFormat, self.hourFormat) diff --git a/gen/wrappers/UserWrapper.py b/gen/wrappers/UserWrapper.py index 713b250..546e4ca 100644 --- a/gen/wrappers/UserWrapper.py +++ b/gen/wrappers/UserWrapper.py @@ -63,7 +63,7 @@ class UserWrapper(AbstractWrapper): '''Returns p_clearPassword, encrypted.''' return self.o.getTool().acl_users._encryptPassword(clearPassword) - def setPassword(self, newPassword=None): + def setPassword(self, newPassword=None, log=True): '''Sets a p_newPassword for self. If p_newPassword is not given, we generate one. This method returns the generated password (or simply p_newPassword if no generation occurred).''' @@ -76,12 +76,13 @@ class UserWrapper(AbstractWrapper): zopeUser = self.getZopeUser() tool = self.tool.o zopeUser.__ = self.encryptPassword(newPassword) - if self.user.login == login: + if self.user and (self.user.login == login): # The user for which we change the password is the currently logged # user. So update the authentication cookie, too. gutils.writeCookie(login, newPassword, self.request) - self.log('Password %s by "%s" for "%s".' % \ - (msgPart, self.user.login, login)) + if log: + self.log('Password %s by "%s" for "%s".' % \ + (msgPart, self.user.login, login)) return newPassword def checkPassword(self, clearPassword): @@ -191,10 +192,11 @@ class UserWrapper(AbstractWrapper): # "self" must be owned by its Zope user. if 'Owner' not in self.o.get_local_roles_for_userid(login): self.o.manage_addLocalRoles(login, ('Owner',)) - # If the user was created by an Anonymous, Anonymous can't stay Owner - # of the object. + # If the user was created by anon or system, remove this local role. if 'anon' in self.o.__ac_local_roles__: del self.o.__ac_local_roles__['anon'] + if 'system' in self.o.__ac_local_roles__: + del self.o.__ac_local_roles__['system'] return self._callCustom('onEdit', created) def mayEdit(self): @@ -311,7 +313,7 @@ class UserWrapper(AbstractWrapper): userLogins = self.getLogins() for login, roles in localRoles.iteritems(): # Ignore logins not corresponding to this user. - if login not in logins: continue + if login not in userLogins: continue for role in roles: if role in allowedRoles: return True # ------------------------------------------------------------------------------ diff --git a/shared/ldap_connector.py b/shared/ldap_connector.py index adcd218..e7c9894 100644 --- a/shared/ldap_connector.py +++ b/shared/ldap_connector.py @@ -26,7 +26,10 @@ class LdapConnector: def log(self, message, type='info'): '''Logs via a Appy tool if available.''' - if self.tool: self.tool.log(message, type=type) + if self.tool: + self.tool.log(message, type=type) + else: + print(message) def connect(self, login, password): '''Connects to the LDAP server using p_login and p_password as