Ticket #11526: auth_ldap.diff
File auth_ldap.diff, 104.6 KB (added by , 15 years ago) |
---|
-
django/contrib/auth/contrib/ldap/config.py
1 """ 2 This module contains classes that will be needed for configuration of LDAP 3 authentication. Unlike backend.py, this is safe to import into settings.py. 4 Please see the docstring on the backend module for more information, including 5 notes on naming conventions. 6 """ 7 8 try: 9 set 10 except NameError: 11 from sets import Set as set # Python 2.3 fallback 12 13 import logging 14 import pprint 15 16 17 class _LDAPConfig(object): 18 """ 19 A private class that loads and caches some global objects. 20 """ 21 ldap = None 22 logger = None 23 24 def get_ldap(cls): 25 """ 26 Returns the ldap module. The unit test harness will assign a mock object 27 to _LDAPConfig.ldap. It is imperative that the ldap module not be 28 imported anywhere else so that the unit tests will pass in the absence 29 of python-ldap. 30 """ 31 if cls.ldap is None: 32 import ldap 33 import ldap.filter 34 import ldap.dn 35 36 cls.ldap = ldap 37 38 return cls.ldap 39 get_ldap = classmethod(get_ldap) 40 41 def get_logger(cls): 42 """ 43 Initializes and returns our logger instance. 44 """ 45 if cls.logger is None: 46 class NullHandler(logging.Handler): 47 def emit(self, record): 48 pass 49 50 cls.logger = logging.getLogger('django.contrib.auth.contrib.ldap') 51 cls.logger.addHandler(NullHandler()) 52 cls.logger.setLevel(logging.DEBUG) 53 54 return cls.logger 55 get_logger = classmethod(get_logger) 56 57 58 # Our global logger 59 logger = _LDAPConfig.get_logger() 60 61 62 class LDAPSearch(object): 63 """ 64 Public class that holds a set of LDAP search parameters. Objects of this 65 class should be considered immutable. Only the initialization method is 66 documented for configuration purposes. Internal clients may use the other 67 methods to refine and execute the search. 68 """ 69 def __init__(self, base_dn, scope, filterstr=u'(objectClass=*)'): 70 """ 71 These parameters are the same as the first three parameters to 72 ldap.search_s. 73 """ 74 self.base_dn = base_dn 75 self.scope = scope 76 self.filterstr = filterstr 77 self.ldap = _LDAPConfig.get_ldap() 78 79 def search_with_additional_terms(self, term_dict, escape=True): 80 """ 81 Returns a new search object with additional search terms and-ed to the 82 filter string. term_dict maps attribute names to assertion values. If 83 you don't want the values escaped, pass escape=False. 84 """ 85 term_strings = [self.filterstr] 86 87 for name, value in term_dict.iteritems(): 88 if escape: 89 value = self.ldap.filter.escape_filter_chars(value) 90 term_strings.append(u'(%s=%s)' % (name, value)) 91 92 filterstr = u'(&%s)' % ''.join(term_strings) 93 94 return self.__class__(self.base_dn, self.scope, filterstr) 95 96 def search_with_additional_term_string(self, filterstr): 97 """ 98 Returns a new search object with filterstr and-ed to the original filter 99 string. The caller is responsible for passing in a properly escaped 100 string. 101 """ 102 filterstr = u'(&%s%s)' % (self.filterstr, filterstr) 103 104 return self.__class__(self.base_dn, self.scope, filterstr) 105 106 def execute(self, connection, filterargs=()): 107 """ 108 Executes the search on the given connection (an LDAPObject). filterargs 109 is an object that will be used for expansion of the filter string. 110 111 The python-ldap library returns utf8-encoded strings. For the sake of 112 sanity, this method will decode all result strings and return them as 113 Unicode. 114 """ 115 try: 116 filterstr = self.filterstr % filterargs 117 results = connection.search_s(self.base_dn.encode('utf-8'), 118 self.scope, filterstr.encode('utf-8')) 119 results = _DeepStringCoder('utf-8').decode(results) 120 121 result_dns = [result[0] for result in results] 122 logger.debug(u"search_s('%s', %d, '%s') returned %d objects: %s" % 123 (self.base_dn, self.scope, filterstr, len(result_dns), "; ".join(result_dns))) 124 except self.ldap.LDAPError, e: 125 results = [] 126 logger.error(u"search_s('%s', %d, '%s') raised %s" % 127 (self.base_dn, self.scope, filterstr, pprint.pformat(e))) 128 129 return results 130 131 132 class _DeepStringCoder(object): 133 """ 134 Encodes and decodes strings in a nested structure of lists, tuples, and 135 dicts. This is helpful when interacting with the Unicode-unaware 136 python-ldap. 137 """ 138 def __init__(self, encoding): 139 self.encoding = encoding 140 141 def decode(self, value): 142 try: 143 if isinstance(value, str): 144 value = value.decode(self.encoding) 145 elif isinstance(value, list): 146 value = self._decode_list(value) 147 elif isinstance(value, tuple): 148 value = tuple(self._decode_list(value)) 149 elif isinstance(value, dict): 150 value = self._decode_dict(value) 151 except UnicodeDecodeError: 152 pass 153 154 return value 155 156 def _decode_list(self, value): 157 return [self.decode(v) for v in value] 158 159 def _decode_dict(self, value): 160 return dict([(self.decode(k), self.decode(v)) for k,v in value.iteritems()]) 161 162 163 class LDAPGroupType(object): 164 """ 165 This is an abstract base class for classes that determine LDAP group 166 membership. A group can mean many different things in LDAP, so we will need 167 a concrete subclass for each grouping mechanism. Clients may subclass this 168 if they have a group mechanism that is not handled by a built-in 169 implementation. 170 171 name_attr is the name of the LDAP attribute from which we will take the 172 Django group name. 173 174 Subclasses in this file must use self.ldap to access the python-ldap module. 175 This will be a mock object during unit tests. 176 """ 177 def __init__(self, name_attr="cn"): 178 self.name_attr = name_attr 179 self.ldap = _LDAPConfig.get_ldap() 180 181 def user_groups(self, ldap_user, group_search): 182 """ 183 Returns a list of group_info structures, each one a group to which 184 ldap_user belongs. group_search is an LDAPSearch object that returns all 185 of the groups that the user might belong to. Typical implementations 186 will apply additional filters to group_search and return the results of 187 the search. ldap_user represents the user and has the following three 188 properties: 189 190 dn: the distinguished name 191 attrs: a dictionary of LDAP attributes (with lists of values) 192 connection: an LDAPObject that has been bound with credentials 193 194 This is the primitive method in the API and must be implemented. 195 """ 196 return [] 197 198 def is_member(self, ldap_user, group_dn): 199 """ 200 This method is an optimization for determining group membership without 201 loading all of the user's groups. Subclasses that are able to do this 202 may return True or False. ldap_user is as above. group_dn is the 203 distinguished name of the group in question. 204 205 The base implementation returns None, which means we don't have enough 206 information. The caller will have to call user_groups() instead and look 207 for group_dn in the results. 208 """ 209 return None 210 211 def group_name_from_info(self, group_info): 212 """ 213 Given the (DN, attrs) 2-tuple of an LDAP group, this returns the name of 214 the Django group. This may return None to indicate that a particular 215 LDAP group has no corresponding Django group. 216 217 The base implementation returns the value of the cn attribute, or 218 whichever attribute was given to __init__ in the name_attr 219 parameter. 220 """ 221 try: 222 name = group_info[1][self.name_attr][0] 223 except (KeyError, IndexError): 224 name = None 225 226 return name 227 228 229 class PosixGroupType(LDAPGroupType): 230 """ 231 An LDAPGroupType subclass that handles groups of class posixGroup. 232 """ 233 def user_groups(self, ldap_user, group_search): 234 """ 235 Searches for any group that is either the user's primary or contains the 236 user as a member. 237 """ 238 groups = [] 239 240 try: 241 user_uid = ldap_user.attrs['uid'][0] 242 user_gid = ldap_user.attrs['gidNumber'][0] 243 244 filterstr = u'(|(gidNumber=%s)(memberUid=%s))' % ( 245 self.ldap.filter.escape_filter_chars(user_gid), 246 self.ldap.filter.escape_filter_chars(user_uid) 247 ) 248 249 search = group_search.search_with_additional_term_string(filterstr) 250 groups = search.execute(ldap_user.connection) 251 except (KeyError, IndexError): 252 pass 253 254 return groups 255 256 def is_member(self, ldap_user, group_dn): 257 """ 258 Returns True if the group is the user's primary group or if the user is 259 listed in the group's memberUid attribute. 260 """ 261 try: 262 user_uid = ldap_user.attrs['uid'][0] 263 user_gid = ldap_user.attrs['gidNumber'][0] 264 265 is_member = ldap_user.connection.compare_s(group_dn.encode('utf-8'), 'memberUid', user_uid.encode('utf-8')) 266 if not is_member: 267 is_member = ldap_user.connection.compare_s(group_dn.encode('utf-8'), 'gidNumber', user_gid.encode('utf-8')) 268 except (KeyError, IndexError): 269 is_member = False 270 271 return is_member 272 273 274 class MemberDNGroupType(LDAPGroupType): 275 """ 276 An abstract base class for group types that store lists of members as 277 distinguished names. Subclasses should set member_attr to define the member 278 attribute name. 279 """ 280 def __init__(self, member_attr, name_attr='cn'): 281 """ 282 member_attr is the attribute on the group object that holds the list of 283 member DNs. 284 """ 285 self.member_attr = member_attr 286 287 super(MemberDNGroupType, self).__init__(name_attr) 288 289 def user_groups(self, ldap_user, group_search): 290 search = group_search.search_with_additional_terms( 291 {self.member_attr: ldap_user.dn}) 292 groups = search.execute(ldap_user.connection) 293 294 return groups 295 296 def is_member(self, ldap_user, group_dn): 297 return ldap_user.connection.compare_s(group_dn.encode('utf-8'), 298 self.member_attr.encode('utf-8'), ldap_user.dn.encode('utf-8')) 299 300 301 class NestedMemberDNGroupType(LDAPGroupType): 302 """ 303 An abstract base class for group types that store lists of members as 304 distinguished names and support nested groups. There is no shortcut for 305 is_member in this case, so it's left unimplemented. 306 """ 307 def __init__(self, member_attr, name_attr='cn'): 308 """ 309 member_attr is the attribute on the group object that holds the list of 310 member DNs. 311 """ 312 self.member_attr = member_attr 313 314 super(NestedMemberDNGroupType, self).__init__(name_attr) 315 316 def user_groups(self, ldap_user, group_search): 317 """ 318 This searches for all of a user's groups from the bottom up. In other 319 words, it returns the groups that the user belongs to, the groups that 320 those groups belong to, etc. Circular references will be detected and 321 pruned. 322 """ 323 group_info_map = {} # Maps group_dn to group_info of groups we've found 324 member_dn_set = set([ldap_user.dn]) # Member DNs to search with next 325 handled_dn_set = set() # Member DNs that we've already searched with 326 327 while len(member_dn_set) > 0: 328 group_infos = self.find_groups_with_any_member(member_dn_set, 329 group_search, ldap_user.connection) 330 new_group_info_map = dict([(info[0], info) for info in group_infos]) 331 group_info_map.update(new_group_info_map) 332 handled_dn_set.update(member_dn_set) 333 334 # Get ready for the next iteration. To avoid cycles, we make sure 335 # never to search with the same member DN twice. 336 member_dn_set = set(new_group_info_map.keys()) - handled_dn_set 337 338 return group_info_map.values() 339 340 def find_groups_with_any_member(self, member_dn_set, group_search, connection): 341 terms = [ 342 u"(%s=%s)" % (self.member_attr, self.ldap.filter.escape_filter_chars(dn)) 343 for dn in member_dn_set 344 ] 345 346 filterstr = u"(|%s)" % "".join(terms) 347 search = group_search.search_with_additional_term_string(filterstr) 348 349 return search.execute(connection) 350 351 352 class GroupOfNamesType(MemberDNGroupType): 353 """ 354 An LDAPGroupType subclass that handles groups of class groupOfNames. 355 """ 356 def __init__(self, name_attr='cn'): 357 super(GroupOfNamesType, self).__init__('member', name_attr) 358 359 360 class NestedGroupOfNamesType(NestedMemberDNGroupType): 361 """ 362 An LDAPGroupType subclass that handles groups of class groupOfNames with 363 nested group references. 364 """ 365 def __init__(self, name_attr='cn'): 366 super(NestedGroupOfNamesType, self).__init__('member', name_attr) 367 368 369 class GroupOfUniqueNamesType(MemberDNGroupType): 370 """ 371 An LDAPGroupType subclass that handles groups of class groupOfUniqueNames. 372 """ 373 def __init__(self, name_attr='cn'): 374 super(GroupOfUniqueNamesType, self).__init__('uniqueMember', name_attr) 375 376 377 class NestedGroupOfUniqueNamesType(NestedMemberDNGroupType): 378 """ 379 An LDAPGroupType subclass that handles groups of class groupOfUniqueNames 380 with nested group references. 381 """ 382 def __init__(self, name_attr='cn'): 383 super(NestedGroupOfUniqueNamesType, self).__init__('uniqueMember', name_attr) 384 385 386 class ActiveDirectoryGroupType(MemberDNGroupType): 387 """ 388 An LDAPGroupType subclass that handles Active Directory groups. 389 """ 390 def __init__(self, name_attr='cn'): 391 super(ActiveDirectoryGroupType, self).__init__('member', name_attr) 392 393 394 class NestedActiveDirectoryGroupType(NestedMemberDNGroupType): 395 """ 396 An LDAPGroupType subclass that handles Active Directory groups with nested 397 group references. 398 """ 399 def __init__(self, name_attr='cn'): 400 super(NestedActiveDirectoryGroupType, self).__init__('member', name_attr) -
django/contrib/auth/contrib/ldap/backend.py
1 """ 2 LDAP authentication backend 3 4 Complete documentation can be found in docs/howto/auth-ldap.txt (or the thing it 5 compiles to). 6 7 Use of this backend requires the python-ldap module. To support unit tests, we 8 import ldap in a single centralized place (config._LDAPConfig) so that the test 9 harness can insert a mock object. 10 11 A few notes on naming conventions. If an identifier ends in _dn, it is a string 12 representation of a distinguished name. If it ends in _info, it is a 2-tuple 13 containing a DN and a dictionary of lists of attributes. ldap.search_s returns a 14 list of such structures. An identifier that ends in _attrs is the dictionary of 15 attributes from the _info structure. 16 17 A connection is an LDAPObject that has been successfully bound with a DN and 18 password. The identifier 'user' always refers to a User model object; LDAP user 19 information will be user_dn or user_info. 20 21 Additional classes can be found in the config module next to this one. 22 """ 23 24 try: 25 set 26 except NameError: 27 from sets import Set as set # Python 2.3 fallback 28 29 import pprint 30 31 import django.db 32 from django.contrib.auth.models import User, Group, SiteProfileNotAvailable 33 from django.contrib.auth.contrib.ldap.config import _LDAPConfig, LDAPSearch 34 from django.core.cache import cache 35 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist 36 37 38 logger = _LDAPConfig.get_logger() 39 40 41 class LDAPBackend(object): 42 """ 43 The main backend class. This implements the auth backend API, although it 44 actually delegates most of its work to _LDAPUser, which is defined next. 45 """ 46 ldap = None # The cached ldap module (or mock object) 47 48 def __init__(self): 49 self.ldap = self.ldap_module() 50 51 def ldap_module(cls): 52 """ 53 Requests the ldap module from _LDAPConfig. Under a test harness, this 54 will be a mock object. We only do this once because this is where we 55 apply AUTH_LDAP_GLOBAL_OPTIONS. 56 """ 57 if cls.ldap is None: 58 cls.ldap = _LDAPConfig.get_ldap() 59 60 for opt, value in ldap_settings.AUTH_LDAP_GLOBAL_OPTIONS.iteritems(): 61 cls.ldap.set_option(opt, value) 62 63 return cls.ldap 64 ldap_module = classmethod(ldap_module) 65 66 67 # 68 # The Django auth backend API 69 # 70 71 def authenticate(self, username, password): 72 ldap_user = _LDAPUser(self, username=username) 73 user = ldap_user.authenticate(password) 74 75 return user 76 77 def get_user(self, user_id): 78 user = None 79 80 try: 81 user = User.objects.get(pk=user_id) 82 _LDAPUser(self, user=user) # This sets user.ldap_user 83 except User.DoesNotExist: 84 pass 85 86 return user 87 88 def has_perm(self, user, perm): 89 return perm in self.get_all_permissions(user) 90 91 def has_module_perms(self, user, app_label): 92 for perm in self.get_all_permissions(user): 93 if perm[:perm.index('.')] == app_label: 94 return True 95 96 return False 97 98 def get_all_permissions(self, user): 99 return self.get_group_permissions(user) 100 101 def get_group_permissions(self, user): 102 if hasattr(user, 'ldap_user'): 103 return user.ldap_user.get_group_permissions() 104 else: 105 return set() 106 107 # 108 # Hooks for subclasses 109 # 110 111 def ldap_to_django_username(self, username): 112 return username 113 114 def django_to_ldap_username(self, username): 115 return username 116 117 118 class _LDAPUser(object): 119 """ 120 Represents an LDAP user and ultimately fields all requests that the 121 backend receives. This class exists for two reasons. First, it's 122 convenient to have a separate object for each request so that we can use 123 object attributes without running into threading problems. Second, these 124 objects get attached to the User objects, which allows us to cache 125 expensive LDAP information, especially around groups and permissions. 126 127 self.backend is a reference back to the LDAPBackend instance, which we need 128 to access the ldap module and any hooks that a subclass has overridden. 129 """ 130 class AuthenticationFailed(Exception): 131 pass 132 133 # 134 # Initialization 135 # 136 137 def __init__(self, backend, username=None, user=None): 138 """ 139 A new LDAPUser must be initialized with either a username or an 140 authenticated User object. If a user is given, the username will be 141 ignored. 142 """ 143 self.backend = backend 144 self.ldap = backend.ldap_module() 145 self._username = username 146 self._password = None 147 self._user_dn = None 148 self._user_attrs = None 149 self._user = None 150 self._groups = None 151 self._group_permissions = None 152 self._connection = None 153 self._connection_bound = False 154 155 if user is not None: 156 self._set_authenticated_user(user) 157 158 if username is None and user is None: 159 raise Exception("Internal error: _LDAPUser improperly initialized.") 160 161 def _set_authenticated_user(self, user): 162 self._user = user 163 self._username = self.backend.django_to_ldap_username(user.username) 164 165 user.ldap_user = self 166 user.ldap_username = self._username 167 168 # 169 # Entry points 170 # 171 172 def authenticate(self, password): 173 """ 174 Authenticates against the LDAP directory and returns the corresponding 175 User object if successful. Returns None on failure. 176 """ 177 user = None 178 179 self._password = password 180 181 try: 182 self._authenticate_user_dn() 183 self._check_requirements() 184 self._get_or_create_user() 185 186 user = self._user 187 except self.AuthenticationFailed, e: 188 logger.debug(u"Authentication failed for %s" % self._username) 189 except self.ldap.LDAPError, e: 190 logger.warning(u"Caught LDAPError while authenticating %s: %s", 191 self._username, pprint.pformat(e)) 192 except Exception, e: 193 logger.warning(u"Caught Exception while authenticating %s: %s", 194 self._username, pprint.pformat(e)) 195 raise 196 197 return user 198 199 def get_group_permissions(self): 200 """ 201 If allowed by the configuration, this returns the set of permissions 202 defined by the user's LDAP group memberships. 203 """ 204 if self._group_permissions is None: 205 self._group_permissions = set() 206 207 if ldap_settings.AUTH_LDAP_FIND_GROUP_PERMS: 208 try: 209 self._load_group_permissions() 210 except self.ldap.LDAPError, e: 211 logger.warning("Caught LDAPError loading group permissions: %s", 212 pprint.pformat(e)) 213 214 return self._group_permissions 215 216 # 217 # Public properties (callbacks). These are all lazy for performance reasons. 218 # 219 220 def _get_user_dn(self): 221 if self._user_dn is None: 222 self._load_user_dn() 223 224 return self._user_dn 225 dn = property(_get_user_dn) 226 227 def _get_user_attrs(self): 228 if self._user_attrs is None: 229 self._load_user_attrs() 230 231 return self._user_attrs 232 attrs = property(_get_user_attrs) 233 234 def _get_bound_connection(self): 235 if not self._connection_bound: 236 self._bind() 237 238 return self._get_connection() 239 connection = property(_get_bound_connection) 240 241 # 242 # Authentication 243 # 244 245 def _authenticate_user_dn(self): 246 """ 247 Binds to the LDAP server with the user's DN and password. Raises 248 AuthenticationFailed on failure. 249 """ 250 try: 251 self._bind_as(self.dn, self._password) 252 except self.ldap.INVALID_CREDENTIALS: 253 raise self.AuthenticationFailed("User DN/password rejected by LDAP server.") 254 255 def _load_user_attrs(self): 256 search = LDAPSearch(self.dn, self.ldap.SCOPE_BASE) 257 results = search.execute(self.connection) 258 259 self._user_attrs = results[0][1] 260 261 def _load_user_dn(self): 262 """ 263 Returns the (cached) distinguished name of our user. This will 264 ultimately either construct the DN from a template in 265 AUTH_LDAP_USER_DN_TEMPLATE or connect to the server and search for it. 266 This may result in an AuthenticationFailed exception if we do not get 267 satisfactory results searching for the user's DN. 268 """ 269 if self._using_simple_bind_mode(): 270 self._construct_simple_user_dn() 271 else: 272 self._search_for_user_dn() 273 274 def _using_simple_bind_mode(self): 275 return (ldap_settings.AUTH_LDAP_USER_DN_TEMPLATE is not None) 276 277 def _construct_simple_user_dn(self): 278 template = ldap_settings.AUTH_LDAP_USER_DN_TEMPLATE 279 username = self.ldap.dn.escape_dn_chars(self._username) 280 281 self._user_dn = template % {'user': username} 282 283 def _search_for_user_dn(self): 284 """ 285 Searches the directory for a user matching AUTH_LDAP_USER_SEARCH. 286 Populates self._user_dn and self._user_attrs. 287 """ 288 search = ldap_settings.AUTH_LDAP_USER_SEARCH 289 if search is None: 290 raise ImproperlyConfigured('AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance.') 291 292 results = search.execute(self.connection, {'user': self._username}) 293 if results is None or len(results) != 1: 294 raise self.AuthenticationFailed("AUTH_LDAP_USER_SEARCH failed to return exactly one result.") 295 296 (self._user_dn, self._user_attrs) = results[0] 297 298 def _check_requirements(self): 299 """ 300 Checks all authentication requirements beyond credentials. Raises 301 AuthenticationFailed on failure. 302 """ 303 self._check_required_group() 304 305 def _check_required_group(self): 306 """ 307 Returns True if the group requirement (AUTH_LDAP_REQUIRE_GROUP) is 308 met. Always returns True if AUTH_LDAP_REQUIRE_GROUP is None. 309 """ 310 required_group_dn = ldap_settings.AUTH_LDAP_REQUIRE_GROUP 311 312 if required_group_dn is not None: 313 is_member = self._get_groups().is_member_of(required_group_dn) 314 if not is_member: 315 raise self.AuthenticationFailed("User is not a member of AUTH_LDAP_REQUIRE_GROUP") 316 317 # 318 # User management 319 # 320 321 def _get_or_create_user(self): 322 """ 323 Loads the User model object from the database or creates it if it 324 doesn't exist. Also populates the fields, subject to 325 AUTH_LDAP_ALWAYS_UPDATE_USER. 326 """ 327 save_user = False 328 329 username = self.backend.ldap_to_django_username(self._username) 330 331 (self._user, created) = User.objects.get_or_create(username=username) 332 333 if created: 334 logger.debug("Created Django user %s", username) 335 self._user.set_unusable_password() 336 save_user = True 337 338 if(ldap_settings.AUTH_LDAP_ALWAYS_UPDATE_USER or created): 339 logger.debug("Populating Django user %s", username) 340 self._populate_user() 341 self._populate_and_save_user_profile() 342 save_user = True 343 344 if ldap_settings.AUTH_LDAP_MIRROR_GROUPS: 345 self._mirror_groups() 346 347 if save_user: 348 self._user.save() 349 350 self._user.ldap_user = self 351 self._user.ldap_username = self._username 352 353 def _populate_user(self): 354 """ 355 Populates our User object with information from the LDAP directory. 356 """ 357 self._populate_user_from_attributes() 358 self._populate_user_from_group_memberships() 359 360 def _populate_user_from_attributes(self): 361 for field, attr in ldap_settings.AUTH_LDAP_USER_ATTR_MAP.iteritems(): 362 try: 363 setattr(self._user, field, self.attrs[attr][0]) 364 except (KeyError, IndexError): 365 pass 366 367 def _populate_user_from_group_memberships(self): 368 for field, group_dn in ldap_settings.AUTH_LDAP_USER_FLAGS_BY_GROUP.iteritems(): 369 value = self._get_groups().is_member_of(group_dn) 370 setattr(self._user, field, value) 371 372 def _populate_and_save_user_profile(self): 373 """ 374 Populates a User profile object with fields from the LDAP directory. 375 """ 376 try: 377 profile = self._user.get_profile() 378 379 for field, attr in ldap_settings.AUTH_LDAP_PROFILE_ATTR_MAP.iteritems(): 380 try: 381 # user_attrs is a hash of lists of attribute values 382 setattr(profile, field, self.attrs[attr][0]) 383 except (KeyError, IndexError): 384 pass 385 386 if len(ldap_settings.AUTH_LDAP_PROFILE_ATTR_MAP) > 0: 387 profile.save() 388 except (SiteProfileNotAvailable, ObjectDoesNotExist): 389 pass 390 391 def _mirror_groups(self): 392 """ 393 Mirrors the user's LDAP groups in the Django database and updates the 394 user's membership. 395 """ 396 group_names = self._get_groups().get_group_names() 397 groups = [Group.objects.get_or_create(name=group_name)[0] for group_name 398 in group_names] 399 400 self._user.groups = groups 401 402 # 403 # Group information 404 # 405 406 def _load_group_permissions(self): 407 """ 408 Populates self._group_permissions based on LDAP group membership and 409 Django group permissions. 410 411 The SQL is lifted (with modifications) from ModelBackend. 412 """ 413 group_names = self._get_groups().get_group_names() 414 placeholders = ', '.join(['%s'] * len(group_names)) 415 416 cursor = django.db.connection.cursor() 417 # The SQL below works out to the following, after DB quoting: 418 # cursor.execute(""" 419 # SELECT ct."app_label", p."codename" 420 # FROM "auth_permission" p, "auth_group_permissions" gp, "auth_group" g, "django_content_type" ct 421 # WHERE p."id" = gp."permission_id" 422 # AND gp."group_id" = g."id" 423 # AND ct."id" = p."content_type_id" 424 # AND g."name" IN (%s, %s, ...)""", ['group1', 'group2', ...]) 425 qn = django.db.connection.ops.quote_name 426 sql = u""" 427 SELECT ct.%s, p.%s 428 FROM %s p, %s gp, %s g, %s ct 429 WHERE p.%s = gp.%s 430 AND gp.%s = g.%s 431 AND ct.%s = p.%s 432 AND g.%s IN (%s)""" % ( 433 qn('app_label'), qn('codename'), 434 qn('auth_permission'), qn('auth_group_permissions'), 435 qn('auth_group'), qn('django_content_type'), 436 qn('id'), qn('permission_id'), 437 qn('group_id'), qn('id'), 438 qn('id'), qn('content_type_id'), 439 qn('name'), placeholders) 440 441 cursor.execute(sql, group_names) 442 self._group_permissions = \ 443 set([u"%s.%s" % (row[0], row[1]) for row in cursor.fetchall()]) 444 445 def _get_groups(self): 446 """ 447 Returns an _LDAPUserGroups object, which can determine group 448 membership. 449 """ 450 if self._groups is None: 451 self._groups = _LDAPUserGroups(self) 452 453 return self._groups 454 455 # 456 # LDAP connection 457 # 458 459 def _bind(self): 460 """ 461 Binds to the LDAP server with AUTH_LDAP_BIND_DN and 462 AUTH_LDAP_BIND_PASSWORD. 463 """ 464 self._bind_as(ldap_settings.AUTH_LDAP_BIND_DN, 465 ldap_settings.AUTH_LDAP_BIND_PASSWORD) 466 467 def _bind_as(self, bind_dn, bind_password): 468 """ 469 Binds to the LDAP server with the given credentials. This does not trap 470 exceptions. 471 """ 472 self._get_connection().simple_bind_s(bind_dn.encode('utf-8'), 473 bind_password.encode('utf-8')) 474 self._connection_bound = True 475 476 def _get_connection(self): 477 """ 478 Returns our cached LDAPObject, which may or may not be bound. 479 """ 480 if self._connection is None: 481 self._connection = self.ldap.initialize(ldap_settings.AUTH_LDAP_SERVER_URI) 482 483 for opt, value in ldap_settings.AUTH_LDAP_CONNECTION_OPTIONS.iteritems(): 484 self._connection.set_option(opt, value) 485 486 return self._connection 487 488 489 490 class _LDAPUserGroups(object): 491 """ 492 Represents the set of groups that a user belongs to. 493 """ 494 def __init__(self, ldap_user): 495 self._ldap_user = ldap_user 496 self._group_type = None 497 self._group_search = None 498 self._group_infos = None 499 self._group_dns = None 500 self._group_names = None 501 502 self._init_group_settings() 503 504 def _init_group_settings(self): 505 """ 506 Loads the settings we need to deal with groups. Raises 507 ImproperlyConfigured if anything's not right. 508 """ 509 self._group_type = ldap_settings.AUTH_LDAP_GROUP_TYPE 510 if self._group_type is None: 511 raise ImproperlyConfigured("AUTH_LDAP_GROUP_TYPE must be an LDAPGroupType instance.") 512 513 self._group_search = ldap_settings.AUTH_LDAP_GROUP_SEARCH 514 if self._group_search is None: 515 raise ImproperlyConfigured("AUTH_LDAP_GROUP_SEARCH must be an LDAPSearch instance.") 516 517 def get_group_names(self): 518 """ 519 Returns the list of Django group names that this user belongs to by 520 virtue of LDAP group memberships. 521 """ 522 if self._group_names is None: 523 self._load_cached_attr("_group_names") 524 525 if self._group_names is None: 526 group_infos = self._get_group_infos() 527 self._group_names = [self._group_type.group_name_from_info(group_info) 528 for group_info in group_infos] 529 self._cache_attr("_group_names") 530 531 return self._group_names 532 533 def is_member_of(self, group_dn): 534 """ 535 Returns true if our user is a member of the given group. 536 """ 537 is_member = None 538 539 # If we have self._group_dns, we'll use it. Otherwise, we'll try to 540 # avoid the cost of loading it. 541 if self._group_dns is None: 542 is_member = self._group_type.is_member(self._ldap_user, group_dn) 543 544 if is_member is None: 545 is_member = (group_dn in self._get_group_dns()) 546 547 logger.debug("%s is%sa member of %s", self._ldap_user.dn, 548 is_member and " " or " not ", group_dn) 549 550 return is_member 551 552 def _get_group_dns(self): 553 """ 554 Returns a (cached) set of the distinguished names in self._group_infos. 555 """ 556 if self._group_dns is None: 557 group_infos = self._get_group_infos() 558 self._group_dns = set([group_info[0] for group_info in group_infos]) 559 560 return self._group_dns 561 562 def _get_group_infos(self): 563 """ 564 Returns a (cached) list of group_info structures for the groups that our 565 user is a member of. 566 """ 567 if self._group_infos is None: 568 self._group_infos = self._group_type.user_groups(self._ldap_user, 569 self._group_search) 570 571 return self._group_infos 572 573 def _load_cached_attr(self, attr_name): 574 if ldap_settings.AUTH_LDAP_CACHE_GROUPS: 575 key = self._cache_key(attr_name) 576 value = cache.get(key) 577 setattr(self, attr_name, value) 578 579 def _cache_attr(self, attr_name): 580 if ldap_settings.AUTH_LDAP_CACHE_GROUPS: 581 key = self._cache_key(attr_name) 582 value = getattr(self, attr_name, None) 583 cache.set(key, value, ldap_settings.AUTH_LDAP_GROUP_CACHE_TIMEOUT) 584 585 def _cache_key(self, attr_name): 586 return u'auth_ldap.%s.%s.%s' % (self.__class__.__name__, attr_name, self._ldap_user.dn) 587 588 589 class LDAPSettings(object): 590 """ 591 This is a simple class to take the place of the global settings object. An 592 instance will contain all of our settings as attributes, with default values 593 if they are not specified by the configuration. 594 """ 595 defaults = { 596 'AUTH_LDAP_ALWAYS_UPDATE_USER': True, 597 'AUTH_LDAP_BIND_DN': '', 598 'AUTH_LDAP_BIND_PASSWORD': '', 599 'AUTH_LDAP_CACHE_GROUPS': False, 600 'AUTH_LDAP_CONNECTION_OPTIONS': {}, 601 'AUTH_LDAP_FIND_GROUP_PERMS': False, 602 'AUTH_LDAP_GLOBAL_OPTIONS': {}, 603 'AUTH_LDAP_GROUP_CACHE_TIMEOUT': None, 604 'AUTH_LDAP_GROUP_SEARCH': None, 605 'AUTH_LDAP_GROUP_TYPE': None, 606 'AUTH_LDAP_MIRROR_GROUPS': False, 607 'AUTH_LDAP_PROFILE_ATTR_MAP': {}, 608 'AUTH_LDAP_REQUIRE_GROUP': None, 609 'AUTH_LDAP_SERVER_URI': 'ldap://localhost', 610 'AUTH_LDAP_USER_ATTR_MAP': {}, 611 'AUTH_LDAP_USER_DN_TEMPLATE': None, 612 'AUTH_LDAP_USER_FLAGS_BY_GROUP': {}, 613 'AUTH_LDAP_USER_SEARCH': None, 614 } 615 616 def __init__(self): 617 """ 618 Loads our settings from django.conf.settings, applying defaults for any 619 that are omitted. 620 """ 621 from django.conf import settings 622 623 for name, default in self.defaults.iteritems(): 624 value = getattr(settings, name, default) 625 setattr(self, name, value) 626 627 628 # Our global settings object 629 ldap_settings = LDAPSettings() -
django/contrib/auth/tests/__init__.py
4 4 from django.contrib.auth.tests.forms import FORM_TESTS 5 5 from django.contrib.auth.tests.remote_user \ 6 6 import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest 7 from django.contrib.auth.tests.ldap import LDAPTest 7 8 from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS 8 9 9 10 # The password for the fixture data users is 'password' -
django/contrib/auth/tests/ldap.py
1 # coding: utf-8 2 3 try: 4 set 5 except NameError: 6 from sets import Set as set # Python 2.3 fallback 7 8 import logging 9 import sys 10 11 from django.contrib.auth.contrib.ldap import backend 12 from django.contrib.auth.contrib.ldap.config import _LDAPConfig, LDAPSearch 13 from django.contrib.auth.contrib.ldap.config import PosixGroupType, MemberDNGroupType, NestedMemberDNGroupType 14 from django.contrib.auth.contrib.ldap.config import GroupOfNamesType, NestedGroupOfNamesType 15 from django.contrib.auth.contrib.ldap.config import GroupOfUniqueNamesType, NestedGroupOfUniqueNamesType 16 from django.contrib.auth.contrib.ldap.config import ActiveDirectoryGroupType, NestedActiveDirectoryGroupType 17 from django.contrib.auth.models import User, Permission, Group 18 from django.test import TestCase 19 20 21 class TestSettings(backend.LDAPSettings): 22 """ 23 A replacement for backend.LDAPSettings that does not load settings 24 from django.conf. 25 """ 26 def __init__(self, **kwargs): 27 for name, default in self.defaults.iteritems(): 28 value = kwargs.get(name, default) 29 setattr(self, name, value) 30 31 32 class MockLDAP(object): 33 """ 34 This is a stand-in for the python-ldap module; it serves as both the ldap 35 module and the LDAPObject class. While it's temping to add some real LDAP 36 capabilities here, this is designed to remain as simple as possible, so as 37 to minimize the risk of creating bogus unit tests through a buggy test 38 harness. 39 40 Simple operations can be simulated, but for nontrivial searches, the client 41 will have to seed the mock object with return values for expected API calls. 42 This may sound like cheating, but it's really no more so than a simulated 43 LDAP server. The fact is we can not require python-ldap to be installed in 44 order to run the unit tests, so all we can do is verify that LDAPBackend is 45 calling the APIs that we expect. 46 47 set_return_value takes the name of an API, a tuple of arguments, and a 48 return value. Every time an API is called, it looks for a predetermined 49 return value based on the arguments received. If it finds one, then it 50 returns it, or raises it if it's an Exception. If it doesn't find one, then 51 it tries to satisfy the request internally. If it can't, it raises a 52 PresetReturnRequiredError. 53 54 At any time, the client may call ldap_methods_called_with_arguments() or 55 ldap_methods_called() to get a record of all of the LDAP API calls that have 56 been made, with or without arguments. 57 """ 58 59 class PresetReturnRequiredError(Exception): pass 60 61 SCOPE_BASE = 0 62 SCOPE_ONELEVEL = 1 63 SCOPE_SUBTREE = 2 64 65 class LDAPError(Exception): pass 66 class INVALID_CREDENTIALS(LDAPError): pass 67 class NO_SUCH_OBJECT(LDAPError): pass 68 69 # 70 # Submodules 71 # 72 class dn(object): 73 def escape_dn_chars(s): 74 return s 75 escape_dn_chars = staticmethod(escape_dn_chars) 76 77 class filter(object): 78 def escape_filter_chars(s): 79 return s 80 escape_filter_chars = staticmethod(escape_filter_chars) 81 82 83 def __init__(self, directory): 84 """ 85 directory is a complex structure with the entire contents of the 86 mock LDAP directory. directory must be a dictionary mapping 87 distinguished names to dictionaries of attributes. Each attribute 88 dictionary maps attribute names to lists of values. e.g.: 89 90 { 91 "uid=alice,ou=users,dc=example,dc=com": 92 { 93 "uid": ["alice"], 94 "userPassword": ["secret"], 95 }, 96 } 97 """ 98 self.directory = directory 99 100 self.reset() 101 102 def reset(self): 103 """ 104 Resets our recorded API calls and queued return values as well as 105 miscellaneous configuration options. 106 """ 107 self.calls = [] 108 self.return_value_maps = {} 109 self.options = {} 110 111 def set_return_value(self, api_name, arguments, value): 112 """ 113 Stores a preset return value for a given API with a given set of 114 arguments. 115 """ 116 self.return_value_maps.setdefault(api_name, {})[arguments] = value 117 118 def ldap_methods_called_with_arguments(self): 119 """ 120 Returns a list of 2-tuples, one for each API call made since the last 121 reset. Each tuple contains the name of the API and a dictionary of 122 arguments. Argument defaults are included. 123 """ 124 return self.calls 125 126 def ldap_methods_called(self): 127 """ 128 Returns the list of API names called. 129 """ 130 return [call[0] for call in self.calls] 131 132 # 133 # Begin LDAP methods 134 # 135 136 def set_option(self, option, invalue): 137 self._record_call('set_option', { 138 'option': option, 139 'invalue': invalue 140 }) 141 142 self.options[option] = invalue 143 144 def initialize(self, uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None): 145 self._record_call('initialize', { 146 'uri': uri, 147 'trace_level': trace_level, 148 'trace_file': trace_file, 149 'trace_stack_limit': trace_stack_limit 150 }) 151 152 value = self._get_return_value('initialize', 153 (uri, trace_level, trace_file, trace_stack_limit)) 154 if value is None: 155 value = self 156 157 return value 158 159 def simple_bind_s(self, who='', cred=''): 160 self._record_call('simple_bind_s', { 161 'who': who, 162 'cred': cred 163 }) 164 165 value = self._get_return_value('simple_bind_s', (who, cred)) 166 if value is None: 167 value = self._simple_bind_s(who, cred) 168 169 return value 170 171 def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0): 172 self._record_call('search_s', { 173 'base': base, 174 'scope': scope, 175 'filterstr':filterstr, 176 'attrlist':attrlist, 177 'attrsonly':attrsonly 178 }) 179 180 value = self._get_return_value('search_s', 181 (base, scope, filterstr, attrlist, attrsonly)) 182 if value is None: 183 value = self._search_s(base, scope, filterstr, attrlist, attrsonly) 184 185 return value 186 187 def compare_s(self, dn, attr, value): 188 self._record_call('compare_s', { 189 'dn': dn, 190 'attr': attr, 191 'value': value 192 }) 193 194 result = self._get_return_value('compare_s', (dn, attr, value)) 195 if result is None: 196 result = self._compare_s(dn, attr, value) 197 198 # print "compare_s('%s', '%s', '%s'): %d" % (dn, attr, value, result) 199 200 return result 201 202 # 203 # Internal implementations 204 # 205 206 def _simple_bind_s(self, who='', cred=''): 207 success = False 208 209 if(who == '' and cred == ''): 210 success = True 211 elif self._compare_s(who, 'userPassword', cred): 212 success = True 213 214 if success: 215 return (97, []) # python-ldap returns this; I don't know what it means 216 else: 217 raise self.INVALID_CREDENTIALS('%s:%s' % (who, cred)) 218 219 def _compare_s(self, dn, attr, value): 220 try: 221 found = (value in self.directory[dn][attr]) 222 except KeyError: 223 found = False 224 225 return found and 1 or 0 226 227 def _search_s(self, base, scope, filterstr, attrlist, attrsonly): 228 """ 229 We can do a SCOPE_BASE search with the default filter. Beyond that, 230 you're on your own. 231 """ 232 if scope != self.SCOPE_BASE: 233 raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' % 234 (base, scope, filterstr, attrlist, attrsonly)) 235 236 if filterstr != '(objectClass=*)': 237 raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' % 238 (base, scope, filterstr, attrlist, attrsonly)) 239 240 attrs = self.directory.get(base) 241 if attrs is None: 242 raise self.NO_SUCH_OBJECT() 243 244 return [(base, attrs)] 245 246 # 247 # Utils 248 # 249 250 def _record_call(self, api_name, arguments): 251 self.calls.append((api_name, arguments)) 252 253 def _get_return_value(self, api_name, arguments): 254 try: 255 value = self.return_value_maps[api_name][arguments] 256 except KeyError: 257 value = None 258 259 if isinstance(value, Exception): 260 raise value 261 262 return value 263 264 265 class LDAPTest(TestCase): 266 267 # Following are the objecgs in our mock LDAP directory 268 alice = ("uid=alice,ou=people,o=test", { 269 "uid": ["alice"], 270 "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"], 271 "userPassword": ["password"], 272 "uidNumber": ["1000"], 273 "gidNumber": ["1000"], 274 "givenName": ["Alice"], 275 "sn": ["Adams"] 276 }) 277 bob = ("uid=bob,ou=people,o=test", { 278 "uid": ["bob"], 279 "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"], 280 "userPassword": ["password"], 281 "uidNumber": ["1001"], 282 "gidNumber": ["50"], 283 "givenName": ["Robert"], 284 "sn": ["Barker"] 285 }) 286 dressler = (u"uid=dreßler,ou=people,o=test".encode('utf-8'), { 287 "uid": [u"dreßler".encode('utf-8')], 288 "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"], 289 "userPassword": ["password"], 290 "uidNumber": ["1002"], 291 "gidNumber": ["50"], 292 "givenName": ["Wolfgang"], 293 "sn": [u"Dreßler".encode('utf-8')] 294 }) 295 nobody = ("uid=nobody,ou=people,o=test", { 296 "uid": ["nobody"], 297 "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"], 298 "userPassword": ["password"], 299 "binaryAttr": ["\xb2"] # Invalid UTF-8 300 }) 301 302 # posixGroup objects 303 active_px = ("cn=active_px,ou=groups,o=test", { 304 "cn": ["active_px"], 305 "objectClass": ["posixGroup"], 306 "gidNumber": ["1000"], 307 }) 308 staff_px = ("cn=staff_px,ou=groups,o=test", { 309 "cn": ["staff_px"], 310 "objectClass": ["posixGroup"], 311 "gidNumber": ["1001"], 312 "memberUid": ["alice"], 313 }) 314 superuser_px = ("cn=superuser_px,ou=groups,o=test", { 315 "cn": ["superuser_px"], 316 "objectClass": ["posixGroup"], 317 "gidNumber": ["1002"], 318 "memberUid": ["alice"], 319 }) 320 321 # groupOfUniqueName groups 322 active_gon = ("cn=active_gon,ou=groups,o=test", { 323 "cn": ["active_gon"], 324 "objectClass": ["groupOfNames"], 325 "member": ["uid=alice,ou=people,o=test"] 326 }) 327 staff_gon = ("cn=staff_gon,ou=groups,o=test", { 328 "cn": ["staff_gon"], 329 "objectClass": ["groupOfNames"], 330 "member": ["uid=alice,ou=people,o=test"] 331 }) 332 superuser_gon = ("cn=superuser_gon,ou=groups,o=test", { 333 "cn": ["superuser_gon"], 334 "objectClass": ["groupOfNames"], 335 "member": ["uid=alice,ou=people,o=test"] 336 }) 337 338 # Nested groups with a circular reference 339 parent_gon = ("cn=parent_gon,ou=groups,o=test", { 340 "cn": ["parent_gon"], 341 "objectClass": ["groupOfNames"], 342 "member": ["cn=nested_gon,ou=groups,o=test"] 343 }) 344 nested_gon = ("cn=nested_gon,ou=groups,o=test", { 345 "cn": ["nested_gon"], 346 "objectClass": ["groupOfNames"], 347 "member": [ 348 "uid=alice,ou=people,o=test", 349 "cn=circular_gon,ou=groups,o=test" 350 ] 351 }) 352 circular_gon = ("cn=circular_gon,ou=groups,o=test", { 353 "cn": ["circular_gon"], 354 "objectClass": ["groupOfNames"], 355 "member": ["cn=parent_gon,ou=groups,o=test"] 356 }) 357 358 359 mock_ldap = MockLDAP({ 360 alice[0]: alice[1], 361 bob[0]: bob[1], 362 dressler[0]: dressler[1], 363 nobody[0]: nobody[1], 364 active_px[0]: active_px[1], 365 staff_px[0]: staff_px[1], 366 superuser_px[0]: superuser_px[1], 367 active_gon[0]: active_gon[1], 368 staff_gon[0]: staff_gon[1], 369 superuser_gon[0]: superuser_gon[1], 370 parent_gon[0]: parent_gon[1], 371 nested_gon[0]: nested_gon[1], 372 circular_gon[0]: circular_gon[1], 373 }) 374 375 376 logging_configured = False 377 def configure_logger(cls): 378 if not cls.logging_configured: 379 logger = logging.getLogger('django.contrib.auth.contrib.ldap') 380 formatter = logging.Formatter("LDAP auth - %(levelname)s - %(message)s") 381 handler = logging.StreamHandler() 382 383 handler.setLevel(logging.DEBUG) 384 handler.setFormatter(formatter) 385 logger.addHandler(handler) 386 387 logger.setLevel(logging.CRITICAL) 388 389 cls.logging_configured = True 390 configure_logger = classmethod(configure_logger) 391 392 393 def setUp(self): 394 self.configure_logger() 395 self.mock_ldap.reset() 396 397 _LDAPConfig.ldap = self.mock_ldap 398 self.backend = backend.LDAPBackend() 399 400 401 def tearDown(self): 402 pass 403 404 405 def test_options(self): 406 self._init_settings( 407 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 408 AUTH_LDAP_CONNECTION_OPTIONS={'opt1': 'value1'} 409 ) 410 411 user = self.backend.authenticate(username='alice', password='password') 412 413 self.assertEqual(self.mock_ldap.options, {'opt1': 'value1'}) 414 415 416 def test_simple_bind(self): 417 self._init_settings( 418 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' 419 ) 420 user_count = User.objects.count() 421 422 user = self.backend.authenticate(username='alice', password='password') 423 424 self.assert_(not user.has_usable_password()) 425 self.assertEqual(user.username, 'alice') 426 self.assertEqual(User.objects.count(), user_count + 1) 427 self.assertEqual(self.mock_ldap.ldap_methods_called(), 428 ['initialize', 'simple_bind_s']) 429 430 431 def test_simple_bind_bad_user(self): 432 self._init_settings( 433 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' 434 ) 435 user_count = User.objects.count() 436 437 user = self.backend.authenticate(username='evil_alice', password='password') 438 439 self.assert_(user is None) 440 self.assertEqual(User.objects.count(), user_count) 441 self.assertEqual(self.mock_ldap.ldap_methods_called(), 442 ['initialize', 'simple_bind_s']) 443 444 445 def test_simple_bind_bad_password(self): 446 self._init_settings( 447 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' 448 ) 449 user_count = User.objects.count() 450 451 user = self.backend.authenticate(username='alice', password='bogus') 452 453 self.assert_(user is None) 454 self.assertEqual(User.objects.count(), user_count) 455 self.assertEqual(self.mock_ldap.ldap_methods_called(), 456 ['initialize', 'simple_bind_s']) 457 458 459 def test_existing_user(self): 460 self._init_settings( 461 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' 462 ) 463 User.objects.create(username='alice') 464 user_count = User.objects.count() 465 466 user = self.backend.authenticate(username='alice', password='password') 467 468 # Make sure we only created one user 469 self.assert_(user is not None) 470 self.assertEqual(User.objects.count(), user_count) 471 472 473 def test_convert_username(self): 474 class MyBackend(backend.LDAPBackend): 475 def ldap_to_django_username(self, username): 476 return 'ldap_%s' % username 477 def django_to_ldap_username(self, username): 478 return username[5:] 479 480 self._init_settings( 481 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test' 482 ) 483 user_count = User.objects.count() 484 self.backend = MyBackend() 485 486 user1 = self.backend.authenticate(username='alice', password='password') 487 user2 = self.backend.get_user(user1.pk) 488 489 self.assertEqual(User.objects.count(), user_count + 1) 490 self.assertEqual(user1.username, 'ldap_alice') 491 self.assertEqual(user1.ldap_user._username, 'alice') 492 self.assertEqual(user1.ldap_username, 'alice') 493 self.assertEqual(user2.username, 'ldap_alice') 494 self.assertEqual(user2.ldap_user._username, 'alice') 495 self.assertEqual(user2.ldap_username, 'alice') 496 497 498 def test_search_bind(self): 499 self._init_settings( 500 AUTH_LDAP_USER_SEARCH=LDAPSearch( 501 "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)' 502 ) 503 ) 504 self.mock_ldap.set_return_value('search_s', 505 ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice]) 506 user_count = User.objects.count() 507 508 user = self.backend.authenticate(username='alice', password='password') 509 510 self.assert_(user is not None) 511 self.assertEqual(User.objects.count(), user_count + 1) 512 self.assertEqual(self.mock_ldap.ldap_methods_called(), 513 ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s']) 514 515 516 def test_search_bind_no_user(self): 517 self._init_settings( 518 AUTH_LDAP_USER_SEARCH=LDAPSearch( 519 "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(cn=%(user)s)' 520 ) 521 ) 522 self.mock_ldap.set_return_value('search_s', 523 ("ou=people,o=test", 2, "(cn=alice)", None, 0), []) 524 525 user = self.backend.authenticate(username='alice', password='password') 526 527 self.assert_(user is None) 528 self.assertEqual(self.mock_ldap.ldap_methods_called(), 529 ['initialize', 'simple_bind_s', 'search_s']) 530 531 532 def test_search_bind_multiple_users(self): 533 self._init_settings( 534 AUTH_LDAP_USER_SEARCH=LDAPSearch( 535 "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=*)' 536 ) 537 ) 538 self.mock_ldap.set_return_value('search_s', 539 ("ou=people,o=test", 2, "(uid=*)", None, 0), [self.alice, self.bob]) 540 541 user = self.backend.authenticate(username='alice', password='password') 542 543 self.assert_(user is None) 544 self.assertEqual(self.mock_ldap.ldap_methods_called(), 545 ['initialize', 'simple_bind_s', 'search_s']) 546 547 548 def test_search_bind_bad_password(self): 549 self._init_settings( 550 AUTH_LDAP_USER_SEARCH=LDAPSearch( 551 "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)' 552 ) 553 ) 554 self.mock_ldap.set_return_value('search_s', 555 ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice]) 556 557 user = self.backend.authenticate(username='alice', password='bogus') 558 559 self.assert_(user is None) 560 self.assertEqual(self.mock_ldap.ldap_methods_called(), 561 ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s']) 562 563 564 def test_search_bind_with_credentials(self): 565 self._init_settings( 566 AUTH_LDAP_BIND_DN='uid=bob,ou=people,o=test', 567 AUTH_LDAP_BIND_PASSWORD='password', 568 AUTH_LDAP_USER_SEARCH=LDAPSearch( 569 "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)' 570 ) 571 ) 572 self.mock_ldap.set_return_value('search_s', 573 ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice]) 574 575 user = self.backend.authenticate(username='alice', password='password') 576 577 self.assert_(user is not None) 578 self.assertEqual(self.mock_ldap.ldap_methods_called(), 579 ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s']) 580 581 582 def test_search_bind_with_bad_credentials(self): 583 self._init_settings( 584 AUTH_LDAP_BIND_DN='uid=bob,ou=people,o=test', 585 AUTH_LDAP_BIND_PASSWORD='bogus', 586 AUTH_LDAP_USER_SEARCH=LDAPSearch( 587 "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)' 588 ) 589 ) 590 591 user = self.backend.authenticate(username='alice', password='password') 592 593 self.assert_(user is None) 594 self.assertEqual(self.mock_ldap.ldap_methods_called(), 595 ['initialize', 'simple_bind_s']) 596 597 598 def test_unicode_user(self): 599 self._init_settings( 600 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 601 AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'} 602 ) 603 604 user = self.backend.authenticate(username=u'dreßler', password='password') 605 606 self.assert_(user is not None) 607 self.assertEqual(user.username, u'dreßler') 608 self.assertEqual(user.last_name, u'Dreßler') 609 610 611 def test_populate_user(self): 612 self._init_settings( 613 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 614 AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'} 615 ) 616 617 user = self.backend.authenticate(username='alice', password='password') 618 619 self.assertEqual(user.username, 'alice') 620 self.assertEqual(user.first_name, 'Alice') 621 self.assertEqual(user.last_name, 'Adams') 622 623 624 def test_no_update_existing(self): 625 self._init_settings( 626 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 627 AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'}, 628 AUTH_LDAP_ALWAYS_UPDATE_USER=False 629 ) 630 User.objects.create(username='alice', first_name='Alicia', last_name='Astro') 631 632 alice = self.backend.authenticate(username='alice', password='password') 633 bob = self.backend.authenticate(username='bob', password='password') 634 635 self.assertEqual(alice.first_name, 'Alicia') 636 self.assertEqual(alice.last_name, 'Astro') 637 self.assertEqual(bob.first_name, 'Robert') 638 self.assertEqual(bob.last_name, 'Barker') 639 640 641 def test_require_group(self): 642 self._init_settings( 643 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 644 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE), 645 AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'), 646 AUTH_LDAP_REQUIRE_GROUP="cn=active_gon,ou=groups,o=test" 647 ) 648 649 alice = self.backend.authenticate(username='alice', password='password') 650 bob = self.backend.authenticate(username='bob', password='password') 651 652 self.assert_(alice is not None) 653 self.assert_(bob is None) 654 self.assertEqual(self.mock_ldap.ldap_methods_called(), 655 ['initialize', 'simple_bind_s', 'compare_s', 'initialize', 'simple_bind_s', 'compare_s']) 656 657 658 def test_dn_group_membership(self): 659 self._init_settings( 660 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 661 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE), 662 AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'), 663 AUTH_LDAP_USER_FLAGS_BY_GROUP={ 664 'is_active': "cn=active_gon,ou=groups,o=test", 665 'is_staff': "cn=staff_gon,ou=groups,o=test", 666 'is_superuser': "cn=superuser_gon,ou=groups,o=test" 667 } 668 ) 669 670 alice = self.backend.authenticate(username='alice', password='password') 671 bob = self.backend.authenticate(username='bob', password='password') 672 673 self.assert_(alice.is_active) 674 self.assert_(alice.is_staff) 675 self.assert_(alice.is_superuser) 676 self.assert_(not bob.is_active) 677 self.assert_(not bob.is_staff) 678 self.assert_(not bob.is_superuser) 679 680 681 def test_posix_membership(self): 682 self._init_settings( 683 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 684 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE), 685 AUTH_LDAP_GROUP_TYPE=PosixGroupType(), 686 AUTH_LDAP_USER_FLAGS_BY_GROUP={ 687 'is_active': "cn=active_px,ou=groups,o=test", 688 'is_staff': "cn=staff_px,ou=groups,o=test", 689 'is_superuser': "cn=superuser_px,ou=groups,o=test" 690 } 691 ) 692 693 alice = self.backend.authenticate(username='alice', password='password') 694 bob = self.backend.authenticate(username='bob', password='password') 695 696 self.assert_(alice.is_active) 697 self.assert_(alice.is_staff) 698 self.assert_(alice.is_superuser) 699 self.assert_(not bob.is_active) 700 self.assert_(not bob.is_staff) 701 self.assert_(not bob.is_superuser) 702 703 704 def test_nested_dn_group_membership(self): 705 self._init_settings( 706 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 707 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE), 708 AUTH_LDAP_GROUP_TYPE=NestedMemberDNGroupType(member_attr='member'), 709 AUTH_LDAP_USER_FLAGS_BY_GROUP={ 710 'is_active': "cn=parent_gon,ou=groups,o=test", 711 'is_staff': "cn=parent_gon,ou=groups,o=test", 712 } 713 ) 714 self.mock_ldap.set_return_value('search_s', 715 ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=alice,ou=people,o=test)))", None, 0), 716 [self.active_gon, self.nested_gon] 717 ) 718 self.mock_ldap.set_return_value('search_s', 719 ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=active_gon,ou=groups,o=test)(member=cn=nested_gon,ou=groups,o=test)))", None, 0), 720 [self.parent_gon] 721 ) 722 self.mock_ldap.set_return_value('search_s', 723 ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=parent_gon,ou=groups,o=test)))", None, 0), 724 [self.circular_gon] 725 ) 726 self.mock_ldap.set_return_value('search_s', 727 ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=circular_gon,ou=groups,o=test)))", None, 0), 728 [self.nested_gon] 729 ) 730 731 self.mock_ldap.set_return_value('search_s', 732 ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=bob,ou=people,o=test)))", None, 0), 733 [] 734 ) 735 736 alice = self.backend.authenticate(username='alice', password='password') 737 bob = self.backend.authenticate(username='bob', password='password') 738 739 self.assert_(alice.is_active) 740 self.assert_(alice.is_staff) 741 self.assert_(not bob.is_active) 742 self.assert_(not bob.is_staff) 743 744 745 def test_posix_missing_attributes(self): 746 self._init_settings( 747 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 748 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE), 749 AUTH_LDAP_GROUP_TYPE=PosixGroupType(), 750 AUTH_LDAP_USER_FLAGS_BY_GROUP={ 751 'is_active': "cn=active_px,ou=groups,o=test" 752 } 753 ) 754 755 nobody = self.backend.authenticate(username='nobody', password='password') 756 757 self.assert_(not nobody.is_active) 758 759 760 def test_dn_group_permissions(self): 761 self._init_settings( 762 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 763 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE), 764 AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'), 765 AUTH_LDAP_FIND_GROUP_PERMS=True 766 ) 767 self._init_groups() 768 self.mock_ldap.set_return_value('search_s', 769 ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0), 770 [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon] 771 ) 772 773 alice = User.objects.create(username='alice') 774 alice = self.backend.get_user(alice.pk) 775 776 self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"])) 777 self.assertEqual(self.backend.get_all_permissions(alice), set(["auth.add_user", "auth.change_user"])) 778 self.assert_(self.backend.has_perm(alice, "auth.add_user")) 779 self.assert_(self.backend.has_module_perms(alice, "auth")) 780 781 782 def test_posix_group_permissions(self): 783 self._init_settings( 784 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 785 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', 786 self.mock_ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" 787 ), 788 AUTH_LDAP_GROUP_TYPE=PosixGroupType(), 789 AUTH_LDAP_FIND_GROUP_PERMS=True 790 ) 791 self._init_groups() 792 self.mock_ldap.set_return_value('search_s', 793 ("ou=groups,o=test", 2, "(&(objectClass=posixGroup)(|(gidNumber=1000)(memberUid=alice)))", None, 0), 794 [self.active_px, self.staff_px, self.superuser_px] 795 ) 796 797 alice = User.objects.create(username='alice') 798 alice = self.backend.get_user(alice.pk) 799 800 self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"])) 801 self.assertEqual(self.backend.get_all_permissions(alice), set(["auth.add_user", "auth.change_user"])) 802 self.assert_(self.backend.has_perm(alice, "auth.add_user")) 803 self.assert_(self.backend.has_module_perms(alice, "auth")) 804 805 def test_foreign_user_permissions(self): 806 self._init_settings( 807 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 808 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE), 809 AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'), 810 AUTH_LDAP_FIND_GROUP_PERMS=True 811 ) 812 self._init_groups() 813 814 alice = User.objects.create(username='alice') 815 816 self.assertEqual(self.backend.get_group_permissions(alice), set()) 817 818 819 def test_group_cache(self): 820 self._init_settings( 821 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 822 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE), 823 AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'), 824 AUTH_LDAP_FIND_GROUP_PERMS=True, 825 AUTH_LDAP_CACHE_GROUPS=True 826 ) 827 self._init_groups() 828 self.mock_ldap.set_return_value('search_s', 829 ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0), 830 [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon] 831 ) 832 self.mock_ldap.set_return_value('search_s', 833 ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=bob,ou=people,o=test))", None, 0), 834 [] 835 ) 836 837 alice_id = User.objects.create(username='alice').pk 838 bob_id = User.objects.create(username='bob').pk 839 840 # Check permissions twice for each user 841 for i in range(2): 842 alice = self.backend.get_user(alice_id) 843 self.assertEqual(self.backend.get_group_permissions(alice), 844 set(["auth.add_user", "auth.change_user"])) 845 846 bob = self.backend.get_user(bob_id) 847 self.assertEqual(self.backend.get_group_permissions(bob), set()) 848 849 # Should have executed one LDAP search per user 850 self.assertEqual(self.mock_ldap.ldap_methods_called(), 851 ['initialize', 'simple_bind_s', 'search_s', 'initialize', 'simple_bind_s', 'search_s']) 852 853 def test_group_mirroring(self): 854 self._init_settings( 855 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 856 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', 857 self.mock_ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)" 858 ), 859 AUTH_LDAP_GROUP_TYPE=PosixGroupType(), 860 AUTH_LDAP_MIRROR_GROUPS=True, 861 ) 862 self.mock_ldap.set_return_value('search_s', 863 ("ou=groups,o=test", 2, "(&(objectClass=posixGroup)(|(gidNumber=1000)(memberUid=alice)))", None, 0), 864 [self.active_px, self.staff_px, self.superuser_px] 865 ) 866 867 self.assertEqual(Group.objects.count(), 0) 868 869 alice = self.backend.authenticate(username='alice', password='password') 870 871 self.assertEqual(Group.objects.count(), 3) 872 self.assertEqual(set(alice.groups.all()), set(Group.objects.all())) 873 874 def test_nested_group_mirroring(self): 875 self._init_settings( 876 AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test', 877 AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE), 878 AUTH_LDAP_GROUP_TYPE=NestedMemberDNGroupType(member_attr='member'), 879 AUTH_LDAP_MIRROR_GROUPS=True, 880 ) 881 self.mock_ldap.set_return_value('search_s', 882 ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=alice,ou=people,o=test)))", None, 0), 883 [self.active_gon, self.nested_gon] 884 ) 885 self.mock_ldap.set_return_value('search_s', 886 ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=active_gon,ou=groups,o=test)(member=cn=nested_gon,ou=groups,o=test)))", None, 0), 887 [self.parent_gon] 888 ) 889 self.mock_ldap.set_return_value('search_s', 890 ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=parent_gon,ou=groups,o=test)))", None, 0), 891 [self.circular_gon] 892 ) 893 self.mock_ldap.set_return_value('search_s', 894 ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=circular_gon,ou=groups,o=test)))", None, 0), 895 [self.nested_gon] 896 ) 897 898 alice = self.backend.authenticate(username='alice', password='password') 899 900 self.assertEqual(Group.objects.count(), 4) 901 self.assertEqual(set(Group.objects.all().values_list('name', flat=True)), 902 set(['active_gon', 'nested_gon', 'parent_gon', 'circular_gon'])) 903 self.assertEqual(set(alice.groups.all()), set(Group.objects.all())) 904 905 906 def _init_settings(self, **kwargs): 907 backend.ldap_settings = TestSettings(**kwargs) 908 909 def _init_groups(self): 910 permissions = [ 911 Permission.objects.get(codename="add_user"), 912 Permission.objects.get(codename="change_user") 913 ] 914 915 active_gon = Group.objects.create(name='active_gon') 916 active_gon.permissions.add(*permissions) 917 918 active_px = Group.objects.create(name='active_px') 919 active_px.permissions.add(*permissions) -
docs/howto/index.txt
12 12 :maxdepth: 1 13 13 14 14 apache-auth 15 auth-ldap 15 16 auth-remote-user 16 17 custom-management-commands 17 18 custom-model-fields -
docs/howto/auth-ldap.txt
1 .. _howto-auth-ldap: 2 3 ========================= 4 Authentication using LDAP 5 ========================= 6 7 .. versionadded:: 1.2 8 9 Django includes an LDAP authentication backend to authenticate against any LDAP 10 server. To enable, add 11 :class:`django.contrib.auth.contrib.ldap.backend.LDAPBackend` to 12 :setting:`AUTHENTICATION_BACKENDS`. LDAP configuration can be as simple as a 13 single distinguished name template, but there are many rich options for working 14 with :class:`~django.contrib.auth.models.User` objects, groups, and permissions. 15 This backend depends on the `Python ldap <http://www.python-ldap.org/>`_ module. 16 17 .. note:: 18 19 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` does not 20 inherit from :class:`~django.contrib.auth.backends.ModelBackend`. It is 21 possible to use 22 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` exclusively 23 by configuring it to draw group membership from the LDAP server. However, if 24 you would like to assign permissions to individual users or add users to 25 groups within Django, you'll need to have both backends installed: 26 27 .. code-block:: python 28 29 AUTHENTICATION_BACKENDS = ( 30 'django.contrib.auth.contrib.ldap.backend.LDAPBackend', 31 'django.contrib.auth.backends.ModelBackend', 32 ) 33 34 35 .. _howto-auth-ldap-basic-authentication: 36 37 Configuring basic authentication 38 ================================ 39 40 If your LDAP server isn't running locally on the default port, you'll want to 41 start by setting :setting:`AUTH_LDAP_SERVER_URI` to point to your server. 42 43 .. code-block:: python 44 45 AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com" 46 47 That done, the first step is to authenticate a username and password against the 48 LDAP service. There are two ways to do this, called search/bind and simply bind. 49 The first one involves connecting to the LDAP server either anonymously or with 50 a fixed account and searching for the distinguished name of the authenticating 51 user. Then we can attempt to bind again with the user's password. The second 52 method is to derive the user's DN from his username and attempt to bind as the 53 user directly. 54 55 Because LDAP searches appear elsewhere in the configuration, the 56 :class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` class is provided 57 to encapsulate search information. In this case, the filter parameter should 58 contain the placeholder ``%(user)s``. A simple configuration for the search/bind 59 approach looks like this (some defaults included for completeness):: 60 61 import ldap 62 from django.contrib.auth.contrib.ldap.config import LDAPSearch 63 64 AUTH_LDAP_BIND_DN = "" 65 AUTH_LDAP_BIND_PASSWORD = "" 66 AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=example,dc=com", 67 ldap.SCOPE_SUBTREE, "(uid=%(user)s)") 68 69 This will perform an anonymous bind, search under 70 ``"ou=users,dc=example,dc=com"`` for an object with a uid matching the user's 71 name, and try to bind using that DN and the user's password. The search must 72 return exactly one result or authentication will fail. If you can't search 73 anonymously, you can set :setting:`AUTH_LDAP_BIND_DN` to the distinguished name 74 of an authorized user and :setting:`AUTH_LDAP_BIND_PASSWORD` to the password. 75 76 To skip the search phase, set :setting:`AUTH_LDAP_USER_DN_TEMPLATE` to a 77 template that will produce the authenticating user's DN directly. This template 78 should have one placeholder, ``%(user)s``. If the previous example had used 79 ``ldap.SCOPE_ONELEVEL``, the following would be a more straightforward (and 80 efficient) equivalent:: 81 82 AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com" 83 84 85 .. _howto-auth-ldap-groups: 86 87 Working with groups 88 =================== 89 90 Working with groups in LDAP can be a tricky business, as there isn't a single 91 standard grouping mechanism. This module includes an extensible API for working 92 with any kind of group and includes implementations for the most common ones. 93 :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` is a base class 94 whose concrete subclasses can determine group membership for particular grouping 95 mechanisms. Three built-in subclasses cover most grouping mechanisms: 96 97 * :class:`~django.contrib.auth.contrib.ldap.config.PosixGroupType` 98 * :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType` 99 * :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType` 100 101 posixGroup objects are somewhat specialized, so they get their own class. The 102 other two cover mechanisms whereby a group object stores a list of its members 103 as distinguished names. This includes groupOfNames, groupOfUniqueNames, and 104 Active Directory groups, among others. The nested variant allows groups to 105 contain other groups, to as many levels as you like. For convenience and 106 readability, several trivial subclasses of the above are provided: 107 108 * :class:`~django.contrib.auth.contrib.ldap.config.GroupOfNamesType` 109 * :class:`~django.contrib.auth.contrib.ldap.config.NestedGroupOfNamesType` 110 * :class:`~django.contrib.auth.contrib.ldap.config.GroupOfUniqueNamesType` 111 * :class:`~django.contrib.auth.contrib.ldap.config.NestedGroupOfUniqueNamesType` 112 * :class:`~django.contrib.auth.contrib.ldap.config.ActiveDirectoryGroupType` 113 * :class:`~django.contrib.auth.contrib.ldap.config.NestedActiveDirectoryGroupType` 114 115 To get started, you'll need to provide some basic information about your LDAP 116 groups. :setting:`AUTH_LDAP_GROUP_SEARCH` is an 117 :class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` object that 118 identifies the set of relevant group objects. That is, all groups that users 119 might belong to as well as any others that we might need to know about (in the 120 case of nested groups, for example). :setting:`AUTH_LDAP_GROUP_TYPE` is an 121 instance of the class corresponding to the type of group that will be returned 122 by :setting:`AUTH_LDAP_GROUP_SEARCH`. All groups referenced elsewhere in the 123 configuration must be of this type and part of the search results. 124 125 .. code-block:: python 126 127 import ldap 128 from django.contrib.auth.contrib.ldap.config import LDAPSearch, GroupOfNamesType 129 130 AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=groups,dc=example,dc=com", 131 ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" 132 ) 133 AUTH_LDAP_GROUP_TYPE = GroupOfNamesType() 134 135 The simplest use of groups is to limit the users who are allowed to log in. If 136 :setting:`AUTH_LDAP_REQUIRE_GROUP` is set, then only users who are members of 137 that group will successfully authenticate:: 138 139 AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=groups,dc=example,dc=com" 140 141 More advanced uses of groups are covered in the next two sections. 142 143 144 .. _howto-auth-ldap-user-objects: 145 146 User objects 147 ============ 148 149 Authenticating against an external source is swell, but Django's auth module is 150 tightly bound to the :class:`django.contrib.auth.models.User` model. Thus, when 151 a user logs in, we have to create a :class:`~django.contrib.auth.models.User` 152 object to represent him in the database. 153 154 The only required field for a user is the username, which we obviously have. The 155 :class:`~django.contrib.auth.models.User` model is picky about the characters 156 allowed in usernames, so 157 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` includes a pair 158 of hooks, 159 :meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.ldap_to_django_username` 160 and 161 :meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.django_to_ldap_username`, 162 to translate between LDAP usernames and Django usernames. You'll need this, for 163 example, if your LDAP names have periods in them. You can subclass 164 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` to implement 165 these hooks; by default the username is not modified. 166 :class:`~django.contrib.auth.models.User` objects that are authenticated by 167 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will have an 168 :attr:`~django.contrib.auth.models.User.ldap_username` attribute with the 169 original (LDAP) username. :attr:`~django.contrib.auth.models.User.username` 170 will, of course, be the Django username. 171 172 LDAP directories tend to contain much more information about users that you may 173 wish to propagate. A pair of settings, :setting:`AUTH_LDAP_USER_ATTR_MAP` and 174 :setting:`AUTH_LDAP_PROFILE_ATTR_MAP`, serve to copy directory information into 175 :class:`~django.contrib.auth.models.User` and profile objects. These are 176 dictionaries that map user and profile model keys, respectively, to LDAP 177 attribute names:: 178 179 AUTH_LDAP_USER_ATTR_MAP = {"first_name": "givenName", "last_name": "sn"} 180 AUTH_LDAP_PROFILE_ATTR_MAP = {"home_directory": "homeDirectory"} 181 182 Only string fields can be mapped to attributes. Boolean fields can be defined by 183 group membership:: 184 185 AUTH_LDAP_USER_FLAGS_BY_GROUP = { 186 "is_active": "cn=active,ou=groups,dc=example,dc=com", 187 "is_staff": "cn=staff,ou=groups,dc=example,dc=com", 188 "is_superuser": "cn=superuser,ou=groups,dc=example,dc=com" 189 } 190 191 By default, all mapped user fields will be updated each time the user logs in. 192 To disable this, set :setting:`AUTH_LDAP_ALWAYS_UPDATE_USER` to ``False``. 193 194 If you need to access multi-value attributes or there is some other reason that 195 the above is inadequate, you can also access the user's raw LDAP attributes. 196 ``user.ldap_user`` is an object with two public properties: 197 198 * ``dn``: The user's distinguished name. 199 * ``attrs``: The user's LDAP attributes as a dictionary of lists of string 200 values. 201 202 Python-ldap returns all attribute values as utf8-encoded strings. For 203 convenience, this module will try to decode all values into Unicode strings. Any 204 string that can not be successfully decoded will be left as-is; this may apply 205 to binary values such as Active Directory's objectSid. 206 207 .. note:: 208 209 Users created by 210 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will have an 211 unusable password set. This will only happen when the user is created, so if 212 you set a valid password in Django, the user will be able to log in through 213 :class:`~django.contrib.auth.backends.ModelBackend` (if configured) even if 214 he is rejected by LDAP. This is not generally recommended, but could be 215 useful as a fail-safe for selected users in case the LDAP server is 216 unavailable. 217 218 219 .. _howto-auth-ldap-permissions: 220 221 Permissions 222 =========== 223 224 Groups are useful for more than just populating the user's ``is_*`` fields. 225 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` would not be 226 complete without some way to turn a user's LDAP group memberships into Django 227 model permissions. In fact, there are two ways to do this. 228 229 Ultimately, both mechanisms need some way to map LDAP groups to Django groups. 230 Implementations of 231 :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` will have an 232 algorithm for deriving the Django group name from the LDAP group. Clients that 233 need to modify this behavior can subclass the 234 :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` class. All of 235 the built-in implementations take a ``name_attr`` argument to ``__init__``, 236 which specifies the LDAP attribute from which to take the Django group name. By 237 default, the ``cn`` attribute is used. 238 239 The least invasive way to map group permissions is to set 240 :setting:`AUTH_LDAP_FIND_GROUP_PERMS` to ``True`` 241 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will then find 242 all of the LDAP groups that a user belongs to, map them to Django groups, and 243 load the permissions for those groups. You will need to create the Django groups 244 yourself, generally through the admin interface. 245 246 .. note:: 247 248 After the user logs in, subsequent requests will have to determine group 249 membership based solely on the :class:`~django.contrib.auth.models.User` 250 object of the logged-in user. We will not have the user's password at this 251 point. This means that if :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``, 252 we must have access to the LDAP directory through 253 :setting:`AUTH_LDAP_BIND_DN` and :setting:`AUTH_LDAP_BIND_PASSWORD`, even if 254 you're using :setting:`AUTH_LDAP_USER_DN_TEMPLATE` to authenticate the user. 255 256 To minimize traffic to the LDAP server, 257 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` can make use of 258 Django's :ref:`cache framework <topics-cache>` to keep a copy of a user's LDAP 259 group memberships. To enable this feature, set :setting:`AUTH_LDAP_CACHE_GROUPS` 260 to ``True``. You can also set :setting:`AUTH_LDAP_GROUP_CACHE_TIMEOUT` to 261 override the timeout of cache entries (in seconds). 262 263 .. code-block:: python 264 265 AUTH_LDAP_CACHE_GROUPS = True 266 AUTH_LDAP_GROUP_CACHE_TIMEOUT = 300 267 268 The second way to turn LDAP group memberships into permissions is to mirror the 269 groups themselves. If :setting:`AUTH_LDAP_MIRROR_GROUPS` is ``True``, then every 270 time a user logs in, 271 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will update the 272 database with the user's LDAP groups. Any group that doesn't exist will be 273 created and the user's Django group membership will be updated to exactly match 274 his LDAP group membership. Note that if the LDAP server has nested groups, the 275 Django database will end up with a flattened representation. 276 277 This approach has two main differences from 278 :setting:`AUTH_LDAP_FIND_GROUP_PERMS`. First, 279 :setting:`AUTH_LDAP_FIND_GROUP_PERMS` will query for LDAP group membership 280 either for every request or according to the cache timeout. With group 281 mirroring, membership will be updated when the user authenticates. This may not 282 be appropriate for sites with long session timeouts. The second difference is 283 that with :setting:`AUTH_LDAP_FIND_GROUP_PERMS`, there is no way for clients to 284 determine a user's group memberships, only their permissions. If you want to 285 make decisions based directly on group membership, you'll have to mirror the 286 groups. 287 288 289 .. _howto-auth-ldap-logging: 290 291 Logging 292 ======= 293 294 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` uses the standard 295 logging module to log debug and warning messages to the logger named 296 ``'django.contrib.auth.contrib.ldap'``. If you need debug messages to 297 help with configuration issues, you should add a handler to this logger. 298 299 300 .. _howto-auth-ldap-options: 301 302 More options 303 ============ 304 305 Miscellaneous settings for 306 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend`: 307 308 * :setting:`AUTH_LDAP_GLOBAL_OPTIONS`: A dictionary of options to pass to 309 python-ldap via ``ldap.set_option()``. 310 * :setting:`AUTH_LDAP_CONNECTION_OPTIONS`: A dictionary of options to pass 311 to each LDAPObject instance via ``LDAPObject.set_option()``. 312 313 314 .. _howto-auth-ldap-performance: 315 316 Performance 317 =========== 318 319 :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` is carefully 320 designed not to require a connection to the LDAP service for every request. Of 321 course, this depends heavily on how it is configured. If LDAP traffic or latency 322 is a concern for your deployment, this section has a few tips on minimizing it, 323 in decreasing order of impact. 324 325 #. **Cache groups**. If :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``, 326 the default behavior is to reload a user's group memberships on every 327 request. This is the safest behavior, as any membership change takes 328 effect immediately, but it is expensive. If possible, set 329 :setting:`AUTH_LDAP_CACHE_GROUPS` to ``True`` to remove most of this 330 traffic. Alternatively, you might consider using 331 :setting:`AUTH_LDAP_MIRROR_GROUPS` and relying on 332 :class:`~django.contrib.auth.backends.ModelBackend` to supply group 333 permissions. 334 #. **Don't access user.ldap_user.***. These properties are only cached 335 on a per-request basis. If you can propagate LDAP attributes to a 336 :class:`~django.contrib.auth.models.User` or profile object, they will 337 only be updated at login. ``user.ldap_user.attrs`` triggers an LDAP 338 connection for every request in which it's accessed. If you're not using 339 :setting:`AUTH_LDAP_USER_DN_TEMPLATE`, then accessing 340 ``user.ldap_user.dn`` will also trigger an LDAP connection. 341 #. **Use simpler group types**. Some grouping mechanisms are more expensive 342 than others. This will often be outside your control, but it's important 343 to note that the extra functionality of more complex group types like 344 :class:`~django.contrib.auth.contrib.ldap.config.NestedGroupOfNamesType` 345 is not free and will generally require a greater number and complexity of 346 LDAP queries. 347 #. **Use direct binding**. Binding with 348 :setting:`AUTH_LDAP_USER_DN_TEMPLATE` is a little bit more efficient than 349 relying on :setting:`AUTH_LDAP_USER_SEARCH`. Specifically, it saves two 350 LDAP operations (one bind and one search) per login. 351 352 353 .. _howto-auth-ldap-example: 354 355 Example configuration 356 ===================== 357 358 Here is a complete example configuration from :file:`settings.py` that 359 exercises nearly all of the features. In this example, we're authenticating 360 against a global pool of users in the directory, but we have a special area set 361 aside for Django groups (ou=django,ou=groups,dc=example,dc=com). Remember that 362 most of this is optional if you just need simple authentication. Some default 363 settings and arguments are included for completeness. 364 365 .. code-block:: python 366 367 import ldap 368 from django.contrib.auth.contrib.ldap.config import LDAPSearch, GroupOfNamesType 369 370 371 # Baseline configuration. 372 AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com" 373 374 AUTH_LDAP_BIND_DN = "cn=django-agent,dc=example,dc=com" 375 AUTH_LDAP_BIND_PASSWORD = "phlebotinum" 376 AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=example,dc=com", 377 ldap.SCOPE_SUBTREE, "(uid=%(user)s)") 378 # or perhaps: 379 # AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com" 380 381 # Set up the basic group parameters. 382 AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=django,ou=groups,dc=example,dc=com", 383 ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)" 384 ) 385 AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn") 386 387 # Only users in this group can log in. 388 AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=django,ou=groups,dc=example,dc=com" 389 390 # Populate the Django user from the LDAP directory. 391 AUTH_LDAP_USER_ATTR_MAP = { 392 "first_name": "givenName", 393 "last_name": "sn", 394 "email": "mail" 395 } 396 397 AUTH_LDAP_PROFILE_ATTR_MAP = { 398 "employee_number": "employeeNumber" 399 } 400 401 AUTH_LDAP_USER_FLAGS_BY_GROUP = { 402 "is_active": "cn=active,ou=django,ou=groups,dc=example,dc=com", 403 "is_staff": "cn=staff,ou=django,ou=groups,dc=example,dc=com", 404 "is_superuser": "cn=superuser,ou=django,ou=groups,dc=example,dc=com" 405 } 406 407 # This is the default, but I like to be explicit. 408 AUTH_LDAP_ALWAYS_UPDATE_USER = True 409 410 # Use LDAP group membership to calculate group permissions. 411 AUTH_LDAP_FIND_GROUP_PERMS = True 412 413 # Cache group memberships for an hour to minimize LDAP traffic 414 AUTH_LDAP_CACHE_GROUPS = True 415 AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600 416 417 418 # Keep ModelBackend around for per-user permissions and maybe a local 419 # superuser. 420 AUTHENTICATION_BACKENDS = ( 421 'django.contrib.auth.contrib.ldap.backend.LDAPBackend', 422 'django.contrib.auth.backends.ModelBackend', 423 ) 424 425 426 .. _howto-auth-ldap-api-reference: 427 428 API reference 429 ============= 430 431 Configuration 432 ------------- 433 434 .. module:: django.contrib.auth.contrib.ldap.config 435 436 .. class:: LDAPSearch 437 438 .. method:: __init__(base_dn, scope, filterstr='(objectClass=*)') 439 440 * ``base_dn``: The distinguished name of the search base. 441 * ``scope``: One of ``ldap.SCOPE_*``. 442 * ``filterstr``: An optional filter string (e.g. '(objectClass=person)'). 443 In order to be valid, ``filterstr`` must be enclosed in parentheses. 444 445 446 .. class:: LDAPGroupType 447 448 The base class for objects that will determine group membership for various 449 LDAP grouping mechanisms. Implementations are provided for common group 450 types or you can write your own. See the source code for subclassing notes. 451 452 .. method:: __init__(name_attr='cn') 453 454 By default, LDAP groups will be mapped to Django groups by taking the 455 first value of the cn attribute. You can specify a different attribute 456 with ``name_attr``. 457 458 459 .. class:: PosixGroupType 460 461 A concrete subclass of 462 :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` that handles 463 the ``posixGroup`` object class. This checks for both primary group and 464 group membership. 465 466 .. method:: __init__(name_attr='cn') 467 468 .. class:: MemberDNGroupType 469 470 A concrete subclass of 471 :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` that handles 472 grouping mechanisms wherein the group object contains a list of its member 473 DNs. 474 475 .. method:: __init__(member_attr, name_attr='cn') 476 477 * ``member_attr``: The attribute on the group object that contains a 478 list of member DNs. 'member' and 'uniqueMember' are common examples. 479 480 481 .. class:: NestedMemberDNGroupType 482 483 Similar to 484 :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType`, except 485 this allows groups to contain other groups as members. Group hierarchies 486 will be traversed to determine membership. 487 488 .. method:: __init__(member_attr, name_attr='cn') 489 490 As above. 491 492 493 .. class:: GroupOfNamesType 494 495 A concrete subclass of 496 :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType` that 497 handles the ``groupOfNames`` object class. Equivalent to 498 ``MemberDNGroupType('member')``. 499 500 .. method:: __init__(name_attr='cn') 501 502 503 .. class:: NestedGroupOfNamesType 504 505 A concrete subclass of 506 :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType` 507 that handles the ``groupOfNames`` object class. Equivalent to 508 ``NestedMemberDNGroupType('member')``. 509 510 .. method:: __init__(name_attr='cn') 511 512 513 .. class:: GroupOfUniqueNamesType 514 515 A concrete subclass of 516 :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType` that 517 handles the ``groupOfUniqueNames`` object class. Equivalent to 518 ``MemberDNGroupType('uniqueMember')``. 519 520 .. method:: __init__(name_attr='cn') 521 522 523 .. class:: NestedGroupOfUniqueNamesType 524 525 A concrete subclass of 526 :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType` 527 that handles the ``groupOfUniqueNames`` object class. Equivalent to 528 ``NestedMemberDNGroupType('uniqueMember')``. 529 530 .. method:: __init__(name_attr='cn') 531 532 533 .. class:: ActiveDirectoryGroupType 534 535 A concrete subclass of 536 :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType` that 537 handles Active Directory groups. Equivalent to 538 ``MemberDNGroupType('member')``. 539 540 .. method:: __init__(name_attr='cn') 541 542 543 .. class:: NestedActiveDirectoryGroupType 544 545 A concrete subclass of 546 :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType` 547 that handles Active Directory groups. Equivalent to 548 ``NestedMemberDNGroupType('member')``. 549 550 .. method:: __init__(name_attr='cn') 551 552 553 Backend 554 ------- 555 556 .. module:: django.contrib.auth.contrib.ldap.backend 557 558 .. class:: LDAPBackend 559 560 .. method:: ldap_to_django_username(username) 561 562 Returns a valid Django username based on the given LDAP username (which 563 is what the user enters). By default, ``username`` is returned 564 unchanged. This can be overriden by subclasses. 565 566 .. method:: django_to_ldap_username(username) 567 568 The inverse of 569 :meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.ldap_to_django_username`. 570 If this is not symmetrical to 571 :meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.ldap_to_django_username`, 572 the behavior is undefined. -
docs/ref/authbackends.txt
35 35 :attr:`request.META['REMOTE_USER'] <django.http.HttpRequest.META>`. See 36 36 the :ref:`Authenticating against REMOTE_USER <howto-auth-remote-user>` 37 37 documentation. 38 39 40 .. class:: LDAPBackend 41 42 .. versionadded:: 1.2 43 44 This backend authenticates against an existing LDAP directory. You must have 45 python-ldap installed in order to use it. In addition to authenticating 46 individual users, it can optionally query the LDAP server for group 47 membership and thus group permissions. See the :ref:`howto-auth-ldap` 48 documentation. -
docs/ref/settings.txt
105 105 authenticate a user. See the :ref:`authentication backends documentation 106 106 <authentication-backends>` for details. 107 107 108 109 .. setting:: AUTH_LDAP_ALWAYS_UPDATE_USER 110 111 AUTH_LDAP_ALWAYS_UPDATE_USER 112 ---------------------------- 113 114 Default: ``True`` 115 116 If ``True``, the fields of a :class:`~django.contrib.auth.models.User` object 117 will be updated with the latest values from the LDAP directory every time the 118 user logs in. Otherwise the :class:`~django.contrib.auth.models.User` object 119 will only be populated when it is automatically created. See 120 :ref:`Authentication using LDAP <howto-auth-ldap-user-objects>`. 121 122 123 .. setting:: AUTH_LDAP_BIND_DN 124 125 AUTH_LDAP_BIND_DN 126 ----------------- 127 128 Default: ``''`` (Empty string) 129 130 The distinguished name to use for general access to the LDAP server. Use the 131 empty string (the default) for an anonymous bind. If 132 :setting:`AUTH_LDAP_USER_DN_TEMPLATE` is not set, we'll need this to search for 133 the user. If :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``, we'll also need 134 it to determine group membership on subsequent requests. See 135 :ref:`Authentication using LDAP <howto-auth-ldap-basic-authentication>`. 136 137 138 .. setting:: AUTH_LDAP_BIND_PASSWORD 139 140 AUTH_LDAP_BIND_PASSWORD 141 ----------------------- 142 143 Default: ``''`` (Empty string) 144 145 The password to use with :setting:`AUTH_LDAP_BIND_DN`. See :ref:`Authentication 146 using LDAP <howto-auth-ldap-basic-authentication>`. 147 148 149 .. setting:: AUTH_LDAP_CACHE_GROUPS 150 151 AUTH_LDAP_CACHE_GROUPS 152 ---------------------- 153 154 Default: ``False`` 155 156 If ``True``, LDAP group membership will be cached using Django's :ref:`cache 157 framework <topics-cache>`. The cache timeout can be customized with 158 :setting:`AUTH_LDAP_GROUP_CACHE_TIMEOUT`. See :ref:`Authentication using LDAP 159 <howto-auth-ldap-permissions>`. 160 161 162 .. setting:: AUTH_LDAP_CONNECTION_OPTIONS 163 164 AUTH_LDAP_CONNECTION_OPTIONS 165 ---------------------------- 166 167 Default: ``{}`` 168 169 A hash of options to pass to each connection to the LDAP server via 170 ``LDAPObject.set_option()``. Keys are ``ldap.OPT_*`` constants. See 171 :ref:`Authentication using LDAP <howto-auth-ldap-options>`. 172 173 174 .. setting:: AUTH_LDAP_GROUP_CACHE_TIMEOUT 175 176 AUTH_LDAP_GROUP_CACHE_TIMEOUT 177 ----------------------------- 178 179 Default: ``None`` 180 181 If :setting:`AUTH_LDAP_CACHE_GROUPS` is ``True``, this overrides the default 182 cache timeout for the group cache. See :ref:`Authentication using LDAP 183 <howto-auth-ldap-permissions>`. 184 185 186 .. setting:: AUTH_LDAP_FIND_GROUP_PERMS 187 188 AUTH_LDAP_FIND_GROUP_PERMS 189 -------------------------- 190 191 Default: ``False`` 192 193 If ``True``, :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will 194 furnish group permissions based on the LDAP groups the authenticated user 195 belongs to. :setting:`AUTH_LDAP_GROUP_SEARCH` and 196 :setting:`AUTH_LDAP_GROUP_TYPE` must also be set. See :ref:`Authentication using 197 LDAP <howto-auth-ldap-permissions>`. 198 199 200 .. setting:: AUTH_LDAP_GLOBAL_OPTIONS 201 202 AUTH_LDAP_GLOBAL_OPTIONS 203 ------------------------ 204 205 Default: ``{}`` 206 207 A hash of options to pass to ``ldap.set_option()``. Keys are ``ldap.OPT_*`` 208 constants. See :ref:`Authentication using LDAP <howto-auth-ldap-options>`. 209 210 211 .. setting:: AUTH_LDAP_GROUP_SEARCH 212 213 AUTH_LDAP_GROUP_SEARCH 214 ---------------------- 215 216 Default: ``None`` 217 218 An :class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` object that 219 finds all LDAP groups that users might belong to. If your configuration makes 220 any references to LDAP groups, this and :setting:`AUTH_LDAP_GROUP_TYPE` must be 221 set. See :ref:`Authentication using LDAP <howto-auth-ldap-groups>`. 222 223 224 .. setting:: AUTH_LDAP_GROUP_TYPE 225 226 AUTH_LDAP_GROUP_TYPE 227 -------------------- 228 229 Default: ``None`` 230 231 An :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` 232 instance describing the type of group returned by 233 :setting:`AUTH_LDAP_GROUP_SEARCH`. See :ref:`Authentication using LDAP 234 <howto-auth-ldap-groups>`. 235 236 237 .. setting:: AUTH_LDAP_MIRROR_GROUPS 238 239 AUTH_LDAP_MIRROR_GROUPS 240 ----------------------- 241 242 Default: ``False`` 243 244 If ``True``, :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will 245 mirror a user's LDAP group membership in the Django database. Any time a user 246 authenticates, we will create all of his LDAP groups as Django groups and update 247 his Django group membership to exactly match his LDAP group membership. If the 248 LDAP server has nested groups, the Django database will end up with a flattened 249 representation. See :ref:`Authentication using LDAP 250 <howto-auth-ldap-permissions>`. 251 252 253 .. setting:: AUTH_LDAP_PROFILE_ATTR_MAP 254 255 AUTH_LDAP_PROFILE_ATTR_MAP 256 -------------------------- 257 258 Default: ``{}`` 259 260 A mapping from user profile field names to LDAP attribute names. See 261 :ref:`Authentication using LDAP <howto-auth-ldap-user-objects>`. 262 263 264 .. setting:: AUTH_LDAP_REQUIRE_GROUP 265 266 AUTH_LDAP_REQUIRE_GROUP 267 ----------------------- 268 269 Default: ``None`` 270 271 The distinguished name of a group that a user must belong to in order to 272 successfully authenticate. See :ref:`Authentication using LDAP 273 <howto-auth-ldap-groups>`. 274 275 276 .. setting:: AUTH_LDAP_SERVER_URI 277 278 AUTH_LDAP_SERVER_URI 279 -------------------- 280 281 Default: ``ldap://localhost`` 282 283 The URI of the LDAP server. This can be any URI that is supported by your 284 underlying LDAP libraries. See :ref:`Authentication using LDAP 285 <howto-auth-ldap>`. 286 287 288 .. setting:: AUTH_LDAP_USER_ATTR_MAP 289 290 AUTH_LDAP_USER_ATTR_MAP 291 ----------------------- 292 293 Default: ``{}`` 294 295 A mapping from :class:`~django.contrib.auth.models.User` field names to LDAP 296 attribute names. See :ref:`Authentication using LDAP 297 <howto-auth-ldap-user-objects>`. 298 299 300 .. setting:: AUTH_LDAP_USER_DN_TEMPLATE 301 302 AUTH_LDAP_USER_DN_TEMPLATE 303 -------------------------- 304 305 Default: ``None`` 306 307 A string template that describes any user's distinguished name based on the 308 username. This must contain the placeholder ``%(user)s``. See 309 :ref:`Authentication using LDAP <howto-auth-ldap-basic-authentication>`. 310 311 312 .. setting:: AUTH_LDAP_USER_FLAGS_BY_GROUP 313 314 AUTH_LDAP_USER_FLAGS_BY_GROUP 315 ------------------------------ 316 317 Default: ``{}`` 318 319 A mapping from boolean :class:`~django.contrib.auth.models.User` field names to 320 distinguished names of LDAP groups. The corresponding field is set to ``True`` 321 or ``False`` according to whether the user is a member of the group. See 322 :ref:`Authentication using LDAP <howto-auth-ldap-user-objects>`. 323 324 325 .. setting:: AUTH_LDAP_USER_SEARCH 326 327 AUTH_LDAP_USER_SEARCH 328 --------------------- 329 330 Default: ``None`` 331 332 An :class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` object that will 333 locate a user in the directory. The filter parameter should contain the 334 placeholder ``%(user)s`` for the username. It must return exactly one result for 335 authentication to succeed. See :ref:`Authentication using LDAP 336 <howto-auth-ldap>`. 337 338 108 339 .. setting:: AUTH_PROFILE_MODULE 109 340 110 341 AUTH_PROFILE_MODULE