pirates-auth 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020, Jan Bednařík
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: pirates-auth
3
+ Version: 0.8.0
4
+ Summary: Passwordless SSO (OpenID Connect) authentication backend and abstract user, team and group models for Django. Used in the Czech Pirate Party.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: django,openid,sso
8
+ Author: Jan Bednařík
9
+ Author-email: jan.bednarik@gmail.com
10
+ Requires-Python: >=3.11
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Framework :: Django
22
+ Classifier: Framework :: Django :: 5.2
23
+ Classifier: Framework :: Django :: 6.0
24
+ Classifier: Topic :: Utilities
25
+ Requires-Dist: Django (>=5.2,<7.0)
26
+ Requires-Dist: mozilla-django-oidc (>=3.0,<5.0)
27
+ Project-URL: Homepage, https://gitlab.pirati.cz/to/pirates
28
+ Project-URL: Repository, https://gitlab.pirati.cz/to/pirates
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Pirates
32
+
33
+ Django app na uživatele, týmy a skupiny, s napojením na SSO.
34
+
35
+ [![code style: Black](https://img.shields.io/badge/code%20style-Black-000000)](https://github.com/psf/black)
36
+ [![license MIT](https://img.shields.io/badge/license-MIT-brightgreen)](LICENSE)
37
+ ![Python Version](https://img.shields.io/pypi/pyversions/pirates-auth)
38
+ ![Django Version](https://img.shields.io/pypi/djversions/pirates-auth?color=0C4B33)
39
+
40
+ Bezheslová SSO autentizace (OpenID Connect) a abstraktní modely uživatele, týmu
41
+ a skupiny pro Django.
42
+
43
+ ## Požadavky
44
+
45
+ * Python 3.11+
46
+ * Django 5.2–6.x
47
+
48
+ ## Instalace
49
+
50
+ ```
51
+ pip install pirates-auth
52
+ ```
53
+
54
+ Na PyPI se balíček jmenuje **`pirates-auth`**, ale importuje se jako `pirates` –
55
+ pod tímto názvem se také přidává do `INSTALLED_APPS`.
56
+
57
+ ## Použití
58
+
59
+ ### Settings
60
+
61
+ Přidat `pirates` do `INSTALLED_APPS`.
62
+
63
+ ### Modely
64
+
65
+ Jsou k dipozici abstraktní modely pro uživatele, tým a organizační skupinu. Ty
66
+ lze doplnit o další fieldy specifické pro aplikaci. Příklad:
67
+
68
+ ```python
69
+ from django.db import models
70
+ from pirates.models import AbstractUser
71
+
72
+ class CustomUser(AbstractUser):
73
+ is_friendly = models.BooleanField(default=True)
74
+ ```
75
+
76
+ A nezapomenout model pro uživatele nastavit v settings:
77
+
78
+ ```python
79
+ AUTH_USER_MODEL = "myapp.CustomUser"
80
+ ```
81
+
82
+ Uživatel je bezheslový – autentizuje se přes SSO (OpenID Connect), takže
83
+ `AbstractUser` nedědí z `AbstractBaseUser` a nemá pole `password` ani
84
+ `last_login`. `get_session_auth_hash()` je proto odvozen z `sso_id`, aby
85
+ `django.contrib.auth.login()` fungoval i na Djangu 6+.
86
+
87
+ ### URLs
88
+
89
+ URL patterns (v současné době pouze pro OpenID Connect) jsou definovány v
90
+ `pirates.urls`. Stačí je připojit k URL patterns projektu:
91
+
92
+ ```python
93
+ from pirates.urls import urlpatterns as pirates_urlpatterns
94
+
95
+ urlpatterns = [
96
+ # URL patterns projektu
97
+ # ...
98
+ ] + pirates_urlpatterns
99
+
100
+ ```
101
+
102
+ ### SSO přes OpenID Connect
103
+
104
+ Implementaci OpenID zajišťuje knihovna
105
+ [mozilla-django-oidc](https://github.com/mozilla/mozilla-django-oidc).
106
+
107
+ V settings projektu je třeba nastavit několik konfiguračních konstant a
108
+ autentifikační backend. Příklad settings (s využitím
109
+ [django-environ](https://github.com/joke2k/django-environ)):
110
+
111
+ ```python
112
+ from os.path import join
113
+ import environ
114
+
115
+ env = environ.Env()
116
+
117
+ AUTHENTICATION_BACKENDS = ["pirates.auth.PiratesOIDCAuthenticationBackend"]
118
+
119
+ OIDC_RP_CLIENT_ID = env.str("OIDC_RP_CLIENT_ID")
120
+ OIDC_RP_CLIENT_SECRET = env.str("OIDC_RP_CLIENT_SECRET")
121
+ OIDC_RP_REALM_URL = env.str("OIDC_RP_REALM_URL")
122
+ OIDC_RP_SIGN_ALGO = "RS256"
123
+ OIDC_OP_JWKS_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/certs")
124
+ OIDC_OP_AUTHORIZATION_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/auth")
125
+ OIDC_OP_TOKEN_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/token")
126
+ OIDC_OP_USER_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/userinfo")
127
+ ```
128
+
129
+ URL patterns pro OpenID Connect už jsou součástí `pirates.urls` (viz výše).
130
+
131
+ #### Signál po přihlášení
132
+
133
+ Po přihlášení uživatele je poslán signál `pirates.signals.post_login` s
134
+ parametry:
135
+
136
+ * `sender` - `PiratesOIDCAuthenticationBackend`
137
+ * `user` - přihlášený uživatel (instance `AUTH_USER_MODEL`)
138
+ * `created` - `True`/`False` zda-li byl vytvořen nový uživatel
139
+ * `request` - instance `HttpRequest`
140
+
@@ -0,0 +1,109 @@
1
+ # Pirates
2
+
3
+ Django app na uživatele, týmy a skupiny, s napojením na SSO.
4
+
5
+ [![code style: Black](https://img.shields.io/badge/code%20style-Black-000000)](https://github.com/psf/black)
6
+ [![license MIT](https://img.shields.io/badge/license-MIT-brightgreen)](LICENSE)
7
+ ![Python Version](https://img.shields.io/pypi/pyversions/pirates-auth)
8
+ ![Django Version](https://img.shields.io/pypi/djversions/pirates-auth?color=0C4B33)
9
+
10
+ Bezheslová SSO autentizace (OpenID Connect) a abstraktní modely uživatele, týmu
11
+ a skupiny pro Django.
12
+
13
+ ## Požadavky
14
+
15
+ * Python 3.11+
16
+ * Django 5.2–6.x
17
+
18
+ ## Instalace
19
+
20
+ ```
21
+ pip install pirates-auth
22
+ ```
23
+
24
+ Na PyPI se balíček jmenuje **`pirates-auth`**, ale importuje se jako `pirates` –
25
+ pod tímto názvem se také přidává do `INSTALLED_APPS`.
26
+
27
+ ## Použití
28
+
29
+ ### Settings
30
+
31
+ Přidat `pirates` do `INSTALLED_APPS`.
32
+
33
+ ### Modely
34
+
35
+ Jsou k dipozici abstraktní modely pro uživatele, tým a organizační skupinu. Ty
36
+ lze doplnit o další fieldy specifické pro aplikaci. Příklad:
37
+
38
+ ```python
39
+ from django.db import models
40
+ from pirates.models import AbstractUser
41
+
42
+ class CustomUser(AbstractUser):
43
+ is_friendly = models.BooleanField(default=True)
44
+ ```
45
+
46
+ A nezapomenout model pro uživatele nastavit v settings:
47
+
48
+ ```python
49
+ AUTH_USER_MODEL = "myapp.CustomUser"
50
+ ```
51
+
52
+ Uživatel je bezheslový – autentizuje se přes SSO (OpenID Connect), takže
53
+ `AbstractUser` nedědí z `AbstractBaseUser` a nemá pole `password` ani
54
+ `last_login`. `get_session_auth_hash()` je proto odvozen z `sso_id`, aby
55
+ `django.contrib.auth.login()` fungoval i na Djangu 6+.
56
+
57
+ ### URLs
58
+
59
+ URL patterns (v současné době pouze pro OpenID Connect) jsou definovány v
60
+ `pirates.urls`. Stačí je připojit k URL patterns projektu:
61
+
62
+ ```python
63
+ from pirates.urls import urlpatterns as pirates_urlpatterns
64
+
65
+ urlpatterns = [
66
+ # URL patterns projektu
67
+ # ...
68
+ ] + pirates_urlpatterns
69
+
70
+ ```
71
+
72
+ ### SSO přes OpenID Connect
73
+
74
+ Implementaci OpenID zajišťuje knihovna
75
+ [mozilla-django-oidc](https://github.com/mozilla/mozilla-django-oidc).
76
+
77
+ V settings projektu je třeba nastavit několik konfiguračních konstant a
78
+ autentifikační backend. Příklad settings (s využitím
79
+ [django-environ](https://github.com/joke2k/django-environ)):
80
+
81
+ ```python
82
+ from os.path import join
83
+ import environ
84
+
85
+ env = environ.Env()
86
+
87
+ AUTHENTICATION_BACKENDS = ["pirates.auth.PiratesOIDCAuthenticationBackend"]
88
+
89
+ OIDC_RP_CLIENT_ID = env.str("OIDC_RP_CLIENT_ID")
90
+ OIDC_RP_CLIENT_SECRET = env.str("OIDC_RP_CLIENT_SECRET")
91
+ OIDC_RP_REALM_URL = env.str("OIDC_RP_REALM_URL")
92
+ OIDC_RP_SIGN_ALGO = "RS256"
93
+ OIDC_OP_JWKS_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/certs")
94
+ OIDC_OP_AUTHORIZATION_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/auth")
95
+ OIDC_OP_TOKEN_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/token")
96
+ OIDC_OP_USER_ENDPOINT = join(OIDC_RP_REALM_URL, "protocol/openid-connect/userinfo")
97
+ ```
98
+
99
+ URL patterns pro OpenID Connect už jsou součástí `pirates.urls` (viz výše).
100
+
101
+ #### Signál po přihlášení
102
+
103
+ Po přihlášení uživatele je poslán signál `pirates.signals.post_login` s
104
+ parametry:
105
+
106
+ * `sender` - `PiratesOIDCAuthenticationBackend`
107
+ * `user` - přihlášený uživatel (instance `AUTH_USER_MODEL`)
108
+ * `created` - `True`/`False` zda-li byl vytvořen nový uživatel
109
+ * `request` - instance `HttpRequest`
File without changes
@@ -0,0 +1,13 @@
1
+ from django.apps import AppConfig
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class PiratesConfig(AppConfig):
6
+ name = "pirates"
7
+ verbose_name = _("Pirates")
8
+
9
+ def ready(self):
10
+ try:
11
+ import pirates.signals
12
+ except ImportError:
13
+ pass
@@ -0,0 +1,49 @@
1
+ from mozilla_django_oidc.auth import OIDCAuthenticationBackend
2
+
3
+ from .signals import post_login
4
+
5
+
6
+ class PiratesOIDCAuthenticationBackend(OIDCAuthenticationBackend):
7
+ """
8
+ Pirates OIDC Authentication Backend.
9
+
10
+ Instead of `email` uses claim `sub` (as `sso_id`) to identify users. Which
11
+ allows for email change.
12
+ """
13
+
14
+ def get_sso_id(self, claims):
15
+ return claims.get("sub")
16
+
17
+ def filter_users_by_claims(self, claims):
18
+ sso_id = self.get_sso_id(claims)
19
+ if not sso_id:
20
+ return self.UserModel.objects.none()
21
+ return self.UserModel.objects.filter(sso_id=sso_id)
22
+
23
+ def create_user(self, claims):
24
+ sso_id = self.get_sso_id(claims)
25
+ first_name = claims.get("given_name", "")
26
+ last_name = claims.get("family_name", "")
27
+ email = claims.get("email", "")
28
+ user = self.UserModel.objects.create(
29
+ sso_id=sso_id, first_name=first_name, last_name=last_name, email=email
30
+ )
31
+ self.send_post_login_signal(user, True, claims)
32
+ return user
33
+
34
+ def update_user(self, user, claims):
35
+ user.first_name = claims.get("given_name", "")
36
+ user.last_name = claims.get("family_name", "")
37
+ user.email = claims.get("email", "")
38
+ user.save()
39
+ self.send_post_login_signal(user, False, claims)
40
+ return user
41
+
42
+ def send_post_login_signal(self, user, created, claims):
43
+ post_login.send(
44
+ sender=self.__class__,
45
+ user=user,
46
+ created=created,
47
+ claims=claims,
48
+ request=self.request,
49
+ )
@@ -0,0 +1,129 @@
1
+ from django.conf import settings
2
+ from django.contrib.auth.models import PermissionsMixin
3
+ from django.db import models
4
+ from django.utils import timezone
5
+ from django.utils.crypto import salted_hmac
6
+ from django.utils.translation import gettext_lazy as _
7
+
8
+
9
+ class AbstractUser(PermissionsMixin):
10
+ """
11
+ An abstract base class implementing a fully featured User model with
12
+ admin-compliant permissions.
13
+ """
14
+
15
+ sso_id = models.CharField(
16
+ _("SSO ID"),
17
+ max_length=150,
18
+ unique=True,
19
+ error_messages={"unique": _("A user with that SSO ID already exists.")},
20
+ )
21
+ first_name = models.CharField(_("first name"), max_length=150, blank=True)
22
+ last_name = models.CharField(_("last name"), max_length=150, blank=True)
23
+ email = models.EmailField(_("email address"), blank=True)
24
+ is_staff = models.BooleanField(
25
+ _("staff status"),
26
+ default=False,
27
+ help_text=_("Designates whether the user can log into this admin site."),
28
+ )
29
+ is_active = models.BooleanField(
30
+ _("active"),
31
+ default=True,
32
+ help_text=_(
33
+ "Designates whether this user should be treated as active. "
34
+ "Unselect this instead of deleting accounts."
35
+ ),
36
+ )
37
+ date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
38
+
39
+ USERNAME_FIELD = "sso_id"
40
+ REQUIRED_FIELDS = []
41
+
42
+ class Meta:
43
+ abstract = True
44
+ verbose_name = _("user")
45
+ verbose_name_plural = _("users")
46
+
47
+ def get_full_name(self):
48
+ """Return the first_name plus the last_name, with a space in between."""
49
+ full_name = f"{self.first_name} {self.last_name}"
50
+ return full_name.strip()
51
+
52
+ def get_short_name(self):
53
+ """Return the short name for the user."""
54
+ return self.first_name
55
+
56
+ def get_session_auth_hash(self):
57
+ """
58
+ Return an HMAC of the ``sso_id`` field.
59
+
60
+ This account is passwordless -- it authenticates through SSO and does
61
+ not inherit ``AbstractBaseUser``, so there is no ``password`` to hash
62
+ (the mechanism Django's own ``AbstractBaseUser`` uses). Django still
63
+ requires the method: since Django 6.0 ``django.contrib.auth.login()``
64
+ calls it unconditionally, so without it every login raises
65
+ ``AttributeError``. We derive the hash from the immutable ``sso_id``
66
+ instead. It must be non-empty and stable -- an empty value would make
67
+ Django flush the session on the next request and log the user out.
68
+ """
69
+ return self._get_session_auth_hash()
70
+
71
+ def get_session_auth_fallback_hash(self):
72
+ for fallback_secret in settings.SECRET_KEY_FALLBACKS:
73
+ yield self._get_session_auth_hash(secret=fallback_secret)
74
+
75
+ def _get_session_auth_hash(self, secret=None):
76
+ key_salt = "pirates.models.AbstractUser.get_session_auth_hash"
77
+ return salted_hmac(
78
+ key_salt,
79
+ self.sso_id,
80
+ secret=secret,
81
+ algorithm="sha256",
82
+ ).hexdigest()
83
+
84
+ def __str__(self):
85
+ return self.get_full_name() or self.email or self.sso_id
86
+
87
+ @property
88
+ def is_anonymous(self):
89
+ """
90
+ Always return False. This is a way of comparing User objects to
91
+ anonymous users.
92
+ """
93
+ return False
94
+
95
+ @property
96
+ def is_authenticated(self):
97
+ """
98
+ Always return True. This is a way to tell if the user has been
99
+ authenticated in templates.
100
+ """
101
+ return True
102
+
103
+
104
+ class AbstractTeam(models.Model):
105
+ name = models.CharField(max_length=250, unique=True)
106
+ members = models.ManyToManyField(settings.AUTH_USER_MODEL)
107
+
108
+ class Meta:
109
+ abstract = True
110
+ ordering = ["name"]
111
+ verbose_name = _("team")
112
+ verbose_name_plural = _("teams")
113
+
114
+ def __str__(self):
115
+ return self.name
116
+
117
+
118
+ class AbstractOrgGroup(models.Model):
119
+ name = models.CharField(max_length=250, unique=True)
120
+ members = models.ManyToManyField(settings.AUTH_USER_MODEL)
121
+
122
+ class Meta:
123
+ abstract = True
124
+ ordering = ["name"]
125
+ verbose_name = _("group")
126
+ verbose_name_plural = _("groups")
127
+
128
+ def __str__(self):
129
+ return self.name
@@ -0,0 +1,3 @@
1
+ from django.dispatch import Signal
2
+
3
+ post_login = Signal()
@@ -0,0 +1,5 @@
1
+ from django.urls import include, path
2
+
3
+ urlpatterns = [
4
+ path("oidc/", include("mozilla_django_oidc.urls")),
5
+ ]
@@ -0,0 +1,63 @@
1
+ [project]
2
+ name = "pirates-auth"
3
+ version = "0.8.0"
4
+ description = "Passwordless SSO (OpenID Connect) authentication backend and abstract user, team and group models for Django. Used in the Czech Pirate Party."
5
+ authors = [
6
+ { name = "Jan Bednařík", email = "jan.bednarik@gmail.com" },
7
+ { name = "Alexandra Ťapková", email = "git@ouppy.space" }
8
+ ]
9
+ license = "MIT"
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ keywords = ["django", "openid", "sso"]
13
+ classifiers = [
14
+ "Development Status :: 5 - Production/Stable",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Programming Language :: Python :: Implementation :: CPython",
24
+ "Framework :: Django",
25
+ "Framework :: Django :: 5.2",
26
+ "Framework :: Django :: 6.0",
27
+ "Topic :: Utilities",
28
+ ]
29
+ dependencies = [
30
+ "Django>=5.2,<7.0",
31
+ "mozilla-django-oidc>=3.0,<5.0",
32
+ ]
33
+
34
+ [project.urls]
35
+ homepage = "https://gitlab.pirati.cz/to/pirates"
36
+ repository = "https://gitlab.pirati.cz/to/pirates"
37
+
38
+ [tool.poetry]
39
+ packages = [{ include = "pirates" }]
40
+
41
+ [tool.poetry.group.dev.dependencies]
42
+ tox = ">=4.15"
43
+ pytest = ">=8"
44
+ pytest-cov = ">=5"
45
+ pytest-factoryboy = ">=2.7"
46
+ pytest-django = ">=4.8"
47
+ pytest-mock = ">=3.14"
48
+
49
+ [build-system]
50
+ requires = ["poetry-core>=2.0.0"]
51
+ build-backend = "poetry.core.masonry.api"
52
+
53
+ [tool.pytest.ini_options]
54
+ python_files = "test_*.py"
55
+ addopts = "--ds=tests.settings"
56
+
57
+ [tool.isort]
58
+ # config compatible with Black
59
+ line_length = 88
60
+ multi_line_output = 3
61
+ default_section = "THIRDPARTY"
62
+ include_trailing_comma = true
63
+ known_third_party = ["django", "factory", "faker", "mozilla_django_oidc", "pytest", "pytest_factoryboy"]