- Users may use their username or any of their registered email addresses as an account identifier (UID) when logging in.
- Users may use the same credentials to log into Nadine (Django) or LDAP / LDAP-supporting systems (WiFi, shared network drives, etc)
- Needs to be an optional plugin for Nadine (only some deployments will make use of it).
- We can use django-auth-ldap for auth in Nadine.
- We use django-ldapdb to create new user accounts in LDAP and propagate user account updates back to LDAP.
With the proposed tools we can meet requirements (1) and (2) for Nadine/Django and it will set up the LDAP infrastructure for future integrations with other systems.
However meeting requirement (2) for non-Django/Nadine systems (WiFi,
shared network drives, etc) is going to be dependant on what is supported by
the specific system. Short of researching how other systems auth against LDAP,
the best we can do is ensure our LDAP schema to follow common
standards/conventions.
Damien is confident that if we stick to standard LDAP account & group objectClass (eg: inetOrgPerson & posixAccount) then we should be compatible with anything else that auths against LDAP.
Setting up Nadine/Django LDAP auth is pretty straightforward, mostly configuration; need to provide a query to lookup the LDAP user by email and/or username/uid.
Handling new user registration & user management (updates) in Nadine and writing to LDAP will require hooking into Django/Nadine's user registration process.
Implementation details and example configuration for the various systems below:
User accounts are inetOrgPerson|posixAccount objectClass. These classes meet the minimum required attributes for our accounts (username, password, multiple email addresses). Usernames and email addresses are enforced to be unique by default. They also benefit (according to Damien) from being standardized auth targets for other external systems.
Groups are implemented with posixGroup objectClass. This decision is fairly arbitrary at this point (there's a whole range of group class types available) but it is consistent with the decision posixAccount class for user accounts.
dc=312main,dc=ca
o=312 Main
objectClass=top
objectClass=dcObject
objectClass=organization
cn=admin
objectClass=simpleSecurityObject
objectClass=organizationalRole
ou=groups
objectClass=organizationalUnit
objectClass=top
cn=members
objectClass=posixGroup
objectClass=top
cn=users
objectClass=posixGroup
objectClass=top
ou=users
objectClass=organizationalUnit
objectClass=top
cn=<username>
mail=<email1>
mail=<email2>
uid=<username>
uidNumber={LDAP generated}
objectClass=inetOrgPerson
objectClass=posixAccount
objectClass=top
AUTH_LDAP_BIND_DN = "cn=admin,dc=312main,dc=ca"
AUTH_LDAP_BIND_PASSWORD = "..."
# Search query for a user.
AUTH_LDAP_USER_SEARCH = LDAPSearch(
# Look under 'users' organizational unit (ou)
"ou=users,dc=312main,dc=ca",
ldap.SCOPE_SUBTREE,
# Match against the uid (alias: 'User Name') attribute
# "(uid=%(user)s)"
# TODO: Need to match against mail (alias: 'Email') attribute, this can
# contain multiple values in LDAP. The below check will end up
# creating a Django user for each value which is not what we want.
# Ideally we would be able to resolve the returned LDAP object to an
# existing field via a different (unique) attribute such as uid.
# UPDATE:
# This can be done with a custom LDAPBackend that overrides
# get_or_create_user() (https://django-auth-ldap.readthedocs.io/en/1.2.x/reference.html#django_auth_ldap.backend.LDAPBackend.get_or_create_user).
"(mail=%(user)s)"
)
# Specify the class to use when handling LDAP groups in Django
AUTH_LDAP_GROUP_TYPE = PosixGroupType(name_attr="cn")
# Search query to return all groups used by django-ldap-auth.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
"ou=groups,dc=312main,dc=ca",
ldap.SCOPE_SUBTREE,
# Grab everything that is a 'objectClass=posixGroup'
"(objectClass=posixGroup)"
)
# Set arbitrary properties on Django User based on LDAP attributes & queries
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
"is_active": "cn=users,ou=groups,dc=tnightingale,dc=com",
"is_staff": LDAPGroupQuery("cn=users,ou=groups,dc=tnightingale,dc=com")
}
We need to hook into the following events in Nadine:
- new user creation
- user password reset (user self-resetting or admin doing it)
- User permission changes that affect ability to login (i.e. inclusion in or removal from members group, user disabled by admin, something like that).
- (optional) update of user profile fields (if we save those in LDAP too... prob isn't much extra work)
For requirement (3) this integration must be implmenented in a pluggable way so as to make it optional per-deployment. Adding Django signals/events to relavent actions in Nadine will likely be sufficient.
Unfortunately the Django + LDAP integration story is fragmented. I suspect this is due to LDAP's amorphous / "anything to anyone" nature.
Below are the main contenders that I have found appropriate for our integration:
Opting for django-auth-ldap due to more features & flexible configuration
This is the Django plugin Jacob tested
- Django auth provider that talks to LDAP
- Django users are created on-demand
- Django users are updated from LDAP on each login
- Provides command / cron task
ldap_sync_users
as alternative method to keep Django users up to date - Doesn't write to LDAP
Docs: https://django-auth-ldap.readthedocs.io/en/1.2.x/
- Similar to django-python3-ldap but supports more elaborate methods for querying/syncing data from LDAP
- Supports using LDAP groups to drive Django user access & permissions
- Doesn't write to LDAP
- Provides Django ORM database adapter for LDAP
- Provides ability to create / query / update / delete LDAP objects via Django's ORM API
- Does not include a Django auth provider