Django LDAP ========== The latest guidance for installation can be found at https://django-auth-ldap.readthedocs.io/en/latest/install.html CUBRIC's setup has been tested on Django version 2 -> 4 (LTS). The Docker image used at the time of writing is `python:3.11.5-slim`. ## Packages Install the following packages on Ubuntu / Debian-based hosts ``` apt install ca-certificates libldap2-dev ``` ## Installation Install the following Python packages ``` pip install django-auth-ldap python-ldap ``` On the host/container, add the LDAP certificate ``` cp Cardiff-University-Root-Web-Combine.pem /usr/local/share/ca-certificates/ && update-ca-certificates ``` Add the following to settings.py ``` import ldap import logging from django_auth_ldap.config import LDAPSearch, GroupOfNamesType, LDAPGroupQuery # Troubleshooting logger = logging.getLogger("django_auth_ldap") logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG) # LDAP # Baseline configuration. ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) AUTH_LDAP_SERVER_URI = "ldaps://ldap-auth.cf.ac.uk" AUTH_LDAP_BIND_DN = <ldap-bind-account> # provided by server team AUTH_LDAP_BIND_PASSWORD = <ldap-bind-password> AUTH_LDAP_USER_SEARCH = LDAPSearch("o=CF", ldap.SCOPE_SUBTREE, "(uid=%(user)s)") # Search for users in Cardiff University AUTH_LDAP_GROUP_SEARCH = LDAPSearch( "o=CF", ldap.SCOPE_SUBTREE, "(objectClass=inetorgperson)", ) AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn") # Simple group restrictions # AUTH_LDAP_REQUIRE_GROUP = "cn=CUBRIC-All,ou=PSYCH,ou=AdHocGroups,ou=Groups,o=Resources" # AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=django,ou=groups,dc=example,dc=com" # Populate the Django user from the LDAP directory. AUTH_LDAP_USER_ATTR_MAP = { # django built-in user model fields -> ldap attributes "username": "uid", "first_name": "givenName", "last_name": "sn", "email": "mail", } # https://django-auth-ldap.readthedocs.io/en/latest/groups.html#limiting-access AUTH_LDAP_USER_FLAGS_BY_GROUP = { # "is_active": "cn=CUBRIC-All,ou=PSYCH,ou=AdHocGroups,ou=Groups,o=Resources", # is_active defaults to True, allowing anyone from the uni to login "is_staff": (LDAPGroupQuery("cn=CUBRIC-All,ou=PSYCH,ou=AdHocGroups,ou=Groups,o=Resources") | LDAPGroupQuery("cn=Research_Staff_EVERY,ou=UniversityWide,ou=DA,ou=Groups,ou=Resources,o=CF") | LDAPGroupQuery("cn=PSYCH-AllPostgraduates,ou=PSYCH,ou=DAGroups,ou=Groups,o=Resources") ), "is_superuser": "cn=CUBRIC-Sudo,ou=PSYCH,ou=AdHocGroups,ou=Groups,o=Resources", } # This is the default, but I like to be explicit. AUTH_LDAP_ALWAYS_UPDATE_USER = True # Use LDAP group membership to calculate group permissions. # AUTH_LDAP_FIND_GROUP_PERMS = True # Cache distinguished names and group memberships for 30 mins to minimize # LDAP traffic. AUTH_LDAP_CACHE_TIMEOUT = 1800 # Keep ModelBackend around for per-user permissions and maybe a local # superuser. AUTHENTICATION_BACKENDS = ( "django_auth_ldap.backend.LDAPBackend", "django.contrib.auth.backends.ModelBackend", ) AUTH_LDAP_CACHE_GROUPS = True ``` ## Accessing the model in HTML templates You can make use of `jinja` within HTML templates, allowing you to display content based on conditions. The example below assumes you use Bootstrap along with `crispy-bootstrap5` and `django-crispy-forms`. These tools make working with forms much easier and take care of the CSS (headaches) etc. ``` {% load crispy_forms_tags %} {% if user.is_authenticated %} <h4>Hello {{ user.first_name }} {{ user.last_name }}. You are already logged in.</h4> <br> <a class="btn btn-outline-secondary" href="{% url 'logout' %}" role="button">Logout</a> {% else %} <form method="post"> {% csrf_token %} {{ form|crispy }} <button type="submit" class="btn btn-outline-secondary">Login</button> </form> ``` ## Adding users (using code) It's also possible to add a user to Django from LDAP, without them logging in. This can be easily run from Jupyter (Django `shell_plus`). This is handy when you want to add them to a Django group or check their LDAP attributes before they access your site. ``` from django_auth_ldap.backend import LDAPBackend import pprint user = LDAPBackend().populate_user("sapgh3") if user is None: raise Exception(No user found in LDAP with that username") pprint(user.ldap_user.attrs) ``` ## Advanced configuration In CUBRIC's configuration, we have extended Django built-in User model and created a Profile model (within the 'Accounts' app). This allows us to pull **additional fields from LDAP** into the model. For example: Create the 'accounts' app ``` django-admin createapp accounts ``` models.py ``` from django.contrib.auth.models import AbstractUser from django.db.models import BooleanField, CharField, IntegerField from django_extensions.db.fields import AutoSlugField from django.urls import reverse class Profile(AbstractUser): """User Profile Data - Extends built-in User model""" slug = AutoSlugField(max_length=10, populate_from=["username"]) school = CharField(max_length=5, null=True, blank=True) userType = CharField(max_length=3, null=True, blank=True) uidNumber = IntegerField(null=True, blank=True) gidNumber = IntegerField(null=True, blank=True) telephoneNumber = CharField(max_length=31, null=True, blank=True) class Meta: ordering = ["last_name"] def __str__(self): return self.first_name + " " + self.last_name def get_absolute_url(self): """Return URL to detail page of Profile""" return reverse("profile_detail", kwargs={"slug": self.username}) ``` settings.py ``` INSTALLED_APPS = [ [...] 'accounts', [...] ] AUTH_USER_MODEL = "accounts.Profile" AUTH_LDAP_USER_ATTR_MAP = { [...] "school": "CardiffJCCSTransDept", "userType": "CardiffJCCSTransType", "uidNumber": "uidNumber", "gidNumber": "gidNumber", "groupMembership": "groupMembership", "telephoneNumber": "telephoneNumber", } ``` If you plan on implementing the same design, you must "start fresh" and migrate the new model (e.g. Profile) **before any other models**. This is very important so other models are using the correct one. ``` python manage.py makemigrations accounts python manage.py migrate accounts ``` If you're using ``django-guardian`` to manage object-level permissions, update the following: ``` AUTHENTICATION_BACKENDS = ( [...] "guardian.backends.ObjectPermissionBackend", ) ```