Ticket #2507: ldapauth.py

File ldapauth.py, 12.8 KB (added by Rozza, 16 years ago)

See comments for ldapauth.py

Line 
1from django.conf import settings
2from django.contrib.auth.models import User
3
4import logging
5
6class 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()
Back to Top