Page MenuHomeSoftware Heritage

D5219.diff
No OneTemporary

D5219.diff

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

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

Event Timeline