Skip to content

Commit

Permalink
Finishing up user model switchover
Browse files Browse the repository at this point in the history
This works best if you trash your existing database. (We're early enough that that shouldn't be a big deal.)

- Finalizing changes to the user model and squashing the users migrations down to one
- Fixes for the APISIX middleware to check that the user is active, create the user properly when it has to
- Set a default for global_id - if we create a manual user, the field needs to be populated, and it's a UUID so no harm in just filling it as per usual
- Update the docker_compose env to actually put the Postgres database in a persistent volume, imagine that!
- Update the docker_compose env to pull in an init script for Keycloak so you don't have to manually do it
- Update the Dockerfile to pull in mitol_django packages you might have built and added manually - no changes to the pyproject.toml for this, you get to do that yourself still
- Update gitignore to ignore the mitol_django manual packages
  • Loading branch information
jkachel committed Feb 4, 2025
1 parent c37bd6a commit 44d6ca7
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 70 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,6 @@ generated-codes.csv

# SQLite
*.sqlite3

# Manually-built ol-django packages
mitol_django_*
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.12.6
FROM python:3.12
LABEL maintainer "ODL DevOps <[email protected]>"

# Add package files, install updated node and pip
Expand Down Expand Up @@ -32,6 +32,7 @@ RUN pip install "poetry==$POETRY_VERSION"
# Install project packages
COPY pyproject.toml /src
COPY poetry.lock /src
COPY mitol_django_*.gz /src
WORKDIR /src
RUN poetry install

Expand Down
2 changes: 2 additions & 0 deletions config/keycloak/db/init-keycloak.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CREATE DATABASE keycloak;
GRANT ALL PRIVILEGES ON DATABASE keycloak TO postgres;
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ services:
- "${POSTGRES_PORT}:5432"
environment:
<<: *py-environment
volumes:
- postgres_data:/var/lib/postgresql/data
- ./config/keycloak/db:/docker-entrypoint-initdb.d

redis:
image: redis:6.2
Expand Down Expand Up @@ -122,3 +125,4 @@ volumes:
django_media:
yarn-cache:
keycloak-store:
postgres_data:
6 changes: 5 additions & 1 deletion unified_ecommerce/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@
"hijack.middleware.HijackUserMiddleware",
]

AUTH_USER_MODEL = "users.User"

