backends: implement a new ldap storage backend

Closes #5425
This commit is contained in:
Jérôme Schneider 2014-09-15 10:48:46 +02:00
parent 85cc82c4e2
commit 5294fd40c4
6 changed files with 261 additions and 6 deletions

93
ldap/mandaye.schema Normal file
View File

@ -0,0 +1,93 @@
# vim:et:sw=4
#
# CUD Schema
#
# Date : 20140908
# Révision :
# * 20140908 - Jérôme Schneider <jschneider@entrouvert.com> - Création initiale
#
objectIdentifier EoRoot 1.3.6.1.4.1.36560
objectIdentifier EoClientRoot EoRoot:3
objectIdentifier MandayeRoot EoClientRoot:1127
objectIdentifier MandayeLdap MandayeRoot:1
objectIdentifier MandayeLdapAttributes MandayeLdap:1
objectIdentifier MandayeLdapObjectClass MandayeLdap:2
objectIdentifier Int 1.3.6.1.4.1.1466.115.121.1.27
objectIdentifier UTF8 1.3.6.1.4.1.1466.115.121.1.15
objectIdentifier Boolean 1.3.6.1.4.1.1466.115.121.1.7
objectIdentifier Binary 1.3.6.1.4.1.1466.115.121.1.40
objectIdentifier DateTime 1.3.6.1.4.1.1466.115.121.1.24
objectIdentifier IA5String 1.3.6.1.4.1.1466.115.121.1.26
attributetype (MandayeLdapAttributes:1
NAME 'uniqueID'
DESC 'Mandaye user unique identifier'
EQUALITY caseIgnoreMatch
SYNTAX UTF8
SINGLE-VALUE )
attributetype (MandayeLdapAttributes:2
NAME 'idpUniqueID'
DESC 'IDP unique id (like NameID)'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX UTF8
SINGLE-VALUE )
attributetype (MandayeLdapAttributes:3
NAME 'idpName'
DESC 'Name of the idp'
EQUALITY caseIgnoreMatch
SYNTAX UTF8
SINGLE-VALUE )
attributetype (MandayeLdapAttributes:4
NAME 'spLogin'
DESC 'SP login'
EQUALITY caseIgnoreMatch
SYNTAX UTF8
SINGLE-VALUE )
attributetype (MandayeLdapAttributes:5
NAME 'spPostValues'
DESC 'SP post values'
EQUALITY caseIgnoreMatch
SYNTAX UTF8
SINGLE-VALUE )
attributetype (MandayeLdapAttributes:6
NAME 'spName'
DESC 'name of the service provider'
EQUALITY caseIgnoreMatch
SYNTAX UTF8
SINGLE-VALUE )
attributetype (MandayeLdapAttributes:7
NAME 'creationDate'
DESC 'creation date of the association (ISO8601 format)'
EQUALITY generalizedTimeMatch
SYNTAX DateTime
SINGLE-VALUE )
attributetype (MandayeLdapAttributes:8
NAME 'lastConnectionDate'
DESC 'Last connection date ISO8601'
EQUALITY generalizedTimeMatch
ORDERING generalizedTimeOrderingMatch
SYNTAX DateTime
SINGLE-VALUE )
#
# Classes d'objets:
#
objectclass (MandayeLdapObjectClass:1
NAME 'MandayeUser'
DESC 'Mandaye user Objectclass'
SUP top
STRUCTURAL
MUST ( uniqueID $ idpUniqueId $ idpName $ spLogin $ spPostValues $ creationDate $ spName )
MAY ( lastConnectionDate ) )

View File

@ -28,6 +28,11 @@ if config.storage_backend == "mandaye.backends.sql":
bind=create_engine(config.db_url)
)
)
elif config.storage_backend == "mandaye.backends.ldap_back":
import ldap
storage_conn = ldap.initialize(config.ldap_url)
storage_conn.protocol_version = ldap.VERSION3
storage_conn.simple_bind(config.ldap_bind_dn, config.ldap_bind_password)
backend = import_backend(config.storage_backend)
Association = backend.Association

View File

