Skip to content

Commit

Permalink
allow mapping SAML2 IdP groups to local ones
Browse files Browse the repository at this point in the history
  • Loading branch information
fopina committed Nov 16, 2024
1 parent 73f0667 commit 2685839
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 2 deletions.
82 changes: 82 additions & 0 deletions dojo/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import logging
import re
from functools import cached_property

from django.conf import settings
from djangosaml2.backends import Saml2Backend as _Saml2Backend

from dojo.authorization.roles_permissions import Roles
from dojo.models import Dojo_Group, Dojo_Group_Member, Role

logger = logging.getLogger(__name__)


class Saml2Backend(_Saml2Backend):

"""Subclass to handle adding SAML2 groups as DefectDojo/Django groups to a user"""

@cached_property
def group_re(self):
if not settings.SAML2_ENABLED or not settings.SAML2_GROUPS_ATTRIBUTE or not settings.SAML2_GROUPS_FILTER:
return None
return re.compile(settings.SAML2_GROUPS_FILTER)

def _update_user(
self,
user,
attributes: dict,
attribute_mapping: dict,
force_save=False,
):
"""
Method overriden to handle groups after user object is saved.
Ideally we would only override "public" methods: in this case, get_or_create_user() would be the one but it doesn't save the NEW user
We could override that AND save_user() (each to handle new or existing users) but the latter does not receive the attributes which include the groups...
This does NOT create the groups if they do not exist. They have to be created in the UI
This is not a big issue and it works around an existing bug with dojo/group/utils.py::group_post_save_handler (user does not yet exist and he is forcefully added to the new group - boom)
"""
user = super()._update_user(user, attributes, attribute_mapping, force_save=force_save)
if self.group_re is None:
return user

# list of all existing "SAML2-mapped" groups
all_saml_groups = {group.name: group for group in Dojo_Group.objects.all() if self.group_re.match(group.name)}

# list of groups user MUST have
needs_groups = set()
if attributes[settings.SAML2_GROUPS_ATTRIBUTE]:
needs_groups.update(
group_name
for group_name in attributes[settings.SAML2_GROUPS_ATTRIBUTE]
if self.group_re.match(group_name)
)

# list of groups user ALREADY has
has_groups = {
dgm.group.name: dgm
for dgm in Dojo_Group_Member.objects.filter(user_id=user.id).select_related("group")
if dgm.group.name in all_saml_groups
}

groups_to_remove = has_groups.keys() - needs_groups
groups_to_add = needs_groups - has_groups.keys()

if groups_to_remove:
# bulk .delete() can be used as it emits post_delete signal
deleted, _ = Dojo_Group_Member.objects.filter(user_id=user.id, group__name__in=groups_to_remove).delete()
logger.info("User %s removed from SAML2 groups: %s", user, ", ".join(groups_to_remove))
if deleted != len(groups_to_remove):
logger.error("User %s had %d groups to be removed but %d were", user, len(groups_to_remove), deleted)

if groups_to_add:
# .bulk_create() cannot be used as it does NOT emit post_save signal
reader_role = Role.objects.get(id=Roles.Reader)
for group_name in groups_to_add:
group = all_saml_groups.get(group_name)
if group is None:
logger.error("Group %s is mapped for SAML2 but it does not exist in Dojo", group_name)
else:
Dojo_Group_Member.objects.create(group=group, user_id=user.pk, role=reader_role)
logger.debug("User %s became member of SAML2 group: %s", user, group.name)
return user
2 changes: 1 addition & 1 deletion dojo/settings/.settings.dist.py.sha256sum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
fc660db6c2f55181fd8515d9b13c75197d8272c5c635235f6f60e4b1fc77af04
4e8c4505c83679c75ad310c9f00040aae8740457625ef3c03a5de8f08170d66c
9 changes: 8 additions & 1 deletion dojo/settings/settings.dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@
DD_SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET=(str, ""),
DD_SAML2_ENABLED=(bool, False),
# Allows to override default SAML authentication backend. Check https://djangosaml2.readthedocs.io/contents/setup.html#custom-user-attributes-processing
DD_SAML2_AUTHENTICATION_BACKENDS=(str, "djangosaml2.backends.Saml2Backend"),
DD_SAML2_AUTHENTICATION_BACKENDS=(str, "dojo.backends.Saml2Backend"),
# Force Authentication to make SSO possible with SAML2
DD_SAML2_FORCE_AUTH=(bool, True),
DD_SAML2_LOGIN_BUTTON_TEXT=(str, "Login with SAML"),
Expand All @@ -177,6 +177,11 @@
"Lastname": "last_name",
}),
DD_SAML2_ALLOW_UNKNOWN_ATTRIBUTE=(bool, False),
# similar to DD_SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GROUPS_FILTER, regular expression for which SAML2 groups to map to Dojo groups (with same name)
# Groups need to already exist in Dojo. And if value is not set, no group processing is done
DD_SAML2_GROUPS_FILTER=(str, ""),
# SAML2 attribute with groups to match in Dojo. And if value is not set, no group processing is done
DD_SAML2_GROUPS_ATTRIBUTE=(str, ""),
# Authentication via HTTP Proxy which put username to HTTP Header REMOTE_USER
DD_AUTH_REMOTEUSER_ENABLED=(bool, False),
# Names of headers which will be used for processing user data.
Expand Down Expand Up @@ -1054,6 +1059,8 @@ def saml2_attrib_map_format(dict):
},
"valid_for": 24, # how long is our metadata valid
}
SAML2_GROUPS_ATTRIBUTE = env("DD_SAML2_GROUPS_ATTRIBUTE")
SAML2_GROUPS_FILTER = env("DD_SAML2_GROUPS_FILTER")

# ------------------------------------------------------------------------------
# REMOTE_USER
Expand Down

0 comments on commit 2685839

Please sign in to comment.