# CORS
CORS_ALLOWED_ORIGINS = get_list_of_str("CORS_ALLOWED_ORIGINS", [])
CORS_ALLOWED_ORIGIN_REGEXES = get_list_of_str("CORS_ALLOWED_ORIGIN_REGEXES", [])
Expand Down Expand Up @@ -479,9 +481,11 @@
APISIX_USERDATA_MAP = {
"auth_user": {
"email": "email",
"preferred_username": "sub",
"global_id": "sub",
"given_name": "given_name",
"family_name": "family_name",
"username": "preferred_username",
"name": "name",
},
"authentication_userprofile": {
"country_code": None,
Expand Down
42 changes: 26 additions & 16 deletions unified_ecommerce/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,48 +394,58 @@ def get_user_from_apisix_headers(request):
if not decoded_headers:
return None

(
email,
preferred_username,
given_name,
family_name,
) = decoded_headers.values()
log.debug("decoded headers: %s", decoded_headers)

log.debug("get_user_from_apisix_headers: Authenticating %s", preferred_username)
email = decoded_headers.get("email", None)
global_id = decoded_headers.get("global_id", None)
username = decoded_headers.get("username", None)
given_name = decoded_headers.get("given_name", None)
family_name = decoded_headers.get("family_name", None)
name = decoded_headers.get("name", None)

user, created = User.objects.filter(username=preferred_username).get_or_create(
log.debug("get_user_from_apisix_headers: Authenticating %s", global_id)

user, created = User.objects.update_or_create(
global_id=global_id,
defaults={
"username": preferred_username,
"global_id": global_id,
"username": username,
"email": email,
"first_name": given_name,
"last_name": family_name,
}
"name": name,
},
)

if created:
log.debug(
"get_user_from_apisix_headers: User %s not found, created new",
preferred_username,
global_id,
)
user.set_unusable_password()
user.is_active = True
user.save()
else:
log.debug(
"get_user_from_apisix_headers: Found existing user for %s: %s",
preferred_username,
global_id,
user,
)

user.first_name = given_name
user.last_name = family_name
user.save()
if not user.is_active:
log.debug(
"get_user_from_apisix_headers: User %s is inactive",
global_id,
)
msg = "User is inactive"
raise KeyError(msg)

profile_data = decode_apisix_headers(request, "authentication_userprofile")

if profile_data:
log.debug(
"get_user_from_apisix_headers: Setting up additional profile for %s",
preferred_username,
global_id,
)

_, profile = UserProfile.objects.filter(user=user).get_or_create(
Expand Down
9 changes: 4 additions & 5 deletions users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin as ContribUserAdmin
from django.contrib.auth.models import Group
from django.utils.translation import gettext_lazy as _

from users.models import UserProfile
Expand Down Expand Up @@ -32,13 +31,14 @@ class UserAdmin(ContribUserAdmin):
None,
{
"fields": (
"global_id",
"username",
"password",
"last_login",
)
},
),
(_("Personal Info"), {"fields": ("first_name", "last_name", "email")}),
(_("Personal Info"), {"fields": ("name", "first_name", "last_name", "email")}),
(
_("Permissions"),
{
Expand All @@ -54,18 +54,17 @@ class UserAdmin(ContribUserAdmin):
),
)
list_display = (
"global_id",
"email",
"username",
"is_staff",
"last_login",
)
list_filter = ("is_staff", "is_superuser", "is_active", "groups")
search_fields = ("username", "email")
search_fields = ("global_id", "username", "email")
ordering = ("email",)
readonly_fields = ("last_login", "password")
inlines = [UserProfileInline]


admin.site.unregister(Group)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
45 changes: 0 additions & 45 deletions users/migrations/0001_add_userprofile_model.py

This file was deleted.

138 changes: 138 additions & 0 deletions users/migrations/0001_create_custom_user_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Generated by Django 4.2.18 on 2025-02-04 14:15

import uuid

import django.db.models.deletion
import django_countries.fields
from django.conf import settings
from django.db import migrations, models

import users.models


class Migration(migrations.Migration):
initial = True

dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]

operations = [
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.BigAutoField(
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",
),
),
("created_on", models.DateTimeField(auto_now_add=True)),
("updated_on", models.DateTimeField(auto_now=True)),
(
"global_id",
models.CharField(
blank=True,
default=uuid.uuid4,
help_text="The SSO ID (usually a Keycloak UUID) for the user.",
max_length=255,
unique=True,
),
),
("username", models.CharField(max_length=150, unique=True)),
("email", models.EmailField(max_length=254, unique=True)),
(
"first_name",
models.CharField(blank=True, default="", max_length=255),
),
("last_name", models.CharField(blank=True, default="", max_length=255)),
("name", models.CharField(blank=True, default="", max_length=255)),
(
"is_staff",
models.BooleanField(
default=False, help_text="The user can access the admin site"
),
),
(
"is_active",
models.BooleanField(
default=False, help_text="The user account is active"
),
),
(
"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={
"abstract": False,
},
managers=[
("objects", users.models.UserManager()),
],
),
migrations.CreateModel(
name="UserProfile",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_on", models.DateTimeField(auto_now_add=True)),
("updated_on", models.DateTimeField(auto_now=True)),
("country_code", django_countries.fields.CountryField(max_length=2)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="profile",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
]
5 changes: 3 additions & 2 deletions users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# ruff: noqa: TD002,TD003,FIX002

import logging
from uuid import uuid4

from django.conf import settings
from django.contrib.auth.base_user import BaseUserManager
Expand All @@ -15,7 +16,7 @@

def _post_create_user(user):
"""
Create records related to the user
Create records related to the user.
Args:
user (users.models.User): the user that was just created
Expand Down Expand Up @@ -78,7 +79,7 @@ class User(AbstractBaseUser, TimestampedModel, PermissionsMixin):
unique=True,
max_length=255,
blank=True,
default="",
default=uuid4,
help_text="The SSO ID (usually a Keycloak UUID) for the user.",
)
username = models.CharField(unique=True, max_length=150)
Expand Down

0 comments on commit 44d6ca7

Please sign in to comment.