From ea20c5379fc03b2fb3b90094efe024a6f7b6c649 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Thu, 9 Apr 2026 15:00:13 -0400 Subject: [PATCH 1/5] make staff and access attributes configurable for saml2 --- userportal/authentication.py | 18 ++++++++++++++---- userportal/settings/40-saml.py | 9 ++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/userportal/authentication.py b/userportal/authentication.py index 9bc37cb..20ac61e 100644 --- a/userportal/authentication.py +++ b/userportal/authentication.py @@ -1,15 +1,25 @@ from djangosaml2.backends import Saml2Backend from django.contrib.auth.backends import RemoteUserBackend +from django.conf import settings + class staffSaml2Backend(Saml2Backend): """This will add/remove the is_staff attribute from the user as appropriate.""" def _update_user(self, user, attributes: dict, attribute_mapping: dict, force_save: bool = False): - if 'eduPersonAffiliation' in attributes: - if 'staff' in attributes['eduPersonAffiliation']: + # figure out if user is active (i.e. can login) + user.is_active = True + for attribute, value in settings.SAML_CONFIG['required_access_attributes']: + if attribute not in attributes or value not in attributes[attribute]: + user.is_active = False + + # figure out if user is staff + user.is_staff = False + for attribute, value in settings.SAML_CONFIG['staff_attributes']: + if attribute in attributes and value in attributes[attribute]: user.is_staff = True - else: - user.is_staff = False + break + user.first_name = attributes['givenName'][0] user.last_name = attributes['sn'][0] force_save = True diff --git a/userportal/settings/40-saml.py b/userportal/settings/40-saml.py index f55f36b..f566d97 100644 --- a/userportal/settings/40-saml.py +++ b/userportal/settings/40-saml.py @@ -64,4 +64,11 @@ 'key_file': '/opt/private.key', # private part 'cert_file': '/opt/public.cert', # public part }], -} \ No newline at end of file + # Use this to define if the user can login + 'required_access_attributes': [], + + # Use this to assign the staff role based on attributes returned by SAML + 'staff_attributes': [ + ('eduPersonAffiliation', 'staff') + ] +} From fe6cddc71635c7a3a60a4e74fd6018728d2fa132 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Fri, 10 Apr 2026 16:36:04 -0400 Subject: [PATCH 2/5] moved djangosaml2 to a try except to define the class only if djangosaml2 is installed --- userportal/authentication.py | 48 +++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/userportal/authentication.py b/userportal/authentication.py index 20ac61e..03b314b 100644 --- a/userportal/authentication.py +++ b/userportal/authentication.py @@ -1,29 +1,33 @@ -from djangosaml2.backends import Saml2Backend from django.contrib.auth.backends import RemoteUserBackend - from django.conf import settings +try: + from djangosaml2.backends import Saml2Backend + + class staffSaml2Backend(Saml2Backend): + """This will add/remove the is_staff attribute from the user as appropriate.""" + def _update_user(self, user, attributes: dict, attribute_mapping: dict, force_save: bool = False): + # figure out if user is active (i.e. can login) + user.is_active = True + for attribute, value in settings.SAML_CONFIG['required_access_attributes']: + if attribute not in attributes or value not in attributes[attribute]: + user.is_active = False + + # figure out if user is staff + user.is_staff = False + for attribute, value in settings.SAML_CONFIG['staff_attributes']: + if attribute in attributes and value in attributes[attribute]: + user.is_staff = True + break + + user.first_name = attributes['givenName'][0] + user.last_name = attributes['sn'][0] + force_save = True + return super()._update_user(user, attributes, attribute_mapping, force_save) + -class staffSaml2Backend(Saml2Backend): - """This will add/remove the is_staff attribute from the user as appropriate.""" - def _update_user(self, user, attributes: dict, attribute_mapping: dict, force_save: bool = False): - # figure out if user is active (i.e. can login) - user.is_active = True - for attribute, value in settings.SAML_CONFIG['required_access_attributes']: - if attribute not in attributes or value not in attributes[attribute]: - user.is_active = False - - # figure out if user is staff - user.is_staff = False - for attribute, value in settings.SAML_CONFIG['staff_attributes']: - if attribute in attributes and value in attributes[attribute]: - user.is_staff = True - break - - user.first_name = attributes['givenName'][0] - user.last_name = attributes['sn'][0] - force_save = True - return super()._update_user(user, attributes, attribute_mapping, force_save) +except ImportError: + pass class staffRemoteUserBackend(RemoteUserBackend): From bdbf8d8b625f49955075e8ae67feb2cf3f0ca357 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Fri, 10 Apr 2026 16:43:56 -0400 Subject: [PATCH 3/5] make is_staff configurable based on LDAP attributes --- userportal/authentication.py | 20 ++++++++++++++++++++ userportal/settings/10-base.py | 2 ++ 2 files changed, 22 insertions(+) diff --git a/userportal/authentication.py b/userportal/authentication.py index 9bc37cb..36b2026 100644 --- a/userportal/authentication.py +++ b/userportal/authentication.py @@ -1,5 +1,6 @@ from djangosaml2.backends import Saml2Backend from django.contrib.auth.backends import RemoteUserBackend +from django.conf import settings class staffSaml2Backend(Saml2Backend): @@ -32,3 +33,22 @@ def configure_user(self, request, user, created=True): user.is_staff = False user.save() return user + + +try: + from django_auth_ldap.backend import LDAPBackend + + class staffLdapBackend(LDAPBackend): + def get_or_build_user(self, username, ldap_user): + user, built = super().get_or_build_user(username, ldap_user) + + user.is_staff = False + for attribute, value in settings.LDAP_CONFIG['staff_attributes']: + if attribute in ldap_user.attrs.data and value in ldap_user.attrs.data[attribute]: + user.is_staff = True + + return user, built + + +except ImportError: + pass diff --git a/userportal/settings/10-base.py b/userportal/settings/10-base.py index 8eab4bc..e56a61b 100644 --- a/userportal/settings/10-base.py +++ b/userportal/settings/10-base.py @@ -152,6 +152,8 @@ LDAP_BASE_DN = 'dc=computecanada,dc=ca' +LDAP_CONFIG = {} + LOGIN_REDIRECT_URL = '/' # Set to DEMO to True to enable demo mode with anonymized data From 54e0cea0abe646189243b6772b3846a35d2b8c89 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Fri, 17 Apr 2026 08:24:07 -0400 Subject: [PATCH 4/5] also add required_access_attributes to LDAP backend --- userportal/authentication.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/userportal/authentication.py b/userportal/authentication.py index 6d8e5b3..cfda28e 100644 --- a/userportal/authentication.py +++ b/userportal/authentication.py @@ -55,6 +55,12 @@ class staffLdapBackend(LDAPBackend): def get_or_build_user(self, username, ldap_user): user, built = super().get_or_build_user(username, ldap_user) + # figure out if user is active (i.e. can login) + user.is_active = True + for attribute, value in settings.LDAP_CONFIG['required_access_attributes']: + if attribute not in ldap_user.attrs.data or value not in ldap_user.attrs.data[attribute]: + user.is_active = False + user.is_staff = False for attribute, value in settings.LDAP_CONFIG['staff_attributes']: if attribute in ldap_user.attrs.data and value in ldap_user.attrs.data[attribute]: From ac2a9aca8ffdaa5cb61a24345bfc9c3842515bc1 Mon Sep 17 00:00:00 2001 From: Maxime Boissonneault Date: Fri, 17 Apr 2026 08:28:26 -0400 Subject: [PATCH 5/5] no need to continue the loop if one attribute is missing --- userportal/authentication.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/userportal/authentication.py b/userportal/authentication.py index cfda28e..199a07c 100644 --- a/userportal/authentication.py +++ b/userportal/authentication.py @@ -12,6 +12,7 @@ def _update_user(self, user, attributes: dict, attribute_mapping: dict, force_sa for attribute, value in settings.SAML_CONFIG['required_access_attributes']: if attribute not in attributes or value not in attributes[attribute]: user.is_active = False + break # figure out if user is staff user.is_staff = False @@ -60,6 +61,7 @@ def get_or_build_user(self, username, ldap_user): for attribute, value in settings.LDAP_CONFIG['required_access_attributes']: if attribute not in ldap_user.attrs.data or value not in ldap_user.attrs.data[attribute]: user.is_active = False + break user.is_staff = False for attribute, value in settings.LDAP_CONFIG['staff_attributes']: