Tentative de backend shell pour OpenLDAP.
[progfou.git] / openldap / slapd-shell-backend
CommitLineData
bcf7e0c5
P
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# slapd-backend-shell - backend shell pour slapd (OpenLDAP)
4# Copyright ©2010 Agence universitaire de la Francophonie
5# http://www.auf.org/
6# Licence : GNU General Public License, version 3
7# Auteur : Progfou <jean-christophe.andre@auf.org>
8# Création : 2010-03-23
9#
10# Documentation de base : voir "man slapd-shell"
11#
12# Mise en place :
13# - placer ce script dans /usr/local/sbin/slapd-backend-shell
14# - le rendre exécutable
15# - ajouter les lignes suivantes dans /etc/ldap/slapd.conf
16# moduleload back_shell
17# database shell
18# suffix "o=shell"
19# bind /usr/local/sbin/slapd-shell-backend
20# unbind /usr/local/sbin/slapd-shell-backend
21# search /usr/local/sbin/slapd-shell-backend
22# - ajouter éventuellement "loglevel stats shell" et surveiller /var/log/debug
23# - créer un dossier /var/log/ldap et l'attribuer à openldap:openldap en 0700
24# - relancer le service slapd
25# - tester avec (le mot de passe est test1) :
26# ldapsearch -x -b o=shell -D mail=test1@example.com,o=shell -W
27#
28
29import os
30import sys
31import logging
32from crypt import crypt
33
34LDAP_BASE = 'o=shell'
35LOGFILE = '/var/log/ldap/backend-shell.log'
36
37#=============================================================================
38# CONSTANTES
39#=============================================================================
40
41# BIND OPERATION, voir http://tools.ietf.org/html/rfc2251#section-4.2
42LDAP_AUTH_SIMPLE = 128
43
44# BIND RESPONSE, voir http://tools.ietf.org/html/rfc2251#section-4.2.3
45# et http://tools.ietf.org/html/rfc2251#section-4.1.10
46# quand l'opération se termine avec succès
47LDAP_SUCCESS = 0
48# quand ce serveur n'est pas approprié
49LDAP_REFERRAL = 10
50# quand c'est un méchanisme SASL inconnu
51LDAP_AUTH_METHOD_NOT_SUPPORTED = 7
52# quand on veut forcer une authentification (interdir l'accès anonyme) :
53LDAP_INAPPROPRIATE_AUHTENTICATION = 48
54# quand le mot de passe est incorrect :
55LDAP_INVALID_CREDENTIALS = 49
56
57#=============================================================================
58# OUTILS
59#=============================================================================
60
61_cache = {
62 'test1@example.com': '$1$2JF5hLaZ$tZz4RXau.GCTqUsAmCeXM/',
63 'test2@example.com': '$1$o2YURjVS$Qd02DXFpGqPtfy1yGH1CM0',
64}
65
66def find_cred(key):
67 """Recherche du mot de passe crypté en fonction de l'utilisateur.
68 On cherche d'abord dans le cache... et un jour dans la vraie base. ;-)
69 """
70 if key in _cache:
71 return _cache[key]
72 return None
73
74def search_object(filter=None):
75 """On recherche les objects correspondant aux filtre.
76 """
77 result = [ ]
78 # FIXME: tout pour le moment...
79 # TODO: tenir compte de filter
80 for key in _cache:
81 dn = 'mail=%s,%s' % (key, LDAP_BASE)
82 obj = { 'dn': [ dn ] }
83 for k,v in (x.split('=') for x in dn.split(',')):
84 if k not in obj:
85 obj[k] = [ v ]
86 else:
87 obj[k].append(v)
88 obj.update({'mail': [ key ] })
89 obj.update({'userPassword': [ '{CRYPT}' + _cache[key] ] })
90 result.append(obj)
91 return result
92
93def get_params():
94 params = { }
95 line = sys.stdin.readline().rstrip('\n').rstrip('\r')
96 while line != '':
97 key,value = line.split(': ')
98 if key not in params:
99 params[key] = [ value ]
100 else:
101 params[key].append(value)
102 line = sys.stdin.readline().rstrip('\n').rstrip('\r')
103 return params
104
105def ldap_result(code=None, matched=None, info=None):
106 lines = [ 'RESULT' ]
107 if code != None:
108 lines.append('code: %s' % code)
109 if matched != None:
110 lines.append('matched: %s' % matched)
111 if info != None:
112 lines.append('info: %s' % info)
113 return '\n'.join(lines) + '\n'
114
115#=============================================================================
116# PROGRAMME PRINCIPAL
117#=============================================================================
118
119logging.basicConfig(level=logging.DEBUG, filename=LOGFILE,
120 format="%(asctime)s %(levelname)s %(message)s")
121logging.info('Starting.')
122
123command = sys.stdin.readline().rstrip('\n').rstrip('\r')
124logging.debug("Got command '%s'." % command)
125
126if command == 'UNBIND':
127 # msgid: <message id>
128 # <repeat { "suffix:" <database suffix DN> }>
129 # dn: <bound DN>
130 params = get_params()
131 logging.debug("Got params '%s'." % params)
132 # FIXME: on devrait vérifier les suffixes, le dn, noter le unbind, ...
133 print ldap_result(code=LDAP_SUCCESS)
134 sys.exit(0)
135
136if command == 'BIND':
137 # msgid: <message id>
138 # <repeat { "suffix:" <database suffix DN> }>
139 # dn: <DN>
140 # method: <method number>
141 # credlen: <length of <credentials>>
142 # cred: <credentials>
143 params = get_params()
144 logging.debug("Got params '%s'." % params)
145 # on vérifie qu'on supporte bien la méthode demandée
146 method = int(params['method'][0])
147 if method != LDAP_AUTH_SIMPLE:
148 logging.info("Unsupported auth method '%s'." % method)
149 print ldap_result(code=LDAP_AUTH_METHOD_NOT_SUPPORTED)
150 sys.exit(0)
151 # on vérifie que la demande concerne bien notre base
152 if LDAP_BASE not in params['suffix']:
153 logging.warning("'%s' not in requested suffixes." % LDAP_BASE)
154 print ldap_result(code=LDAP_REFERRAL)
155 sys.exit(0)
156 # on vérifie que le 'dn' n'est pas vide (pas de connexion anonyme)
157 dn = params['dn'][0]
158 if dn == '':
159 logging.info("Rejecting anonymous connection.")
160 print ldap_result(code=LDAP_INAPPROPRIATE_AUTHENTICATION)
161 sys.exit(0)
162 # on vérifie que le 'dn' demandé est valide (contient 'mail')
163 dn_dict = dict([x.split('=') for x in dn.split(',')])
164 if 'mail' not in dn_dict:
165 logging.info("Invalid dn '%s' (missing 'mail' component)." % dn)
166 print ldap_result(code=LDAP_INVALID_CREDENTIALS)
167 sys.exit(0)
168 # on vérifie que l'objet demandé existe bien dans la base
169 mail = dn_dict['mail']
170 cred = find_cred(mail)
171 if not cred:
172 logging.info("Can't find credential for '%s'." % mail)
173 print ldap_result(code=LDAP_INVALID_CREDENTIALS)
174 sys.exit(0)
175 # on vérifie que le mot de passe correspond bien
176 if crypt(params['cred'][0], cred) != cred:
177 logging.info("Credential mismatch for '%s'." % mail)
178 print ldap_result(code=LDAP_INVALID_CREDENTIALS)
179 sys.exit(0)
180 # si on a passé tout ça, c'est tout bon ! :-)
181 logging.debug("Successful bind for '%s'." % mail)
182 print ldap_result(code=LDAP_SUCCESS)
183 sys.exit(0)
184
185if command == 'SEARCH':
186 # msgid: <message id>
187 # <repeat { "suffix:" <database suffix DN> }>
188 # base: <base DN>
189 # scope: <0-2, see ldap.h>
190 # deref: <0-3, see ldap.h>
191 # sizelimit: <size limit>
192 # timelimit: <time limit>
193 # filter: <filter>
194 # attrsonly: <0 or 1>
195 # attrs: <"all" or space-separated attribute list>
196 params = get_params()
197 logging.debug("Got params '%s'." % params)
198 # on vérifie que la demande concerne bien notre base
199 if LDAP_BASE not in params['suffix']:
200 logging.warning("'%s' not in requested suffixes." % LDAP_BASE)
201 print ldap_result(code=LDAP_REFERRAL)
202 sys.exit(0)
203 base = params['base'][0]
204 if base != LDAP_BASE:
205 logging.warning("Invalid base '%s'." % base)
206 print ldap_result(code=LDAP_REFERRAL)
207 sys.exit(0)
208 # on affiche le résultat
209 sizelimit = int(params['sizelimit'][0])
210 filter = params['filter'][0]
211 attrs = params['attrs'][0].split(' ')
212 logging.debug("Start of search for '%s' (sizelimit=%s, attrs='%s')." \
213 % (filter, sizelimit, attrs))
214 for obj in search_object(filter)[0:sizelimit]:
215 if attrs[0] == 'all':
216 attr_list = obj.keys()
217 else:
218 attr_list = attrs
219 print 'dn: %s' % obj['dn'][0]
220 attr_list = set(attr_list) - set(['dn','userPassword'])
221 for attr in attr_list:
222 try:
223 for value in obj[attr]:
224 print '%s: %s' % (attr, value)
225 except:
226 pass
227 print ""
228 # si on a passé tout ça, c'est tout bon ! :-)
229 logging.debug("End of search.")
230 print ldap_result(code=LDAP_SUCCESS)
231 sys.exit(0)
232
233sys.exit(1)