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