Skip to content

Commit

Permalink
Copy signals.py and SAML groups changes from dead sal-saml to sal.
Browse files Browse the repository at this point in the history
  • Loading branch information
sheagcraig committed Dec 18, 2024
1 parent 2f230dd commit 2d0ca44
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 1 deletion.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ _The following instructions are provided as a best effort to help get started. T
- `single_sign_on_service` Ex: <https://apps.onelogin.com/trust/saml2/http-post/sso/1234567890>
- `single_logout_service` Ex: <https://apps.onelogin.com/trust/saml2/http-redirect/slo/1234567890>

## Using groups in the SAML assertion to assign Sal profiles
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.

To take advantage of this, edit the settings.py that comes with sal-saml for these preferences:
- `SAML_GROUPS_ATTRIBUTE`: Default (`memberOf`) The assertion dict's key for the group membership attribute.
- `SAML_READ_ONLY_GROUPS`: Default `[]` (empty list) List of groups who should be given read-only access.
- `SAML_READ_WRITE_GROUPS`: Default `[]` (empty list) List of groups who should be given read-write access.
- `SAML_GLOBAL_ADMIN_GROUPS` Default `[]` (empty list) List of groups who should be given global admin access. This includes access to the admin site.

For example:
```
SAML_READ_ONLY_GROUPS = ['cn=regular_shorts_wearers,ou=memberOf,dc=blutwurst,dc=com', 'cn=nontraditional_pants_krew,ou=memberOf,dc=blutwurst,dc=com']
SAML_GLOBAL_ADMIN_GROUPS` = ['cn=lederhosen_club,ou=memberOf,dc=blutwurst,dc=com']
```

## An example Docker run

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.
Expand Down
14 changes: 13 additions & 1 deletion docker/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@
"sn": ("last_name",),
}

# Edit these lists to include the names of groups that should get
# the access levels below. See server/signals.py for more details.
# Leave blank to disable the group-based permissions feature.
SAML_READ_ONLY_GROUPS = []
SAML_READ_WRITE_GROUPS = []
SAML_GLOBAL_ADMIN_GROUPS = []
# Edit to match the attribute name used in your SAML assertions for
# group membership information.
SAML_GROUPS_ATTRIBUTE = 'memberOf'

logging_config = get_sal_logging_config()
if DEBUG:
level = "DEBUG"
Expand Down Expand Up @@ -147,11 +157,13 @@
"authn_requests_signed": False,
"allow_unsolicited": True,
"want_assertions_signed": True,
# Allow SAML assertions to contain attributes not specified in the
# attributemaps.
"allow_unknown_attributes": True,
"name": "Federated Django sample SP",
"name_id_format": NAMEID_FORMAT_PERSISTENT,
"endpoints": {
# url and binding to the assetion consumer service view
# url and binding to the assertion consumer service view
# do not change the binding or service name
"assertion_consumer_service": [
("https://sal.example.com/saml2/acs/", saml2.BINDING_HTTP_POST),
Expand Down
3 changes: 3 additions & 0 deletions server/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
class ServerAppConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = "server"

def ready(self):
import server.signals
66 changes: 66 additions & 0 deletions server/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from django.dispatch import receiver

from djangosaml2.signals import pre_user_save

from server.models import UserProfile, ProfileLevel
from server.utils import get_django_setting


READ_ONLY_GROUPS = set(get_django_setting('SAML_READ_ONLY_GROUPS', []))
READ_WRITE_GROUPS = set(get_django_setting('SAML_READ_WRITE_GROUPS', []))
GLOBAL_ADMIN_GROUPS = set(get_django_setting('SAML_GLOBAL_ADMIN_GROUPS', []))
GROUPS_ATTRIBUTE = get_django_setting('SAML_GROUPS_ATTRIBUTE', 'memberOf')


@receiver(pre_user_save)
def update_group_membership(
sender, instance, attributes: dict, user_modified: bool, **kwargs) -> bool:
"""Update user's group membership based on passed SAML groups
Sal access level is based on the highest access level granted across
all groups a user is a member of. For example, if you are in a group
with RO access and a group with GA access, the GA level "wins".
Users who have no group membership in any of the configured
SAML_X_GROUPS settings will be unchanged, allowing changes to these
users via the admin panel to persist.
Args:
sender: The class of the user that just logged in.
instance: User instance
attributes: SAML attributes dict.
user_modified: Bool whether the user has been modified
kwargs:
signal: The signal instance
Returns:
Whether or not the user has been modified. This allows the user
instance to be saved once at the conclusion of the auth process
to keep the writes to a minimum.
"""
assertion_groups = set(attributes.get(GROUPS_ATTRIBUTE, []))
if GLOBAL_ADMIN_GROUPS.intersection(assertion_groups):
instance.userprofile.delete()
user_profile = UserProfile(user=instance, level=ProfileLevel.global_admin)
user_profile.save()
instance.is_superuser = True
instance.is_staff = True
instance.is_active = True
user_modified = True
elif READ_WRITE_GROUPS.intersection(assertion_groups):
instance.userprofile.delete()
user_profile = UserProfile(user=instance, level=ProfileLevel.read_write)
user_profile.save()
instance.is_superuser = False
instance.is_staff = False
instance.is_active = True
user_modified = True
elif READ_ONLY_GROUPS.intersection(assertion_groups):
instance.userprofile.delete()
user_profile = UserProfile(user=instance, level=ProfileLevel.read_only)
user_profile.save()
instance.is_superuser = False
instance.is_staff = False
instance.is_active = True
user_modified = True
return user_modified

0 comments on commit 2d0ca44

Please sign in to comment.