From b5fbe28146d470b1ce7c46f7f81e97e11154f9ac Mon Sep 17 00:00:00 2001 From: Jenny Danzmayr Date: Mon, 16 Sep 2024 13:20:19 +0200 Subject: [PATCH] added feature to grant access permissions via SSO groups --- src/c3nav/control/admin.py | 6 +++ src/c3nav/control/sso/pipeline.py | 29 +++++++++++++ ...sionssogrant_accesspermission_sso_grant.py | 42 +++++++++++++++++++ src/c3nav/mapdata/models/access.py | 24 +++++++++++ src/c3nav/settings.py | 1 + 5 files changed, 102 insertions(+) create mode 100644 src/c3nav/control/sso/pipeline.py create mode 100644 src/c3nav/mapdata/migrations/0109_accesspermissionssogrant_accesspermission_sso_grant.py diff --git a/src/c3nav/control/admin.py b/src/c3nav/control/admin.py index 59216378..db9228dd 100644 --- a/src/c3nav/control/admin.py +++ b/src/c3nav/control/admin.py @@ -5,6 +5,7 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ from c3nav.control.models import UserPermissions +from c3nav.mapdata.models.access import AccessPermissionSSOGrant class UserPermissionsInline(admin.StackedInline): @@ -27,3 +28,8 @@ class UserAdmin(BaseUserAdmin): admin.site.unregister(User) admin.site.register(User, UserAdmin) + + +@admin.register(AccessPermissionSSOGrant) +class AccessPermissionSSOGrantAdmin(admin.ModelAdmin): + model = AccessPermissionSSOGrant diff --git a/src/c3nav/control/sso/pipeline.py b/src/c3nav/control/sso/pipeline.py new file mode 100644 index 00000000..2baf1997 --- /dev/null +++ b/src/c3nav/control/sso/pipeline.py @@ -0,0 +1,29 @@ +from c3nav.mapdata.models.access import AccessPermission, AccessPermissionSSOGrant + + +def access_permissions(backend, response, user=None, *args, **kwargs): + """Grant access permissions using group membership from provider.""" + if not user or (groups := response.get('groups')) is None: + return + + # delete permissions granted to the user by groups they no longer are part of + user.accesspermissions.filter(sso_grant__provider=backend.name).exclude(sso_grant__group__in=groups).delete() + + if not groups: + return + + existing_grants = set(AccessPermission.objects.filter(sso_grant__provider=backend.name) + .values_list('sso_grant_id', flat=True)) + new_grants = AccessPermissionSSOGrant.objects.filter(provider=backend.name, group__in=groups) \ + .exclude(id__in=existing_grants) + + new_perms = [] + for grant in new_grants: + new_perms.append(AccessPermission( + user=user, + access_restriction_id=grant.access_restriction_id, + access_restriction_group_id=grant.access_restriction_group_id, + sso_grant=grant + )) + if new_grants: + AccessPermission.objects.bulk_create(new_perms) diff --git a/src/c3nav/mapdata/migrations/0109_accesspermissionssogrant_accesspermission_sso_grant.py b/src/c3nav/mapdata/migrations/0109_accesspermissionssogrant_accesspermission_sso_grant.py new file mode 100644 index 00000000..fa200556 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0109_accesspermissionssogrant_accesspermission_sso_grant.py @@ -0,0 +1,42 @@ +# Generated by Django 5.0.8 on 2024-09-12 21:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0108_in_legend'), + ] + + operations = [ + migrations.CreateModel( + name='AccessPermissionSSOGrant', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('provider', models.CharField(max_length=32, verbose_name='SSO Backend')), + ('group', models.CharField(max_length=64, verbose_name='SSO Group')), + ('access_restriction', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mapdata.accessrestriction')), + ('access_restriction_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mapdata.accessrestrictiongroup')), + ], + options={ + 'verbose_name': 'Access Permission SSO Grant', + 'verbose_name_plural': 'Access Permission SSO Grants', + 'default_related_name': 'accesspermission_sso_grants', + }, + ), + migrations.AddField( + model_name='accesspermission', + name='sso_grant', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='mapdata.accesspermissionssogrant', verbose_name='Access Permission SSO Grant'), + ), + migrations.AddConstraint( + model_name='accesspermissionssogrant', + constraint=models.CheckConstraint(check=models.Q(models.Q(('access_restriction__isnull', True), ('access_restriction_group__isnull', True), _negated=True), models.Q(('access_restriction__isnull', False), ('access_restriction_group__isnull', False), _negated=True)), name='sso_permission_grant_needs_restriction_or_restriction_group'), + ), + migrations.AlterUniqueTogether( + name='accesspermissionssogrant', + unique_together={('provider', 'group', 'access_restriction', 'access_restriction_group')}, + ), + ] diff --git a/src/c3nav/mapdata/models/access.py b/src/c3nav/mapdata/models/access.py index ff7e7fc0..4c27582c 100644 --- a/src/c3nav/mapdata/models/access.py +++ b/src/c3nav/mapdata/models/access.py @@ -187,6 +187,28 @@ class AccessPermissionToken(models.Model): return ngettext_lazy('Area successfully unlocked.', 'Areas successfully unlocked.', len(self.restrictions)) +class AccessPermissionSSOGrant(models.Model): + + provider = models.CharField(max_length=32, verbose_name=_('SSO Backend')) + group = models.CharField(max_length=64, verbose_name=_('SSO Group')) + access_restriction = models.ForeignKey(AccessRestriction, on_delete=models.CASCADE, null=True, blank=True) + access_restriction_group = models.ForeignKey(AccessRestrictionGroup, on_delete=models.CASCADE, null=True, + blank=True) + + class Meta: + verbose_name = _('Access Permission SSO Grant') + verbose_name_plural = _('Access Permission SSO Grants') + default_related_name = 'accesspermission_sso_grants' + unique_together = ( + ('provider', 'group', 'access_restriction', 'access_restriction_group') + ) + constraints = ( + CheckConstraint(check=(~Q(access_restriction__isnull=True, access_restriction_group__isnull=True) & + ~Q(access_restriction__isnull=False, access_restriction_group__isnull=False)), + name="sso_permission_grant_needs_restriction_or_restriction_group"), + ) + + class AccessPermission(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE) session_token = models.UUIDField(null=True, editable=False) @@ -199,6 +221,8 @@ class AccessPermission(models.Model): unique_key = models.CharField(max_length=32, null=True, verbose_name=_('unique key')) token = models.ForeignKey(AccessPermissionToken, null=True, on_delete=models.CASCADE, verbose_name=_('Access permission token')) + sso_grant = models.ForeignKey(AccessPermissionSSOGrant, null=True, on_delete=models.CASCADE, + verbose_name=_('Access Permission SSO Grant')) class Meta: verbose_name = _('Access Permission') diff --git a/src/c3nav/settings.py b/src/c3nav/settings.py index e1a0ae0a..8ded0d0d 100644 --- a/src/c3nav/settings.py +++ b/src/c3nav/settings.py @@ -809,4 +809,5 @@ SOCIAL_AUTH_PIPELINE = ( 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', + 'c3nav.control.sso.pipeline.access_permissions', )