Tentative de backend shell pour OpenLDAP.
[progfou.git] / openldap / slapd-shell-backend
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)