From 1f1aaec5d8f3f795b2578808e6b5635fd0125be6 Mon Sep 17 00:00:00 2001
From: Art Lukyanchyk <artiom.lukyanchyk@hs-hannover.de>
Date: Mon, 4 Mar 2019 13:53:03 +0100
Subject: [PATCH] Allow receiving isMemberOf (1.3.6.1.4.1.5923.1.5.1.1) in both
 DN and group name format

---
 ssoauth/app_settings/defaults.py |  4 ++--
 ssoauth/auth_utils.py            | 40 ++++++++++++++++++++++----------
 ssoauth/views.py                 |  2 +-
 3 files changed, 31 insertions(+), 15 deletions(-)

diff --git a/ssoauth/app_settings/defaults.py b/ssoauth/app_settings/defaults.py
index a0ab768..5cf1fad 100644
--- a/ssoauth/app_settings/defaults.py
+++ b/ssoauth/app_settings/defaults.py
@@ -31,7 +31,7 @@ SSO_REQUIRED_OUTSIDE_MANAGE_PY = True  # enabled to ensure that production (that
 SP_SLS_ENABLED = False  # single log out creates too many problems, so it is disabled for now
 SP_SLS_X_FRAME_OPTIONS = None  # in case you encounter problems with SLS view not allowed inside of an iframe, e.g. "ALLOW-FROM idp-test.it.hs-hannover.de idp.hs-hannover.de"
 
-GROUP_RESOLVER = "ssoauth.auth_utils.groups_from_saml2_dn_list"  # in case you want to override how groups are resolved for users
+GROUP_RESOLVER = "ssoauth.auth_utils.groups_from_raw_saml2_values"  # in case you want to override how groups are resolved for users
 
 LOGIN_PERM_CODENAME = None  # None or str; value "can_log_in" will require this permission for users to log in
 
@@ -83,7 +83,7 @@ ATTRIBUTE_USERNAME = "urn:oid:0.9.2342.19200300.100.1.1"  # "uid"
 ATTRIBUTE_EMAIL = "urn:oid:0.9.2342.19200300.100.1.3"  # "mail"
 ATTRIBUTE_FORENAME = "urn:oid:2.5.4.42"  # "givenName"
 ATTRIBUTE_SURNAME = "urn:oid:2.5.4.4"  # "sn"
-ATTRIBUTE_ACCOUNT_UUID = "UUID"  # our custom stuff, this one has no OID
+ATTRIBUTE_ACCOUNT_UUID = "UUID"  # custom stuff, this one has no OID
 ATTRIBUTE_GROUPS = "urn:oid:1.3.6.1.4.1.5923.1.5.1.1"  # "isMemberOf"
 
 
diff --git a/ssoauth/auth_utils.py b/ssoauth/auth_utils.py
index 1acfa8b..5a501cc 100644
--- a/ssoauth/auth_utils.py
+++ b/ssoauth/auth_utils.py
@@ -8,6 +8,7 @@ from . import logger
 from . import app_settings
 import functools
 import importlib
+import re
 
 
 def _validate_username(username):
@@ -97,7 +98,7 @@ def update_user_data(user, surname=None, forename=None, email=None):
     user.save()
 
 
-def set_user_groups(user, saml2_groups):
+def set_user_groups(user, raw_values):
     """ Updates groups for the user. """
     # get the group resolver method
     try:
@@ -110,27 +111,42 @@ def set_user_groups(user, saml2_groups):
     except Exception as e:
         raise ImportError("Could not import {r}. {e.__class__.__name__}: {e}".format(r=app_settings.GROUP_RESOLVER, e=e))
     # resolve the groups
-    groups = resolver_method(user, saml2_groups)
-    assert isinstance(groups, (list, tuple, set,)) and all(isinstance(g, Group) for g in groups)
+    groups = set(resolver_method(user, raw_values))
     # update user groups
-    if set(user.groups.all()) != set(groups):
+    if set(user.groups.all()) != groups:
         user.groups.set(groups)
-        assert set(user.groups.all()) == set(groups)
+        assert set(user.groups.all()) == groups
         logger.info("Groups for {user} were updated.".format(user=user))
     # done
     logger.info("User {user} is member of: {groups}".format(user=user, groups=set(str(g) for g in groups)))
 
 
-def groups_from_saml2_dn_list(user, groups_dn_list):
+def groups_from_raw_saml2_values(user, raw_values):
     """ This default group resolver returns list of Group objects based on DN list """
     # using Q to create ignore-case DN lookup since DS is case insensitive
-    q_list = [Q(sso_mapping__dn__iexact=dn) for dn in groups_dn_list]
-    if q_list:
-        q_combined = functools.reduce(lambda q1, q2: q1 | q2, q_list)
-        groups = list(Group.objects.filter(q_combined))
+
+    def from_dn_list(dn_list):
+        logger.warning("Receiving groups from DN list is deprecated. Reconfigure the project to receive group names instead.")
+        q_list = [Q(sso_mapping__dn__iexact=dn) for dn in dn_list]
+        if q_list:
+            q_combined = functools.reduce(lambda q1, q2: q1 | q2, q_list)
+            groups = set(Group.objects.filter(q_combined))
+        else:
+            groups = set()
+        return groups
+
+    def from_group_name_list(group_names):
+        groups = set(Group.objects.filter(name__in=group_names))
+        group_names_resolved = {g.name for g in groups}
+        group_names_redundant = set(group_names) - group_names_resolved
+        if group_names_redundant:
+            logger.warning("Received redundant group names (clean up the configs): {0}".format(group_names_redundant))
+        return groups
+
+    if raw_values and bool(re.match('^\w+=\w+', str(raw_values[0]))):  # if looks like DN
+        return from_dn_list(raw_values)
     else:
-        groups = list()
-    return groups
+        return from_group_name_list(raw_values)
 
 
 def cleanup_direct_permissions(user):
diff --git a/ssoauth/views.py b/ssoauth/views.py
index 142ba2c..fcab98b 100644
--- a/ssoauth/views.py
+++ b/ssoauth/views.py
@@ -199,7 +199,7 @@ class ACSAuthNView(SAMLMixin, View):
         )
         auth_utils.set_user_groups(
             user=user,
-            saml2_groups=get_attr(app_settings.ATTRIBUTE_GROUPS, nullable=True, multivalued=True) or list()
+            raw_values=get_attr(app_settings.ATTRIBUTE_GROUPS, nullable=True, multivalued=True) or list()
         )
         # other necessary steps
         auth_utils.cleanup_direct_permissions(user=user)  # in case somebody was acting funny
-- 
GitLab