Commit | Line | Data |
---|---|---|
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 | ||
29 | import os | |
30 | import sys | |
31 | import logging | |
32 | from crypt import crypt | |
33 | ||
34 | LDAP_BASE = 'o=shell' | |
35 | LOGFILE = '/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 | |
42 | LDAP_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 | |
47 | LDAP_SUCCESS = 0 | |
48 | # quand ce serveur n'est pas approprié | |
49 | LDAP_REFERRAL = 10 | |
50 | # quand c'est un méchanisme SASL inconnu | |
51 | LDAP_AUTH_METHOD_NOT_SUPPORTED = 7 | |
52 | # quand on veut forcer une authentification (interdir l'accès anonyme) : | |
53 | LDAP_INAPPROPRIATE_AUHTENTICATION = 48 | |
54 | # quand le mot de passe est incorrect : | |
55 | LDAP_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 | ||
66 | def 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 | ||
74 | def 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 | ||
93 | def 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 | ||
105 | def 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 | ||
119 | logging.basicConfig(level=logging.DEBUG, filename=LOGFILE, | |
120 | format="%(asctime)s %(levelname)s %(message)s") | |
121 | logging.info('Starting.') | |
122 | ||
123 | command = sys.stdin.readline().rstrip('\n').rstrip('\r') | |
124 | logging.debug("Got command '%s'." % command) | |
125 | ||
126 | if 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 | ||
136 | if 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 | ||
185 | if 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 | ||
233 | sys.exit(1) |