@ -0,0 +1,137 @@
import datetime
import ldap
import ldap.modlist
import random
from mandaye import config
from mandaye.log import logger
from mandaye.backends.default import storage_conn
class Association(object):
"""
association dictionary return by the following methods:
{
'id': '', # identifier of your association (must be unique)
'sp_name': '', # name of the service provider (defined in the mappers)
'sp_login': '', # login on the service provider
'sp_post_values': '', # the post values for sp login form
'idp_unique_id:': '', # the unique identifier of the identity provider (ex.: a saml NameID)
'idp_name': '', # identity provide name
'last_connection': datetime.datetime, # last connection with this association
'creation_date': datetime.datetime, # creation date of this association
}
"""
@staticmethod
def ldap2association(ldap_object):
return {
'id': ldap_object['uniqueID'][0],
'sp_name': ldap_object['spName'][0],
'sp_login': ldap_object['spLogin'][0],
'sp_post_values': ldap_object['spPostValues'][0],
'idp_unique_id': ldap_object['idpUniqueID'][0],
'idp_name': ldap_object['idpName'][0],
'last_connection': datetime.datetime.strptime(
ldap_object['lastConnectionDate'][0][:14],
'%Y%m%d%H%M%S'),
'creation_date': datetime.datetime.strptime(
ldap_object['creationDate'][0][:14],
'%Y%m%d%H%M%S'),
}
@staticmethod
def get(sp_name, idp_unique_id, idp_name='default'):
""" return a list of dict with associations matching all of this options """
associations = []
results = storage_conn.search_s(config.ldap_base_dn, ldap.SCOPE_ONELEVEL,
filterstr='(&(objectClass=MandayeUser)(spName=%s)(idpUniqueID=%s)(idpName=%s))' % (sp_name, idp_unique_id, idp_name))
for result in results:
associations.append(Association.ldap2association(result[1]))
return associations
@staticmethod
def get_by_id(asso_id):
""" return an dict of the association with the id or None if it doesn't exist """
results = storage_conn.search_s(config.ldap_base_dn, ldap.SCOPE_ONELEVEL,
filterstr='(&(objectClass=MandayeUser)(uniqueID=%s))' %\
(asso_id))
if results:
return Association.ldap2association(results[0][1])
return None
@staticmethod
def has_id(asso_id):
""" check the given user is present in the directory """
results = storage_conn.search_s(config.ldap_base_dn, ldap.SCOPE_ONELEVEL,
filterstr='(&(objectClass=MandayeUser)(uniqueID=%s))' %\
(asso_id))
if results:
return True
return False
@staticmethod
def update_or_create(sp_name, sp_login, sp_post_values, idp_unique_id, idp_name='default'):
""" update or create an associtaion which match the following values
return the association id
"""
results = storage_conn.search_s(config.ldap_base_dn, ldap.SCOPE_ONELEVEL,
filterstr='(&(objectClass=MandayeUser)(spName=%s)(spLogin=%s)(idpUniqueID=%s)(idpName=%s))' %\
(sp_name, sp_login, idp_unique_id, idp_name))
if not results:
association = {'spName': sp_name,
'spLogin': sp_login,
'spPostValues': sp_post_values,
'idpUniqueID': idp_unique_id,
'idpName': idp_name,
'creationDate': datetime.datetime.utcnow().strftime('%Y%m%d%H%M%SZ'),
'lastConnectionDate': datetime.datetime.utcnow().strftime('%Y%m%d%H%M%SZ'),
'objectClass': 'MandayeUser'
}
mod_list = ldap.modlist.addModlist(association)
while True:
unique_id = random.randint(1, 5000000)
dn = "uniqueID=%s,%s" % (unique_id, config.ldap_base_dn)
try:
result = storage_conn.add_s(dn, mod_list)
except ldap.ALREADY_EXISTS:
continue
break
logger.info("New association %r with %r", sp_login, idp_unique_id)
return unique_id
else:
dn = results[0][0]
mod_list = [(ldap.MOD_REPLACE, 'spPostValues', sp_post_values)]
storage_conn.modify_s(dn, mod_list)
logger.info("Update post values for %r (%r)", sp_login, idp_unique_id)
return results[0][1]['uniqueID'][0]
@staticmethod
def delete(asso_id):
""" delete the association which has the following asso_id """
dn = "uniqueID=%s,%s" % (asso_id, config.ldap_base_dn)
storage_conn.delete_s(dn)
logger.info('Delete %r association', dn)
@staticmethod
def get_last_connected(sp_name, idp_unique_id, idp_name='default'):
""" get the last connecting association which match the parameters
return a dict of the association
"""
results = storage_conn.search_s(config.ldap_base_dn, ldap.SCOPE_ONELEVEL,
filterstr='(&(objectClass=MandayeUser)(spName=%s)(idpUniqueID=%s)(idpName=%s))' % (sp_name, idp_unique_id, idp_name))
if results:
return Association.ldap2association(results[0][1])
return None
@staticmethod
def update_last_connection(asso_id):
""" update the association last connection time with the current time
return a dict of the association
"""
last_connection = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%SZ")
dn = "uniqueID=%s,%s" % (asso_id, config.ldap_base_dn)
mod_list = [(ldap.MOD_REPLACE, 'lastConnectionDate', last_connection)]
storage_conn.modify_s(dn, mod_list)

