From a05667cc030d3525dd0353396b879e2a90cb6101 Mon Sep 17 00:00:00 2001 From: Dominik George <dominik.george@teckids.org> Date: Thu, 4 Nov 2021 10:36:12 +0100 Subject: [PATCH] [OAuth] Use preference to define allowed grant flows --- .../0023_oauth_application_model.py | 41 +++++++++++++++++++ aleksis/core/models.py | 10 +++++ aleksis/core/preferences.py | 14 +++++++ aleksis/core/settings.py | 2 +- aleksis/core/views.py | 12 +++--- 5 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 aleksis/core/migrations/0023_oauth_application_model.py diff --git a/aleksis/core/migrations/0023_oauth_application_model.py b/aleksis/core/migrations/0023_oauth_application_model.py new file mode 100644 index 000000000..7e5d2812f --- /dev/null +++ b/aleksis/core/migrations/0023_oauth_application_model.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.8 on 2021-11-04 09:52 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import oauth2_provider.generators + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0022_public_favicon'), + ] + + run_before = [ + ('oauth2_provider', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='OAuthApplication', + fields=[ + ('id', models.BigAutoField(primary_key=True, serialize=False)), + ('client_id', models.CharField(db_index=True, default=oauth2_provider.generators.generate_client_id, max_length=100, unique=True)), + ('redirect_uris', models.TextField(blank=True, help_text='Allowed URIs list, space separated')), + ('client_type', models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], max_length=32)), + ('authorization_grant_type', models.CharField(choices=[('authorization-code', 'Authorization code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=32)), + ('client_secret', models.CharField(blank=True, db_index=True, default=oauth2_provider.generators.generate_client_secret, max_length=255)), + ('name', models.CharField(blank=True, max_length=255)), + ('skip_authorization', models.BooleanField(default=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('algorithm', models.CharField(blank=True, choices=[('', 'No OIDC support'), ('RS256', 'RSA with SHA-2 256'), ('HS256', 'HMAC with SHA-2 256')], default='', max_length=5)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='core_oauthapplication', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/aleksis/core/models.py b/aleksis/core/models.py index 1264b645e..b899ae12d 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -30,6 +30,7 @@ from django_celery_results.models import TaskResult from dynamic_preferences.models import PerInstancePreferenceModel from model_utils import FieldTracker from model_utils.models import TimeStampedModel +from oauth2_provider.models import AbstractApplication from phonenumber_field.modelfields import PhoneNumberField from polymorphic.models import PolymorphicModel from templated_email import send_templated_mail @@ -1095,3 +1096,12 @@ class TaskUserAssignment(ExtensibleModel): class Meta: verbose_name = _("Task user assignment") verbose_name_plural = _("Task user assignments") + + +class OAuthApplication(AbstractApplication): + """Modified OAuth application class that supports Grant Flows configured in preferences.""" + + def allows_grant_type(self, *grant_types: set[str]) -> bool: + allowed_grants = get_site_preferences()["auth__oauth_allowed_grants"] + + return bool(set(allowed_grants) & grant_types) diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py index 6faaadf7d..d0de22fe0 100644 --- a/aleksis/core/preferences.py +++ b/aleksis/core/preferences.py @@ -14,6 +14,7 @@ from dynamic_preferences.types import ( MultipleChoicePreference, StringPreference, ) +from oauth2_provider.models import AbstractApplication from .models import Group, Person from .registries import person_preferences_registry, site_preferences_registry @@ -262,6 +263,19 @@ class SignupEnabled(BooleanPreference): verbose_name = _("Enable signup") +@site_preferences_registry.register +class OAuthAllowedGrants(MultipleChoicePreference): + """Grant Flows allowed for OAuth applications.""" + + section = auth + name = "oauth_allowed_grants" + default = [grant[0] for grant in AbstractApplication.GRANT_TYPES] + widget = SelectMultiple + verbose_name = _("Allowed Grant Flows for OAuth applications") + field_attribute = {"initial": []} + choices = AbstractApplication.GRANT_TYPES + + @site_preferences_registry.register class AvailableLanguages(MultipleChoicePreference): """Available languages of your AlekSIS instance.""" diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 10ce06dbb..2d2388354 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -323,8 +323,8 @@ ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE = True ACCOUNT_UNIQUE_EMAIL = _settings.get("auth.login.registration.unique_email", True) # Configuration for OAuth2 provider - OAUTH2_PROVIDER = {"SCOPES_BACKEND_CLASS": "aleksis.core.util.auth_helpers.AppScopes"} +OAUTH2_PROVIDER_APPLICATION_MODEL = "core.OAuthApplication" if _settings.get("oauth2.oidc.enabled", False): with open(_settings.get("oauth2.oidc.rsa_key", "/etc/aleksis/oidc.pem"), "r") as f: diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 8918f3c35..07fd0d524 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -43,7 +43,6 @@ from haystack.inputs import AutoQuery from haystack.query import SearchQuerySet from haystack.utils.loading import UnifiedIndex from health_check.views import MainView -from oauth2_provider.models import Application from reversion import set_user from reversion.views import RevisionMixin from rules.contrib.views import PermissionRequiredMixin, permission_required @@ -76,6 +75,7 @@ from .models import ( Group, GroupType, Notification, + OAuthApplication, PDFFile, Person, SchoolTerm, @@ -1040,7 +1040,7 @@ class OAuth2List(PermissionRequiredMixin, ListView): template_name = "oauth2_provider/application_list.html" def get_queryset(self): - return Application.objects.all() + return OAuthApplication.objects.all() class OAuth2Detail(PermissionRequiredMixin, DetailView): @@ -1051,7 +1051,7 @@ class OAuth2Detail(PermissionRequiredMixin, DetailView): template_name = "oauth2_provider/application_detail.html" def get_queryset(self): - return Application.objects.all() + return OAuthApplication.objects.all() class OAuth2Delete(PermissionRequiredMixin, DeleteView): @@ -1063,7 +1063,7 @@ class OAuth2Delete(PermissionRequiredMixin, DeleteView): template_name = "oauth2_provider/application_confirm_delete.html" def get_queryset(self): - return Application.objects.all() + return OAuthApplication.objects.all() class OAuth2Update(PermissionRequiredMixin, UpdateView): @@ -1074,12 +1074,12 @@ class OAuth2Update(PermissionRequiredMixin, UpdateView): template_name = "oauth2_provider/application_form.html" def get_queryset(self): - return Application.objects.all() + return OAuthApplication.objects.all() def get_form_class(self): """Return the form class for the application model.""" return modelform_factory( - Application, + OAuthApplication, fields=( "name", "client_id", -- GitLab