From c91dcf27f28d767cd4e67467c7c98a019b11ebc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bedna=C5=99=C3=ADk?= <jan.bednarik@gmail.com> Date: Fri, 24 Apr 2020 13:06:10 +0200 Subject: [PATCH] Copy users app from another project as base for this --- pirates/admin.py | 22 +++ pirates/apps.py | 13 ++ pirates/auth.py | 35 +++++ pirates/migrations/0001_initial.py | 132 ++++++++++++++++++ pirates/migrations/0002_remove_user_name.py | 10 ++ pirates/migrations/0003_auto_20200129_2139.py | 32 +++++ pirates/migrations/__init__.py | 0 pirates/models.py | 20 +++ setup.cfg | 2 +- tests/factories.py | 34 +++++ tests/test_auth.py | 71 ++++++++++ 11 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 pirates/admin.py create mode 100644 pirates/apps.py create mode 100644 pirates/auth.py create mode 100644 pirates/migrations/0001_initial.py create mode 100644 pirates/migrations/0002_remove_user_name.py create mode 100644 pirates/migrations/0003_auto_20200129_2139.py create mode 100644 pirates/migrations/__init__.py create mode 100644 pirates/models.py create mode 100644 tests/factories.py create mode 100644 tests/test_auth.py diff --git a/pirates/admin.py b/pirates/admin.py new file mode 100644 index 0000000..a29303a --- /dev/null +++ b/pirates/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin +from django.contrib.auth import admin as auth_admin +from django.contrib.auth import get_user_model + +from .models import Team + +User = get_user_model() + + +@admin.register(User) +class UserAdmin(auth_admin.UserAdmin): + fieldsets = (("User", {"fields": ("teams",)}),) + auth_admin.UserAdmin.fieldsets + + def has_add_permission(self, request): + return False + + +@admin.register(Team) +class TeamAdmin(admin.ModelAdmin): + def has_add_permission(self, request): + # TODO when auto sync is done, disable manual add + return True diff --git a/pirates/apps.py b/pirates/apps.py new file mode 100644 index 0000000..7ede4ef --- /dev/null +++ b/pirates/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class UsersConfig(AppConfig): + name = "nominace.users" + verbose_name = _("Users") + + def ready(self): + try: + import nominace.users.signals + except ImportError: + pass diff --git a/pirates/auth.py b/pirates/auth.py new file mode 100644 index 0000000..9b92120 --- /dev/null +++ b/pirates/auth.py @@ -0,0 +1,35 @@ +from mozilla_django_oidc.auth import OIDCAuthenticationBackend + + +class CustomOIDCAuthenticationBackend(OIDCAuthenticationBackend): + """ + Custom OIDC Authentication Backend. + + Instead of `email` uses claim `sub` (as `username`) to identify users. Which + allows for email change. + """ + + def get_username(self, claims): + return claims.get("sub") + + def filter_users_by_claims(self, claims): + username = self.get_username(claims) + if not username: + return self.UserModel.objects.none() + return self.UserModel.objects.filter(username=username) + + def create_user(self, claims): + username = self.get_username(claims) + first_name = claims.get("given_name", "") + last_name = claims.get("family_name", "") + email = claims.get("email", "") + return self.UserModel.objects.create_user( + username=username, first_name=first_name, last_name=last_name, email=email + ) + + def update_user(self, user, claims): + user.first_name = claims.get("given_name", "") + user.last_name = claims.get("family_name", "") + user.email = claims.get("email", "") + user.save() + return user diff --git a/pirates/migrations/0001_initial.py b/pirates/migrations/0001_initial.py new file mode 100644 index 0000000..1cf8bc3 --- /dev/null +++ b/pirates/migrations/0001_initial.py @@ -0,0 +1,132 @@ +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [("auth", "0008_alter_user_username_max_length")] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=30, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "name", + models.CharField( + blank=True, max_length=255, verbose_name="Name of User" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name_plural": "users", + "verbose_name": "user", + "abstract": False, + }, + managers=[("objects", django.contrib.auth.models.UserManager())], + ) + ] diff --git a/pirates/migrations/0002_remove_user_name.py b/pirates/migrations/0002_remove_user_name.py new file mode 100644 index 0000000..ba42d48 --- /dev/null +++ b/pirates/migrations/0002_remove_user_name.py @@ -0,0 +1,10 @@ +# Generated by Django 3.0.2 on 2020-01-29 14:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [("users", "0001_initial")] + + operations = [migrations.RemoveField(model_name="user", name="name")] diff --git a/pirates/migrations/0003_auto_20200129_2139.py b/pirates/migrations/0003_auto_20200129_2139.py new file mode 100644 index 0000000..0cf0863 --- /dev/null +++ b/pirates/migrations/0003_auto_20200129_2139.py @@ -0,0 +1,32 @@ +# Generated by Django 3.0.2 on 2020-01-29 20:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [("users", "0002_remove_user_name")] + + operations = [ + migrations.CreateModel( + name="Team", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=1000)), + ], + options={"ordering": ["name"]}, + ), + migrations.AddField( + model_name="user", + name="teams", + field=models.ManyToManyField(to="users.Team"), + ), + ] diff --git a/pirates/migrations/__init__.py b/pirates/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pirates/models.py b/pirates/models.py new file mode 100644 index 0000000..5819079 --- /dev/null +++ b/pirates/models.py @@ -0,0 +1,20 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models +from django.urls import reverse + + +class User(AbstractUser): + teams = models.ManyToManyField("Team") + + def get_absolute_url(self): + return reverse("users:detail", kwargs={"username": self.username}) + + +class Team(models.Model): + name = models.CharField(max_length=1000) + + class Meta: + ordering = ["name"] + + def __str__(self): + return self.name diff --git a/setup.cfg b/setup.cfg index e9dbd03..91d8bea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,4 +12,4 @@ line_length = 88 multi_line_output = 3 default_sectiont = "THIRDPARTY" include_trailing_comma = true -known_third_party =setuptools +known_third_party =django,factory,faker,mozilla_django_oidc,pytest,setuptools diff --git a/tests/factories.py b/tests/factories.py new file mode 100644 index 0000000..d1f5278 --- /dev/null +++ b/tests/factories.py @@ -0,0 +1,34 @@ +from typing import Any, Sequence + +from django.contrib.auth import get_user_model +from factory import DjangoModelFactory, Faker, post_generation + +from pirates.models import Team + + +class UserFactory(DjangoModelFactory): + username = Faker("user_name") + email = Faker("email") + + @post_generation + def password(self, create: bool, extracted: Sequence[Any], **kwargs): + password = Faker( + "password", + length=42, + special_chars=True, + digits=True, + upper_case=True, + lower_case=True, + ).generate(extra_kwargs={}) + self.set_password(password) + + class Meta: + model = get_user_model() + django_get_or_create = ["username"] + + +class TeamFactory(DjangoModelFactory): + name = Faker("company") + + class Meta: + model = Team diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..b0bbffd --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,71 @@ +import pytest +from django.contrib.auth import get_user_model +from faker import Faker + +from pirates.auth import CustomOIDCAuthenticationBackend + +pytestmark = pytest.mark.django_db + +fake = Faker() + + +@pytest.fixture +def backend(): + return CustomOIDCAuthenticationBackend() + + +def test_auth_backend__get_username(backend): + sub = fake.user_name() + claims = {"sub": sub} + assert backend.get_username(claims) == sub + + +def test_auth_backend__filter_users_by_claims__missing_sub_in_claims(backend): + claims = {} + users = backend.filter_users_by_claims(claims) + assert users.exists() is False + + +def test_auth_backend__filter_users_by_claims__unknown_user(backend): + claims = {"sub": fake.user_name()} + users = backend.filter_users_by_claims(claims) + assert users.exists() is False + + +def test_auth_backend__filter_users_by_claims__known_user(backend, user): + claims = {"sub": user.username} + users = backend.filter_users_by_claims(claims) + assert list(users) == [user] + + +def test_auth_backend__create_user(backend): + claims = { + "sub": fake.user_name(), + "given_name": fake.first_name(), + "family_name": fake.last_name(), + "email": fake.email(), + } + user = backend.create_user(claims) + assert user.username == claims["sub"] + assert user.first_name == claims["given_name"] + assert user.last_name == claims["family_name"] + assert user.email == claims["email"] + assert get_user_model().objects.get() == user + + +def test_auth_backend__update_user(backend, user): + claims = { + "sub": user.username, + "given_name": fake.first_name(), + "family_name": fake.last_name(), + "email": fake.email(), + } + assert user.first_name != claims["given_name"] + assert user.last_name != claims["family_name"] + assert user.email != claims["email"] + updated_user = backend.update_user(user, claims) + assert updated_user.username == user.username + assert updated_user.first_name == claims["given_name"] + assert updated_user.last_name == claims["family_name"] + assert updated_user.email == claims["email"] + assert get_user_model().objects.get() == updated_user -- GitLab