Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F7123215
D5219.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
9 KB
Subscribers
None
D5219.diff
View Options
diff --git a/mypy.ini b/mypy.ini
--- a/mypy.ini
+++ b/mypy.ini
@@ -13,3 +13,6 @@
[mypy-keycloak.*]
ignore_missing_imports = True
+
+[mypy-django.*]
+ignore_missing_imports = True
diff --git a/pytest.ini b/pytest.ini
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,2 +1,3 @@
[pytest]
norecursedirs = docs .*
+DJANGO_SETTINGS_MODULE = swh.auth.tests.app.apptest.settings
diff --git a/requirements-django.txt b/requirements-django.txt
new file mode 100644
--- /dev/null
+++ b/requirements-django.txt
@@ -0,0 +1 @@
+Django<3
diff --git a/requirements-test.txt b/requirements-test.txt
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,2 +1,3 @@
pytest
requests_mock
+pytest-django
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -50,7 +50,10 @@
tests_require=parse_requirements("test"),
setup_requires=["setuptools-scm"],
use_scm_version=True,
- extras_require={"testing": parse_requirements("test")},
+ extras_require={
+ "django": parse_requirements("django"),
+ "testing": parse_requirements("test"),
+ },
include_package_data=True,
# entry_points="""
# [swh.cli.subcommands]
diff --git a/swh/auth/django/__init__.py b/swh/auth/django/__init__.py
new file mode 100644
diff --git a/swh/auth/django/models.py b/swh/auth/django/models.py
new file mode 100644
--- /dev/null
+++ b/swh/auth/django/models.py
@@ -0,0 +1,85 @@
+# Copyright (C) 2020-2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU Affero General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from datetime import datetime
+from typing import Optional, Set
+
+from django.contrib.auth.models import User
+
+
+class OIDCUser(User):
+ """
+ Custom User proxy model for remote users storing OpenID Connect
+ related data: profile containing authentication tokens.
+
+ The model is also not saved to database as all users are already stored
+ in the Keycloak one.
+ """
+
+ # OIDC subject identifier
+ sub: str = ""
+
+ # OIDC tokens and session related data, only relevant when a user
+ # authenticates from a web browser
+ access_token: Optional[str] = None
+ expires_at: Optional[datetime] = None
+ id_token: Optional[str] = None
+ refresh_token: Optional[str] = None
+ refresh_expires_at: Optional[datetime] = None
+ scope: Optional[str] = None
+ session_state: Optional[str] = None
+
+ # User permissions
+ permissions: Set[str]
+
+ class Meta:
+ # TODO: To redefine in subclass of this class
+ # Forced to empty otherwise, django complains about it
+ # "Model class swh.auth.django.OIDCUser doesn't declare an explicit app_label
+ # and isn't in an application in INSTALLED_APPS"
+ app_label = ""
+ proxy = True
+
+ def save(self, **kwargs):
+ """
+ Override django.db.models.Model.save to avoid saving the remote
+ users to web application database.
+ """
+ pass
+
+ def get_group_permissions(self, obj=None) -> Set[str]:
+ """
+ Override django.contrib.auth.models.PermissionsMixin.get_group_permissions
+ to get permissions from OIDC
+ """
+ return self.get_all_permissions(obj)
+
+ def get_all_permissions(self, obj=None) -> Set[str]:
+ """
+ Override django.contrib.auth.models.PermissionsMixin.get_all_permissions
+ to get permissions from OIDC
+
+ """
+ return self.permissions
+
+ def has_perm(self, perm, obj=None) -> bool:
+ """
+ Override django.contrib.auth.models.PermissionsMixin.has_perm
+ to check permission from OIDC
+ """
+ if self.is_active and self.is_superuser:
+ return True
+
+ return perm in self.permissions
+
+ def has_module_perms(self, app_label) -> bool:
+ """
+ Override django.contrib.auth.models.PermissionsMixin.has_module_perms
+ to check permissions from OIDC.
+ """
+ if self.is_active and self.is_superuser:
+ return True
+
+ return any(perm.startswith(app_label) for perm in self.permissions)
diff --git a/swh/auth/tests/app/__init__.py b/swh/auth/tests/app/__init__.py
new file mode 100644
diff --git a/swh/auth/tests/app/apptest/__init__.py b/swh/auth/tests/app/apptest/__init__.py
new file mode 100644
diff --git a/swh/auth/tests/app/apptest/apps.py b/swh/auth/tests/app/apptest/apps.py
new file mode 100644
--- /dev/null
+++ b/swh/auth/tests/app/apptest/apps.py
@@ -0,0 +1,10 @@
+# Copyright (C) 2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from django.apps import AppConfig
+
+
+class TestApp(AppConfig):
+ name = "swh.auth.tests.app"
diff --git a/swh/auth/tests/app/apptest/models.py b/swh/auth/tests/app/apptest/models.py
new file mode 100644
--- /dev/null
+++ b/swh/auth/tests/app/apptest/models.py
@@ -0,0 +1,19 @@
+# Copyright (C) 2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from django.db import models
+
+from swh.auth.django.models import OIDCUser
+
+
+class AppUser(OIDCUser):
+ """AppUser class to demonstrate the use of the OIDCUser which adds some attributes.
+
+ """
+
+ url = models.TextField(null=False)
+
+ class meta:
+ app_label = "app-label"
diff --git a/swh/auth/tests/app/apptest/settings.py b/swh/auth/tests/app/apptest/settings.py
new file mode 100644
--- /dev/null
+++ b/swh/auth/tests/app/apptest/settings.py
@@ -0,0 +1,7 @@
+SECRET_KEY = "o+&ayiuk(y^wh4ijz5e=c2$$kjj7g^6r%z+8d*c0lbpfs##k#7"
+
+INSTALLED_APPS = [
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "swh.auth.tests.app.apptest.apps.TestApp",
+]
diff --git a/swh/auth/tests/app/apptest/urls.py b/swh/auth/tests/app/apptest/urls.py
new file mode 100644
--- /dev/null
+++ b/swh/auth/tests/app/apptest/urls.py
@@ -0,0 +1,6 @@
+# Copyright (C) 2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+urlpatterns = [] # type: ignore
diff --git a/swh/auth/tests/app/manage.py b/swh/auth/tests/app/manage.py
new file mode 100755
--- /dev/null
+++ b/swh/auth/tests/app/manage.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ os.environ.setdefault(
+ "DJANGO_SETTINGS_MODULE", "swh.auth.tests.app.apptest.settings"
+ )
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/swh/auth/tests/test_models.py b/swh/auth/tests/test_models.py
new file mode 100644
--- /dev/null
+++ b/swh/auth/tests/test_models.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from copy import copy
+from typing import Set
+
+import pytest
+
+from swh.auth.tests.app.apptest.models import AppUser
+
+PERMISSIONS: Set[str] = set(["api", "app-label-read"])
+NO_PERMISSION: Set[str] = set()
+
+
+@pytest.fixture
+def appuser():
+ return copy(
+ AppUser(
+ id=666,
+ username="foo",
+ password="bar",
+ first_name="foobar",
+ last_name="",
+ email="foo@bar.org",
+ )
+ )
+
+
+@pytest.fixture
+def appuser_admin(appuser):
+ appuser_admin = appuser
+ appuser_admin.is_active = True
+ appuser_admin.is_superuser = True
+ return appuser_admin
+
+
+def test_django_appuser(appuser):
+ appuser.permissions = PERMISSIONS
+
+ assert appuser.get_group_permissions() == PERMISSIONS
+ assert appuser.get_group_permissions(appuser) == PERMISSIONS
+ assert appuser.get_all_permissions() == PERMISSIONS
+ assert appuser.get_all_permissions(appuser) == PERMISSIONS
+
+ assert "api" in PERMISSIONS
+ assert appuser.has_perm("api") is True
+ assert appuser.has_perm("something") is False
+
+ assert "app-label-read" in PERMISSIONS
+ assert appuser.has_module_perms("app-label") is True
+ assert appuser.has_module_perms("app-something") is False
+
+
+def test_django_appuser_admin(appuser_admin):
+ appuser_admin.permissions = NO_PERMISSION
+
+ assert appuser_admin.get_group_permissions() == NO_PERMISSION
+ assert appuser_admin.get_group_permissions(appuser_admin) == NO_PERMISSION
+
+ assert appuser_admin.get_all_permissions() == NO_PERMISSION
+ assert appuser_admin.get_all_permissions(appuser) == NO_PERMISSION
+
+ assert "foobar" not in PERMISSIONS
+ assert appuser_admin.has_perm("foobar") is True
+ assert "something" not in PERMISSIONS
+ assert appuser_admin.has_perm("something") is True
+
+ assert appuser_admin.has_module_perms("app-label") is True
+ assert appuser_admin.has_module_perms("really-whatever-app") is True
diff --git a/tox.ini b/tox.ini
--- a/tox.ini
+++ b/tox.ini
@@ -4,6 +4,7 @@
[testenv]
extras =
testing
+ django
deps =
pytest-cov
dev: pdbpp
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Dec 18, 3:25 AM (1 d, 21 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3229995
Attached To
D5219: swh.auth.django: Expose OIDCUser model object
Event Timeline
Log In to Comment