Django

Code

Ticket #2507: ldapauth.py

File ldapauth.py, 12.8 kB (added by Rozza, 3 months ago)

See comments for ldapauth.py

Line 
1 from django.conf import settings
2 from django.contrib.auth.models import User
3
4 import logging
5
6 class LDAPBackend(object):
7     """
8     Authenticate a user against LDAP.
9     Requires python-ldap to be installed.
10
11     Requires the following things to be in settings.py:
12     LDAP_DEBUG -- boolean
13         Uses logging module for debugging messages.
14     LDAP_SERVER_URI -- string, ldap uri.
15         default: 'ldap://localhost'
16     LDAP_SEARCHDN -- string of the LDAP dn to use for searching
17         default: 'dc=localhost'
18     LDAP_SCOPE -- one of: ldap.SCOPE_*, used for searching
19         see python-ldap docs for the search function
20         default = ldap.SCOPE_SUBTREE
21     LDAP_SEARCH_FILTER -- formated string, the filter to use for searching for a
22         user. Used as: filterstr = LDAP_SEARCH_FILTER % username
23         default = 'cn=%s'
24     LDAP_UPDATE_FIELDS -- boolean, do we sync the db with ldap on each auth
25         default = True
26
27     Required unless LDAP_FULL_NAME is set:
28     LDAP_FIRST_NAME -- string, LDAP attribute to get the given name from
29     LDAP_LAST_NAME -- string, LDAP attribute to get the last name from
30
31     Optional Settings:
32     LDAP_FULL_NAME -- string, LDAP attribute to get name from, splits on ' '
33     LDAP_GID -- string, LDAP attribute to get group name/number from
34     LDAP_SU_GIDS -- list of strings, group names/numbers that are superusers
35     LDAP_STAFF_GIDS -- list of strings, group names/numbers that are staff
36     LDAP_EMAIL -- string, LDAP attribute to get email from
37     LDAP_DEFAULT_EMAIL_SUFFIX -- string, appened to username if no email found
38     LDAP_OPTIONS -- hash, python-ldap global options and their values
39         {ldap.OPT_X_TLS_CACERTDIR: '/etc/ldap/ca/'}
40     LDAP_ACTIVE_FIELD -- list of strings, LDAP attribute to get active status
41         from
42     LDAP_ACTIVE -- list of strings, allowed for active from LDAP_ACTIVE_FIELD
43
44     You must pick a method for determining the DN of a user and set the needed
45     settings:
46         - You can set LDAP_BINDDN and LDAP_BIND_ATTRIBUTE like:
47             LDAP_BINDDN = 'ou=people,dc=example,dc=com'
48             LDAP_BIND_ATTRIBUTE = 'uid'
49           and the user DN would be:
50             'uid=%s,ou=people,dc=example,dc=com' % username
51
52         - Look for the DN on the directory, this is what will happen if you do
53           not define the LDAP_BINDDN setting. In that case you may need to
54           define LDAP_PREBINDDN and LDAP_PREBINDPW if your LDAP server does not
55           allow anonymous queries. The search will be performed with the
56           LDAP_SEARCH_FILTER setting.
57
58         - Override the _pre_bind() method, which receives the ldap object and
59           the username as it's parameters and should return the DN of the user.
60
61     By inheriting this class you can change:
62         - How the dn to bind with is produced by overriding _pre_bind()
63         - What type of user object to use by overriding: _get_user_by_name(),
64           _create_user_object(), and get_user()
65     """
66
67     import ldap
68     from django.conf import settings
69     from django.contrib.auth.models import User
70
71     settings = {
72               'LDAP_SERVER_URI': 'ldap://localhost',
73               'LDAP_SEARCHDN': 'dc=localhost',
74               'LDAP_SCOPE': ldap.SCOPE_SUBTREE,
75               'LDAP_SEARCH_FILTER': 'cn=%s',
76               'LDAP_UPDATE_FIELDS': True,
77               'LDAP_PREBINDDN': None,
78               'LDAP_PREBINDPW': None,
79               'LDAP_BINDDN': None,
80               'LDAP_BIND_ATTRIBUTE': None,
81               'LDAP_FIRST_NAME': None,
82               'LDAP_LAST_NAME': None,
83               'LDAP_FULL_NAME': None,
84               'LDAP_GID': None,
85               'LDAP_SU_GIDS': None,
86               'LDAP_STAFF_GIDS': None,
87               'LDAP_ACTIVE_FIELD': None,
88               'LDAP_ACTIVE': None,
89               'LDAP_EMAIL': None,
90               'LDAP_DEFAULT_EMAIL_SUFFIX': None,
91               'LDAP_OPTIONS': None,
92               'LDAP_DEBUG': True,
93       }
94
95     def __init__(self):
96         # Load settings from settings.py, put them on self.settings
97         # overriding the defaults.
98         for var in self.settings.iterkeys():
99             if hasattr(settings, var):
100                 self.settings[var] = settings.__getattr__(var)
101
102     def authenticate(self, username=None, password=None):
103         # Make sure we have a user and pass
104         if not username and password is not None:
105             if self.settings['LDAP_DEBUG']:
106                 assert False
107                 logging.info('LDAPBackend.authenticate failed: username or password empty: %s %s' % (
108                     username, password))
109             return None
110
111         if self.settings['LDAP_OPTIONS']:
112             for k in self.settings['LDAP_OPTIONS']:
113                 self.ldap.set_option(k, self.settings.LDAP_OPTIONS[k])
114
115         l = self.ldap.initialize(self.settings['LDAP_SERVER_URI'])
116
117         bind_string = self._pre_bind(l, username)
118         if not bind_string:
119             if self.settings['LDAP_DEBUG']:
120                 logging.info('LDAPBackend.authenticate failed: _pre_bind return no bind_string (%s, %s)' % (
121                     l, username))
122             return None
123
124         try:
125             # Try to bind as the provided user. We leave the bind until
126             # the end for other ldap.search_s call to work authenticated.
127             l.bind_s(bind_string, password)
128         except (self.ldap.INVALID_CREDENTIALS,
129                 self.ldap.UNWILLING_TO_PERFORM), exc:
130             # Failed user/pass (or missing password)
131             if self.settings['LDAP_DEBUG']:
132                 logging.info('LDAPBackend.authenticate failed: %s' % exc)
133             l.unbind_s()
134             return None
135
136
137         try:
138             user = self._get_user_by_name(username)
139         except User.DoesNotExist:
140             user = self._get_ldap_user(l, username)
141
142         if user is not None:
143             if self.settings['LDAP_UPDATE_FIELDS']:
144                 self._update_user(l, user)
145
146         l.unbind_s()
147         if self.settings['LDAP_DEBUG']:
148             if user is None:
149                 logging.info('LDAPBackend.authenticate failed: user is None')
150             else:
151                 logging.info('LDAPBackend.authenticate ok: %s %s' % (user, user.__dict__))
152         return user
153
154     # Functions provided to override to customize to your LDAP configuration.
155     def _pre_bind(self, l, username):
156         """
157         Function that returns the dn to bind against ldap with.
158         called as: self._pre_bind(ldapobject, username)
159         """
160         if not self.settings['LDAP_BINDDN']:
161             # When the LDAP_BINDDN setting is blank we try to find the
162             # dn binding anonymously or using LDAP_PREBINDDN
163             if self.settings['LDAP_PREBINDDN']:
164                 try:
165                     l.simple_bind_s(self.settings['LDAP_PREBINDDN'],
166                             self.settings['LDAP_PREBINDPW'])
167                 except self.ldap.LDAPError, exc:
168                     if self.settings['LDAP_DEBUG']:
169                         logging.info('LDAPBackend _pre_bind: LDAPError : %s' % exc)
170                         logging.info("LDAP_PREBINDDN: "+self.settings['LDAP_PREBINDDN']+" PW "+self.settings['LDAP_PREBINDPW'])                     
171                     return None
172
173             # Now do the actual search
174             filter = self.settings['LDAP_SEARCH_FILTER'] % username
175             result = l.search_s(self.settings['LDAP_SEARCHDN'],
176                         self.settings['LDAP_SCOPE'], filter, attrsonly=1)
177
178             if len(result) != 1:
179                 if self.settings['LDAP_DEBUG']:
180                     logging.info('LDAPBackend _pre_bind: not exactly one result: %s (%s %s %s)' % (
181                         result, self.settings['LDAP_SEARCHDN'], self.settings['LDAP_SCOPE'], filter))
182                 return None
183             return result[0][0]
184         else:
185             # LDAP_BINDDN is set so we use it as a template.
186             return "%s=%s,%s" % (self.settings['LDAP_BIND_ATTRIBUTE'], username,
187                     self.settings['LDAP_BINDDN'])
188    
189     def _get_user_by_name(self, username):
190         """
191         Returns an object of contrib.auth.models.User that has a matching
192         username.
193         called as: self._get_user_by_name(username)
194         """
195         return User.objects.get(username=username)
196
197     def _create_user_object(self, username, password):
198         """
199         Creates and returns an object of contrib.auth.models.User.
200         called as: self._create_user_object(username, password)
201         """
202         return User(username=username, password=password)
203
204     # Required for an authentication backend
205     def get_user(self, user_id):
206         try:
207             return User.objects.get(pk=user_id)
208         except:
209             return None
210     # End of functions to override
211
212     def _get_ldap_user(self, l, username):
213         """
214         Helper method, makes a user object and call update_user to populate
215         """
216
217         # Generate a random password string.
218         password = User.objects.make_random_password(10)
219         user = self._create_user_object(username, password)
220         return user
221
222     def _update_user(self, l, user):
223         """
224         Helper method, populates a user object with various attributes from
225         LDAP.
226         """
227
228         username = user.username
229         filter = self.settings['LDAP_SEARCH_FILTER'] % username
230
231         # Get results of search and make sure something was found.
232         # At this point this shouldn't fail.
233         hold = l.search_s(self.settings['LDAP_SEARCHDN'],
234                     self.settings['LDAP_SCOPE'], filter)
235         if len(hold) < 1:
236             raise AssertionError('No results found with: %s' % (filter))
237
238         dn = hold[0][0]
239         attrs = hold[0][1]
240         firstn = self.settings['LDAP_FIRST_NAME'] or None
241         lastn = self.settings['LDAP_LAST_NAME'] or None
242         emailf = self.settings['LDAP_EMAIL'] or None
243
244         if firstn:
245             if firstn in attrs:
246                 user.first_name = attrs[firstn][0]
247             else:
248                 raise NameError('Missing attribute: %s in result for %s'
249                         % (firstn, dn))
250         if lastn:
251             if lastn in attrs:
252                 user.last_name = attrs[lastn][0]
253             else:
254                 raise NameError('Missing attribute: %s in result for %s'
255                         % (lastn, dn))
256         if not firstn and not lastn and self.settings['LDAP_FULL_NAME']:
257             fulln = self.settings['LDAP_FULL_NAME']
258             if fulln in attrs:
259                     tmp = attrs[fulln][0]
260                     user.first_name = tmp.split(' ')[0]
261                     user.last_name = ' '.join(tmp.split(' ')[1:])
262             else:
263                 raise NameError('Missing attribute: %s in result for %s'
264                         % (fulln, dn))
265
266         if emailf and emailf in attrs:
267             user.email = attrs[emailf][0]
268         elif self.settings['LDAP_DEFAULT_EMAIL_SUFFIX']:
269             user.email = username + self.settings['LDAP_DEFAULT_EMAIL_SUFFIX'] 
270
271
272         # Check if we are mapping an ldap id to check if the user is staff or super
273         # Other wise the user is created but not give access
274         if ('LDAP_GID' in self.settings
275                 and self.settings['LDAP_GID'] in attrs):
276             # Turn off access flags
277             user.is_superuser = False
278             user.is_staff = False
279             check_staff_flag = True
280             gids = set(attrs[self.settings['LDAP_GID']])
281
282             # Check to see if we are mapping any super users
283             if 'LDAP_SU_GIDS' in self.settings:
284                 su_gids = set(self.settings['LDAP_SU_GIDS'])
285                 # If any of the su_gids exist in the gid_data then the user is super
286                 if (len(gids-su_gids) < len(gids)):
287                     user.is_superuser = True
288                     user.is_staff = True
289                     # No need to check if a staff user
290                     check_staff_flag = False
291
292             # Check for staff user?
293             if 'LDAP_STAFF_GIDS' in self.settings and check_staff_flag == True:
294                 # We are checking to see if the user is staff
295                 staff_gids = set(self.settings['LDAP_STAFF_GIDS'])
296                 if (len(gids-staff_gids) < len(gids)):
297                     user.is_staff = True
298
299         # Check if we need to see if a user is active
300         if ('LDAP_ACTIVE_FIELD' in self.settings
301             and  self.settings['LDAP_ACTIVE_FIELD']):
302             user.is_active = False
303             if (self.settings.LDAP_ACTIVE_FIELD in attrs
304                 and 'LDAP_ACTIVE' in self.settings):
305                 active_data = set(attrs[self.settings['LDAP_ACTIVE_FIELD']])
306                 active_flags = set(self.settings.LDAP_ACTIVE)
307                 # if any of the active flags exist in the active data then
308                 # the user is active
309                 if (len(active_data-active_flags) < len(active_data)):
310                     user.is_active = True
311         else:
312             # LDAP_ACTIVE_FIELD not defined, all users are active
313             user.is_active = True
314         user.save()