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()
|
---|