View File

@ -4,16 +4,21 @@ import os
_PROJECT_PATH = os.path.join(os.path.dirname(__file__), '..')
# Choose storage
# Only sql at the moment
# mandaye.backends.ldap_back or mandaye.backends.sql
storage_backend = "mandaye.backends.sql"
## SQL Backend config
# Database configuration
# http://docs.sqlalchemy.org/en/rel_0_7/core/engines.html
# http://docs.sqlalchemy.org/en/rel_0_8/core/engines.html
# rfc 1738 https://tools.ietf.org/html/rfc1738
# dialect+driver://username:password@host:port/database
db_url = 'sqlite:///test.db'
## LDAP Backend config
ldap_url = 'ldap://127.0.0.1'
ldap_bind_dn = 'cn=admin,dc=acompany,dc=org'
ldap_bind_password = 'MyPassword'
ldap_base_dn = 'ou=mandaye,dc=acompany,dc=org'
# urllib2 debug mode
debug = False

View File

@ -30,6 +30,12 @@ config.read(SETTINGS_INI)
# dialect+driver://username:password@host:port/database
db_url = config.get('database', 'url')
## LDAP Backend config
ldap_url = config.get('ldap', 'url')
ldap_bind_dn = config.get('ldap', 'base_dn')
ldap_bind_password = config.get('ldap', 'bind_password')
ldap_base_dn = config.get('ldap', 'base_dn')
debug = config.getboolean('debug', 'debug')
# Log configuration
@ -137,12 +143,13 @@ mandaye_offline_toolbar = config.getboolean('mandaye', 'offline_toolbar')
# Authentic 2 auto connection
a2_auto_connection = config.getboolean('mandaye', 'a2_auto_connection')
# Choose storage
# Only mandaye.backends.sql at the moment
# Choose storage (sql or ldap)
if config.get('mandaye', 'storage_backend') == 'sql':
storage_backend = "mandaye.backends.sql"
elif config.get('mandaye', 'storage_backend') == 'ldap':
storage_backend = "mandaye.backends.ldap_back"
else:
ImproperlyConfigured('Storage backend must be sql')
ImproperlyConfigured('Storage backend must be sql or ldap')
# Encrypt service provider passwords with a secret
# You should install pycypto to use this feature

View File

@ -2,9 +2,17 @@
base_dir: .
[database]
; use by sql backend
; http://docs.sqlalchemy.org/en/rel_0_8/core/engines.html
url: sqlite:///%(base_dir)s/{project_name}.db
[ldap]
; use by ldap backend
url: ldap://127.0.0.1
bind_dn: cn=admin,dc=acompany,dc=org
bind_password: AdminPassword
base_dn: ou=mandaye,dc=acompany,dc=org
[dirs]
config_root: %(base_dir)s/conf.d
data_dir: %(base_dir)s/data
@ -23,7 +31,7 @@ sentry_dsn:
toolbar: true
offline_toolbar: true
a2_auto_connection: false
; only sql at the moment
; sql or ldap
storage_backend: sql
auto_decompress: true
; if you want to encypt password set to true