Skip to content

Commit 2d0ca44

Browse files
committed
Copy signals.py and SAML groups changes from dead sal-saml to sal.
1 parent 2f230dd commit 2d0ca44

File tree

4 files changed

+97
-1
lines changed

4 files changed

+97
-1
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,21 @@ _The following instructions are provided as a best effort to help get started. T
5454
- `single_sign_on_service` Ex: <https://apps.onelogin.com/trust/saml2/http-post/sso/1234567890>
5555
- `single_logout_service` Ex: <https://apps.onelogin.com/trust/saml2/http-redirect/slo/1234567890>
5656

57+
## Using groups in the SAML assertion to assign Sal profiles
58+
Sal-saml adds a Django signal callback to act on group membership information passed in a SAML assertion during login. If you can configure your IdP to add group information, you can use it to automate the addition and revocation of permissions.
59+
60+
To take advantage of this, edit the settings.py that comes with sal-saml for these preferences:
61+
- `SAML_GROUPS_ATTRIBUTE`: Default (`memberOf`) The assertion dict's key for the group membership attribute.
62+
- `SAML_READ_ONLY_GROUPS`: Default `[]` (empty list) List of groups who should be given read-only access.
63+
- `SAML_READ_WRITE_GROUPS`: Default `[]` (empty list) List of groups who should be given read-write access.
64+
- `SAML_GLOBAL_ADMIN_GROUPS` Default `[]` (empty list) List of groups who should be given global admin access. This includes access to the admin site.
65+
66+
For example:
67+
```
68+
SAML_READ_ONLY_GROUPS = ['cn=regular_shorts_wearers,ou=memberOf,dc=blutwurst,dc=com', 'cn=nontraditional_pants_krew,ou=memberOf,dc=blutwurst,dc=com']
69+
SAML_GLOBAL_ADMIN_GROUPS` = ['cn=lederhosen_club,ou=memberOf,dc=blutwurst,dc=com']
70+
```
71+
5772
## An example Docker run
5873

5974
Please note that this Docker run is **incomplete**, but shows where to pass the `metadata.xml` and `settings.py`. Also note, `latest` in the below run should not be used unless you have a real reason (needing a development version). When performing `docker run`, you should substitute `latest` for the latest tagged release.

docker/settings.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@
109109
"sn": ("last_name",),
110110
}
111111

112+
# Edit these lists to include the names of groups that should get
113+
# the access levels below. See server/signals.py for more details.
114+
# Leave blank to disable the group-based permissions feature.
115+
SAML_READ_ONLY_GROUPS = []
116+
SAML_READ_WRITE_GROUPS = []
117+
SAML_GLOBAL_ADMIN_GROUPS = []
118+
# Edit to match the attribute name used in your SAML assertions for
119+
# group membership information.
120+
SAML_GROUPS_ATTRIBUTE = 'memberOf'
121+
112122
logging_config = get_sal_logging_config()
113123
if DEBUG:
114124
level = "DEBUG"
@@ -147,11 +157,13 @@
147157
"authn_requests_signed": False,
148158
"allow_unsolicited": True,
149159
"want_assertions_signed": True,
160+
# Allow SAML assertions to contain attributes not specified in the
161+
# attributemaps.
150162
"allow_unknown_attributes": True,
151163
"name": "Federated Django sample SP",
152164
"name_id_format": NAMEID_FORMAT_PERSISTENT,
153165
"endpoints": {
154-
# url and binding to the assetion consumer service view
166+
# url and binding to the assertion consumer service view
155167
# do not change the binding or service name
156168
"assertion_consumer_service": [
157169
("https://sal.example.com/saml2/acs/", saml2.BINDING_HTTP_POST),

server/apps.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
class ServerAppConfig(AppConfig):
55
default_auto_field = 'django.db.models.AutoField'
66
name = "server"
7+
8+
def ready(self):
9+
import server.signals

server/signals.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from django.dispatch import receiver
2+
3+
from djangosaml2.signals import pre_user_save
4+
5+
from server.models import UserProfile, ProfileLevel
6+
from server.utils import get_django_setting
7+
8+
9+
READ_ONLY_GROUPS = set(get_django_setting('SAML_READ_ONLY_GROUPS', []))
10+
READ_WRITE_GROUPS = set(get_django_setting('SAML_READ_WRITE_GROUPS', []))
11+
GLOBAL_ADMIN_GROUPS = set(get_django_setting('SAML_GLOBAL_ADMIN_GROUPS', []))
12+
GROUPS_ATTRIBUTE = get_django_setting('SAML_GROUPS_ATTRIBUTE', 'memberOf')
13+
14+
15+
@receiver(pre_user_save)
16+
def update_group_membership(
17+
sender, instance, attributes: dict, user_modified: bool, **kwargs) -> bool:
18+
"""Update user's group membership based on passed SAML groups
19+
20+
Sal access level is based on the highest access level granted across
21+
all groups a user is a member of. For example, if you are in a group
22+
with RO access and a group with GA access, the GA level "wins".
23+
24+
Users who have no group membership in any of the configured
25+
SAML_X_GROUPS settings will be unchanged, allowing changes to these
26+
users via the admin panel to persist.
27+
28+
Args:
29+
sender: The class of the user that just logged in.
30+
instance: User instance
31+
attributes: SAML attributes dict.
32+
user_modified: Bool whether the user has been modified
33+
kwargs:
34+
signal: The signal instance
35+
36+
Returns:
37+
Whether or not the user has been modified. This allows the user
38+
instance to be saved once at the conclusion of the auth process
39+
to keep the writes to a minimum.
40+
"""
41+
assertion_groups = set(attributes.get(GROUPS_ATTRIBUTE, []))
42+
if GLOBAL_ADMIN_GROUPS.intersection(assertion_groups):
43+
instance.userprofile.delete()
44+
user_profile = UserProfile(user=instance, level=ProfileLevel.global_admin)
45+
user_profile.save()
46+
instance.is_superuser = True
47+
instance.is_staff = True
48+
instance.is_active = True
49+
user_modified = True
50+
elif READ_WRITE_GROUPS.intersection(assertion_groups):
51+
instance.userprofile.delete()
52+
user_profile = UserProfile(user=instance, level=ProfileLevel.read_write)
53+
user_profile.save()
54+
instance.is_superuser = False
55+
instance.is_staff = False
56+
instance.is_active = True
57+
user_modified = True
58+
elif READ_ONLY_GROUPS.intersection(assertion_groups):
59+
instance.userprofile.delete()
60+
user_profile = UserProfile(user=instance, level=ProfileLevel.read_only)
61+
user_profile.save()
62+
instance.is_superuser = False
63+
instance.is_staff = False
64+
instance.is_active = True
65+
user_modified = True
66+
return user_modified

0 commit comments

Comments
 (0)