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