""" HTTPS Authentication Source for exUserFolder This class only authenticates users against an https service It stores roles in the prop source This plugin requires that ssl support be compiled into the python interpreter $Header: /cvsroot/exuserfolder/exUserFolder/httpsAuthSource/httpsAuthSource.py,v 1.6 2003/04/09 16:44:31 mrenoch Exp $ """ import string, re import httplib, urllib, urlparse import Acquisition from Globals import HTMLFile, MessageDialog, InitializeClass from OFS.Folder import Folder from Products.exUserFolder.exUserFolder import exUserFolder from Products.exUserFolder.Plugins import PluginRegister from AccessControl import ClassSecurityInfo from time import time from zLOG import LOG, ERROR, DEBUG def manage_addhttpsAuthSource(self, REQUEST): """ Add an https Auth Source """ obj = httpsAuthSource(REQUEST) self._setObject('httpsAuthSource', obj, None, None, 0) obj = getattr(self, 'httpsAuthSource') self.currentAuthSource = obj return '' manage_addhttpsAuthSourceForm=HTMLFile('manage_addhttpsAuthSourceForm', globals()) manage_edithttpsAuthSourceForm=HTMLFile('manage_edithttpsAuthSourceForm', globals()) class httpsAuthSource(Folder): """ Authenticate Users against an HTTPS service """ meta_type= 'Authorisation Source' id = 'httpsAuthSource' title= 'HTTPS Authentication' icon = 'misc_/exUserFolder/exUserFolderPlugin.gif' ROLES_KEY = '_roles' manage_editForm = manage_edithttpsAuthSourceForm manage_properties=HTMLFile('properties', globals()) manage_tabs=Acquisition.Acquired # Cache successful auths so we don't make N network requests per page _v_authenticCache = {} # Cache unauthorized users since this acl folder needs to fail before checking higher ones _v_unAuthenticCache = {} CACHE_TIMEOUT = 60 # how long we store auth in the cache. (in seconds) UNAUTH_CACHE_MAXSIZE = 50 # limit the size that the unauthenticated cache can grow def __init__(self, REQUEST): self._setProps(REQUEST) def manage_editAuthSource(self, REQUEST): """ Handle output of manage_main""" self._setProps(REQUEST) self.clearAuthCache() if REQUEST is not None: return self.MessageDialog(self, REQUEST=REQUEST, title = 'Edited', message = "Properties for %s changed." % self.id, action = 'manage_editAuthSourceForm') def _setProps(self,REQUEST): self.defaultRole = REQUEST.get('defaultRole', 'Member') self.serviceUrl = REQUEST['serviceUrl'] self.userNameParam = REQUEST['userNameParam'] self.passwdParam = REQUEST['passwdParam'] self.authResponse = REQUEST['authResponse'] self.authResponsePattern = re.compile(self.authResponse) # # Don't allow for creation - this happens automatically upon first login # def createUser(self, username, password, roles): pass # we don't store the password - just pass it in the remoteAuthentication def cryptPassword(self, username, password): pass def deleteUsers(self, userids): """delete user from the prop source""" self.currentPropSource.deleteUsers(userids) def updateUser(self, username, password, roles): """Update a user's roles Passwords are managed on the other end, not here""" self.currentPropSource.setUserProperty(username=username, key=self.ROLES_KEY, value=roles) def listUserNames(self): """Return a list of usernames""" return self.currentPropSource.listUsers() # Return a list of user dictionaries the same as listOnUser def listUsers(self): pass # Return one user matching the username # Should be a dictionary; # {'username':username, 'password':cryptedPassword, 'roles':list_of_roles} def listOneUser(self, username): roles=[] # Attempt to aq the roles from the parent, this code # works inside a cmfsite but is not general case try: portal = self.portal_url.getPortalObject() acl_users = portal.aq_parent.acl_users roles += acl_users.getUser(username).getRolesInContext(portal) except: pass # We store site specific roles in the prop source - only tested with zodbBTreeProps source xufRoles = self.currentPropSource.getUserProperty(username=username, key=self.ROLES_KEY, default=[]) # getUserProperty returns None if user has no props if xufRoles: roles += xufRoles # this happens when listOneUser is called before authenticate (?) if not roles: roles = ['Anonymous'] # LOG(self.id, DEBUG, username) # LOG(self.id, DEBUG, roles) return [{'username':username, 'password':'*****', 'roles':roles}] def authenticate(self, username, passwd): """Authenticate a username/password combination against the HTTPS service""" ## Emergency override - uncomment this if you get locked out of your site ## return 1 # first check the authorization cache to minimize network traffic if self.isAuthenticCached(username, passwd): return 1 elif self.isUnAuthenticCached(username, passwd): return 0 auth = 0 # LOG(self.id, ERROR, "%s %s" % (username, passwd)) params = urllib.urlencode({self.userNameParam: username, self.passwdParam: passwd}) # by passing in params, this POSTs response = urllib.urlopen(self.serviceUrl, params) auth = self._parseResponse(response.read()) # Everyone gets 'Authenticated' and the defaultRole # This also insures that user ends up in prop list upon first login if auth and not self.currentPropSource.getUserProperty(key=self.ROLES_KEY, username=username, default=None): roles = ['Authenticated', self.defaultRole] self.currentPropSource.setUserProperty(username=username, key=self.ROLES_KEY, value=roles) # store auth results in cache if auth: self.cacheAuth(username, passwd) else: self.cacheUnAuth(username, passwd) return auth # tell xuf to use our authenticate method remoteAuthMethod = authenticate def _parseResponse(self, response): """ Try to match the expected authorization regex against the response If its found, they're in""" # LOG(self.id, DEBUG, response) retVal = 0 try: if self.authResponsePattern.match(response): retVal = 1 else: retVal = 0 except Exception, e: ERROR("%s: Could not parse the response. (response=%s, auth pattern=%s)" % (e, response, self.authResponse)) retVal = 0 return retVal # # cacheing methods # def isAuthenticCached(self, username, passwd): key = (username, passwd) now = time() if (self._v_authenticCache.has_key(key) and (now - self._v_authenticCache[key]) < self.CACHE_TIMEOUT): # LOG(self.id, ERROR, "Cached authentic user") return 1 else: return 0 def isUnAuthenticCached(self, username, passwd): key = (username, passwd) now = time() if (self._v_unAuthenticCache.has_key(key) and (now - self._v_unAuthenticCache[key]) < self.CACHE_TIMEOUT): # LOG(self.id, ERROR, "Cached UN-Authentic user") return 1 else: return 0 def cacheAuth(self, username, passwd): """Store successful auth attempts""" key = (username, passwd) timestamp = time() self._v_authenticCache[key] = timestamp def cacheUnAuth(self, username, passwd): """Store failed auth attempts""" # don't let the unauth cache grow unbounded if len(self._v_unAuthenticCache) > self.UNAUTH_CACHE_MAXSIZE: self._v_unAuthenticCache = {} key = (username, passwd) timestamp = time() self._v_unAuthenticCache[key] = timestamp def clearAuthCache(self): """clear the user cache""" self._v_authenticCache.clear() self._v_unAuthenticCache.clear() # # Register the plugin and it's manage forms # httpsAuthReg=PluginRegister('httpsAuthSource', 'HTTPS Authentication Source', httpsAuthSource, manage_addhttpsAuthSourceForm, manage_addhttpsAuthSource, manage_edithttpsAuthSourceForm) exUserFolder.authSources['httpsAuthSource']=httpsAuthReg