[gen, shared] More work on LDAP.
This commit is contained in:
parent
1be7d9f0ab
commit
79d89aca2b
|
@ -368,7 +368,12 @@ class User(Model):
|
|||
|
||||
# ------------------------------------------------------------------------------
|
||||
class LdapConfig:
|
||||
'''Parameters for authenticating users to an external LDAP.'''
|
||||
'''Parameters for authenticating users to an LDAP server.'''
|
||||
ldapAttributes = { 'loginAttribute':None, 'emailAttribute':'email',
|
||||
'fullNameAttribute':'title',
|
||||
'firstNameAttribute':'firstName',
|
||||
'lastNameAttribute':'name' }
|
||||
|
||||
def __init__(self):
|
||||
self.server = '' # Name of the LDAP server
|
||||
self.port = None # Port for this server.
|
||||
|
@ -378,7 +383,17 @@ class LdapConfig:
|
|||
self.adminPassword = ''
|
||||
# LDAP attribute to use as login for authenticating users.
|
||||
self.loginAttribute = 'dn' # Can also be "mail", "sAMAccountName", "cn"
|
||||
self.baseDn = '' # Base distinguished name where to find users.
|
||||
# LDAP attributes for storing email
|
||||
self.emailAttribute = None
|
||||
# LDAP attribute for storing full name (first + last name)
|
||||
self.fullNameAttribute = None
|
||||
# Alternately, LDAP attributes for storing 1st & last names separately.
|
||||
self.firstNameAttribute = None
|
||||
self.lastNameAttribute = None
|
||||
# LDAP classes defining the users stored in the LDAP.
|
||||
self.userClasses = ('top', 'person')
|
||||
self.baseDn = '' # Base DN where to find users in the LDAP.
|
||||
self.scope = 'SUBTREE' # Scope of the search within self.baseDn
|
||||
|
||||
def getServerUri(self):
|
||||
'''Returns the complete URI for accessing the LDAP, ie
|
||||
|
@ -386,6 +401,37 @@ class LdapConfig:
|
|||
port = self.port or 389
|
||||
return 'ldap://%s:%d' % (self.server, port)
|
||||
|
||||
def getUserFilterValues(self, login):
|
||||
'''Gets the filter values required to perform a query for finding user
|
||||
corresponding to p_login in the LDAP.'''
|
||||
res = [(self.loginAttribute, login)]
|
||||
for userClass in self.userClasses:
|
||||
res.append( ('objectClass', userClass) )
|
||||
return res
|
||||
|
||||
def getUserAttributes(self):
|
||||
'''Gets the attributes we want to get from the LDAP for characterizing
|
||||
a user.'''
|
||||
res = [self.loginAttribute]
|
||||
for name in self.ldapAttributes.iterkeys():
|
||||
if getattr(self, name):
|
||||
res.append(getattr(self, name))
|
||||
return res
|
||||
|
||||
def getUserParams(self, ldapData):
|
||||
'''Formats the user-related p_ldapData retrieved from the ldap, as a
|
||||
dict of params usable for creating or updating the corresponding
|
||||
Appy user.'''
|
||||
res = {}
|
||||
for name, appyName in self.ldapAttributes.iteritems():
|
||||
if not appyName: continue
|
||||
# Get the name of the attribute as known in the LDAP.
|
||||
ldapName = getattr(self, name)
|
||||
if not ldapName: continue
|
||||
if ldapData.has_key(ldapName) and ldapData[ldapName]:
|
||||
res[appyName] = ldapData[ldapName]
|
||||
return res
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Config:
|
||||
'''If you want to specify some configuration parameters for appy.gen and
|
||||
|
@ -397,7 +443,6 @@ class Config:
|
|||
class Config(appy.gen.Config):
|
||||
langages = ('en', 'fr')
|
||||
'''
|
||||
|
||||
# For every language code that you specify in this list, appy.gen will
|
||||
# produce and maintain translation files.
|
||||
languages = ['en']
|
||||
|
|
34
gen/ldap.py
34
gen/ldap.py
|
@ -1,34 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
try:
|
||||
import ldap
|
||||
except ImportError:
|
||||
# For people that do not care about ldap.
|
||||
ldap = None
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
def connect(serverUri, login, password):
|
||||
'''Tries to connect to some LDAP server whose UIR is p_serverUri, using
|
||||
p_login and p_password as credentials.'''
|
||||
try:
|
||||
server = ldap.initialize(serverUri)
|
||||
server.simple_bind(login, password)
|
||||
return True, server, None
|
||||
except ldap.LDAPError, le:
|
||||
return False, None, str(le)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
def authenticate(login, password, ldapConfig, tool):
|
||||
'''Tries to authenticate user p_login in the LDAP.'''
|
||||
# Connect to the ldap server.
|
||||
serverUri = cfg.getServerUri()
|
||||
success, server, msg = connect(serverUri, cfg.adminLogin, cfg.adminPassword)
|
||||
# Manage a connection error.
|
||||
if not success:
|
||||
tool.log('%s: connect error (%s).' % (serverUri, msg))
|
||||
return
|
||||
# Do p_login and p_password correspond to a user in the LDAP?
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,8 +1,8 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, sys, re, time, random, types
|
||||
import os, os.path, sys, re, time, random, types, base64
|
||||
from appy import Object
|
||||
import appy.gen
|
||||
from appy.gen import Search, UiSearch, String, Page, ldap
|
||||
from appy.gen import Search, UiSearch, String, Page
|
||||
from appy.gen.layout import ColumnLayout
|
||||
from appy.gen import utils as gutils
|
||||
from appy.gen.mixins import BaseMixin
|
||||
|
@ -12,6 +12,7 @@ from appy.gen.mail import sendMail
|
|||
from appy.shared import mimeTypes
|
||||
from appy.shared import utils as sutils
|
||||
from appy.shared.data import languages
|
||||
from appy.shared.ldap_connector import LdapConnector
|
||||
try:
|
||||
from AccessControl.ZopeSecurityPolicy import _noroles
|
||||
except ImportError:
|
||||
|
@ -979,21 +980,106 @@ class ToolMixin(BaseMixin):
|
|||
'''Returns the encrypted version of clear p_password.'''
|
||||
return self.acl_users._encryptPassword(password)
|
||||
|
||||
def _zopeAuthenticate(self, request):
|
||||
'''Performs the Zope-level authentication. Returns True if
|
||||
authentication succeeds.'''
|
||||
user = self.acl_users.validate(request)
|
||||
return user.getUserName() != 'Anonymous User'
|
||||
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).
|
||||
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.
|
||||
if getattr(req, '_auth', None):
|
||||
# HTTP basic authentication credentials are present (used when
|
||||
# connecting to the ZMI). Decode it.
|
||||
creds = req._auth
|
||||
if creds.lower().startswith('basic '):
|
||||
try:
|
||||
creds = creds.split(' ')[-1]
|
||||
login, password = base64.decodestring(creds).split(':', 1)
|
||||
except Exception, e:
|
||||
pass
|
||||
# Ib. Identify the user from the authentication cookie.
|
||||
if not login:
|
||||
login, password = gutils.readCookie(req)
|
||||
# Ic. 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
|
||||
# "anon" or "system".
|
||||
if not login and not authentify:
|
||||
# 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
|
||||
|
||||
def _ldapAuthenticate(self, login, password):
|
||||
'''Performs a LDAP-based authentication. Returns True if authentication
|
||||
succeeds.'''
|
||||
# Check if LDAP is configured.
|
||||
ldapConfig = self.getProductConfig(True).ldap
|
||||
if not ldapConfig: return
|
||||
user = ldap.authenticate(login, password, ldapConfig, self)
|
||||
if not user: return
|
||||
return True
|
||||
cfg = self.getProductConfig(True).ldap
|
||||
if not cfg: return
|
||||
# Get a connector to the LDAP server and connect to the LDAP server.
|
||||
serverUri = cfg.getServerUri()
|
||||
connector = LdapConnector(serverUri, tool=self)
|
||||
success, msg = connector.connect(cfg.adminLogin, cfg.adminPassword)
|
||||
if not success: return
|
||||
# Check if the user corresponding to p_login exists in the LDAP.
|
||||
filter = connector.getFilter(cfg.getUserFilterValues(login))
|
||||
params = cfg.getUserAttributes()
|
||||
ldapData = connector.search(cfg.baseDn, cfg.scope, filter, params)
|
||||
if not ldapData: return
|
||||
# The user exists. Try to connect to the LDAP with this user in order
|
||||
# to validate its password.
|
||||
userConnector = LdapConnector(serverUri, tool=self)
|
||||
success, msg = userConnector.connect(ldapData[0][0], password)
|
||||
if not success: return
|
||||
# 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
|
||||
if user:
|
||||
# Update the user with fresh info about him from the LDAP
|
||||
for name, value in userParams.iteritems():
|
||||
setattr(user, name, value)
|
||||
user.reindex()
|
||||
else:
|
||||
# Create the user
|
||||
user = tool.create('users', login=login, source='ldap',**userParams)
|
||||
return user
|
||||
|
||||
def performLogin(self):
|
||||
'''Logs the user in.'''
|
||||
|
@ -1004,16 +1090,12 @@ class ToolMixin(BaseMixin):
|
|||
if jsEnabled and not cookiesEnabled:
|
||||
msg = self.translate('enable_cookies')
|
||||
return self.goto(urlBack, msg)
|
||||
# Extract the login and password, and create an authentication cookie
|
||||
login = rq.get('__ac_name', '')
|
||||
password = rq.get('__ac_password', '')
|
||||
gutils.writeCookie(login, password, rq)
|
||||
# Perform the Zope-level authentication
|
||||
if self._zopeAuthenticate(rq) or self._ldapAuthenticate(login,password):
|
||||
# Authenticate the user.
|
||||
login = rq.get('__ac_name', None)
|
||||
if self.getUser(authentify=True):
|
||||
msg = self.translate('login_ok')
|
||||
logMsg = 'User "%s" logged in.' % login
|
||||
else:
|
||||
rq.RESPONSE.expireCookie('_appy_', path='/')
|
||||
msg = self.translate('login_ko')
|
||||
logMsg = 'Authentication failed with login "%s".' % login
|
||||
self.log(logMsg)
|
||||
|
@ -1051,33 +1133,19 @@ class ToolMixin(BaseMixin):
|
|||
# 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)
|
||||
print c
|
||||
# Try to get user name and password from basic authentication
|
||||
login, password = self.identify(auth)
|
||||
if not login:
|
||||
# Try to get them from a cookie
|
||||
login, password = gutils.readCookie(request)
|
||||
if not login:
|
||||
# Maybe the user just entered his credentials. The cookie could
|
||||
# have been set in the response, but is not in the request.
|
||||
login = request.get('__ac_name', None)
|
||||
password = request.get('__ac_password', None)
|
||||
# Try to authenticate this user
|
||||
user = self.authenticate(login, password, request)
|
||||
emergency = self._emergency_user
|
||||
if emergency and (user is emergency):
|
||||
# It is the emergency user.
|
||||
return emergency.__of__(self)
|
||||
elif user is None:
|
||||
# Identify and authentify the user
|
||||
user = self.getParentNode().config.getUser(authentify=True)
|
||||
if not user:
|
||||
# Login and/or password incorrect. Try to authorize and return the
|
||||
# anonymous user.
|
||||
if self.authorize(self._nobody, a, c, n, v, roles):
|
||||
return self._nobody.__of__(self)
|
||||
else:
|
||||
return # Anonymous can't acces this object
|
||||
return
|
||||
else:
|
||||
# We found a user and his password was correct. Try to authorize him
|
||||
# against the published object.
|
||||
user = user.getZopeUser()
|
||||
if self.authorize(user, a, c, n, v, roles):
|
||||
return user.__of__(self)
|
||||
# That didn't work. Try to authorize the anonymous user.
|
||||
|
@ -1090,30 +1158,6 @@ class ToolMixin(BaseMixin):
|
|||
from AccessControl.User import BasicUserFolder
|
||||
BasicUserFolder.validate = validate
|
||||
|
||||
def getUser(self):
|
||||
'''Gets the User instance (Appy wrapper) corresponding to the current
|
||||
user.'''
|
||||
tool = self.appy()
|
||||
rq = tool.request
|
||||
# Try first to return the user that can be cached on the request.
|
||||
if hasattr(rq, 'user'): return rq.user
|
||||
# Get the user login from the authentication cookie.
|
||||
login, password = gutils.readCookie(rq)
|
||||
if not login: # It is the anonymous user or the system.
|
||||
# If we have a real request object, it is the anonymous user.
|
||||
login = (rq.__class__.__name__ == 'Object') and 'system' or 'anon'
|
||||
# Get the User object 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 Zope startup.
|
||||
if not user: return
|
||||
rq.user = user
|
||||
# Precompute some values or this user for performance reasons.
|
||||
rq.userRoles = user.getRoles()
|
||||
rq.userLogins = user.getLogins()
|
||||
rq.zopeUser = user.getZopeUser()
|
||||
return user
|
||||
|
||||
def getUserLine(self):
|
||||
'''Returns a info about the currently logged user as a 2-tuple: first
|
||||
elem is the one-line user info as shown on every page; second line is
|
||||
|
|
|
@ -141,7 +141,8 @@ class ModelClass:
|
|||
# The User class ---------------------------------------------------------------
|
||||
class User(ModelClass):
|
||||
_appy_attributes = ['title', 'name', 'firstName', 'login', 'password1',
|
||||
'password2', 'email', 'roles', 'groups', 'toTool']
|
||||
'password2', 'email', 'roles', 'source', 'groups',
|
||||
'toTool']
|
||||
# All methods defined below are fake. Real versions are in the wrapper.
|
||||
title = gen.String(show=False, indexed=True)
|
||||
gm = {'group': 'main', 'width': 25}
|
||||
|
@ -150,6 +151,9 @@ class User(ModelClass):
|
|||
firstName = gen.String(show=showName, **gm)
|
||||
def showEmail(self): pass
|
||||
email = gen.String(show=showEmail, **gm)
|
||||
# Where is this user stored? By default, in the ZODB. But the user can be
|
||||
# stored in an external LDAP (source='ldap').
|
||||
source = gen.String(show=False, default='zodb', layouts='f', **gm)
|
||||
gm['multiplicity'] = (1,1)
|
||||
def showLogin(self): pass
|
||||
def validateLogin(self): pass
|
||||
|
@ -164,9 +168,6 @@ class User(ModelClass):
|
|||
def showRoles(self): pass
|
||||
roles = gen.String(show=showRoles, indexed=True,
|
||||
validator=gen.Selection('getGrantableRoles'), **gm)
|
||||
# Where is this user stored? By default, in the ZODB. But the user can be
|
||||
# stored in an external LDAP.
|
||||
source = gen.String(show=False, default='zodb', layouts='f', **gm)
|
||||
|
||||
# The Group class --------------------------------------------------------------
|
||||
class Group(ModelClass):
|
||||
|
|
72
shared/ldap_connector.py
Normal file
72
shared/ldap_connector.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
try:
|
||||
import ldap
|
||||
except ImportError:
|
||||
# For people that do not care about ldap.
|
||||
ldap = None
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class LdapConnector:
|
||||
'''This class manages the communication with a LDAP server.'''
|
||||
def __init__(self, serverUri, tentatives=5, ssl=False, timeout=5,
|
||||
tool=None):
|
||||
# The URI of the LDAP server, ie ldap://some.ldap.server:389.
|
||||
self.serverUri = serverUri
|
||||
# The object that will represent the LDAP server
|
||||
self.server = None
|
||||
# The number of trials the connector will at most perform to the LDAP
|
||||
# server, when executing a query in it.
|
||||
self.tentatives = tentatives
|
||||
self.ssl = ssl
|
||||
# The timeout for every query to the LDAP.
|
||||
self.timeout = timeout
|
||||
# A tool from a Appy application can be given and will be used, ie for
|
||||
# logging purpose.
|
||||
self.tool = tool
|
||||
|
||||
def log(self, message, type='info'):
|
||||
'''Logs via a Appy tool if available.'''
|
||||
if self.tool: self.tool.log(message, type=type)
|
||||
|
||||
def connect(self, login, password):
|
||||
'''Connects to the LDAP server using p_login and p_password as
|
||||
credentials. If the connection succeeds, a server object is created
|
||||
in self.server and tuple (True, None) is returned. Else, tuple
|
||||
(False, errorMessage) is returned.'''
|
||||
try:
|
||||
self.server = ldap.initialize(self.serverUri)
|
||||
self.server.simple_bind_s(login, password)
|
||||
return True, None
|
||||
except ldap.LDAPError, le:
|
||||
message = str(le)
|
||||
self.log('%s: connect error with login %s (%s).' % \
|
||||
(self.serverUri, login, message))
|
||||
return False, message
|
||||
|
||||
def getFilter(self, values):
|
||||
'''Builds and returns a LDAP filter based on p_values, a tuple or list
|
||||
of tuples (name,value).'''
|
||||
return '(&%s)' % ''.join(['(%s=%s)' % (n, v) for n, v in values])
|
||||
|
||||
def search(self, baseDn, scope, filter, attributes=None):
|
||||
'''Performs a query in the LDAP at node p_baseDn, with the given
|
||||
p_scope. p_filter is a LDAP filter that constraints the search. It
|
||||
can be computed from a list of tuples (value, name) by method
|
||||
m_getFilter. p_attributes is the list of attributes that we will
|
||||
retrieve from the LDAP. If None, all attributes will be retrieved.'''
|
||||
if self.ssl: self.server.start_tls_s()
|
||||
try:
|
||||
# Get the LDAP constant corresponding to p_scope.
|
||||
scope = getattr(ldap, 'SCOPE_%s' % scope)
|
||||
# Perform the query.
|
||||
for i in range(self.tentatives):
|
||||
try:
|
||||
return self.server.search_st(\
|
||||
baseDn, scope, filterstr=filter, attrlist=attributes,
|
||||
timeout=self.timeout)
|
||||
except ldap.TIMEOUT:
|
||||
pass
|
||||
except ldap.LDAPError, le:
|
||||
self.log('LDAP query error %s: %s' % \
|
||||
(le.__class__.__name__, str(le)))
|
||||
# ------------------------------------------------------------------------------
|
Loading…
Reference in a new issue