Django scaffold aligned with the business delivery lane: tenant-keyed Member model without passwords, identity app as sole OIDC/session boundary, dev-claims login, authenticated /app/ shell, ADR-0001, and tests. T01/T02/T05/T06 done; OIDC registration, real user-engine HTTP, flex-auth, and packaging remain open.
100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
"""Member and User models — no local password authentication.
|
|
|
|
Identity is established via NetKingdom OIDC (IAM Profile). Passwords are never
|
|
collected or validated; Django users always have an unusable password.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
|
|
|
|
class UserManager(BaseUserManager):
|
|
def create_user(self, username: str, **extra_fields):
|
|
if not username:
|
|
raise ValueError("username is required")
|
|
user = self.model(username=username, **extra_fields)
|
|
user.set_unusable_password()
|
|
user.save(using=self._db)
|
|
return user
|
|
|
|
def create_superuser(self, username: str, **extra_fields):
|
|
extra_fields.setdefault("is_staff", True)
|
|
extra_fields.setdefault("is_superuser", True)
|
|
return self.create_user(username, **extra_fields)
|
|
|
|
|
|
class User(AbstractBaseUser, PermissionsMixin):
|
|
"""Session principal. Credentials live at the NetKingdom issuer, not here."""
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
username = models.CharField(max_length=255, unique=True)
|
|
email = models.EmailField(blank=True)
|
|
full_name = models.CharField(max_length=255, blank=True)
|
|
is_active = models.BooleanField(default=True)
|
|
is_staff = models.BooleanField(default=False)
|
|
date_joined = models.DateTimeField(default=timezone.now)
|
|
|
|
objects = UserManager()
|
|
|
|
USERNAME_FIELD = "username"
|
|
REQUIRED_FIELDS: list[str] = []
|
|
|
|
class Meta:
|
|
verbose_name = "user"
|
|
verbose_name_plural = "users"
|
|
|
|
def get_full_name(self) -> str:
|
|
return self.full_name or self.username
|
|
|
|
def get_short_name(self) -> str:
|
|
return self.full_name or self.username
|
|
|
|
def __str__(self) -> str:
|
|
return self.username
|
|
|
|
|
|
class Member(models.Model):
|
|
"""Local app context for a platform user within a tenant.
|
|
|
|
Links NetKingdom OIDC subject + user-engine user id. No password fields.
|
|
"""
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
tenant_id = models.CharField(max_length=64, db_index=True)
|
|
user = models.OneToOneField(
|
|
User,
|
|
on_delete=models.CASCADE,
|
|
related_name="member",
|
|
)
|
|
# Stable id from user-engine (string to avoid coupling to UUID vs ULID choice)
|
|
user_engine_user_id = models.CharField(max_length=64, db_index=True)
|
|
issuer = models.CharField(max_length=512)
|
|
subject = models.CharField(max_length=255)
|
|
display_name = models.CharField(max_length=255, blank=True)
|
|
email = models.EmailField(blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
last_login_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["issuer", "subject"],
|
|
name="members_member_issuer_subject_uniq",
|
|
),
|
|
models.UniqueConstraint(
|
|
fields=["tenant_id", "user_engine_user_id"],
|
|
name="members_member_tenant_ue_user_uniq",
|
|
),
|
|
]
|
|
indexes = [
|
|
models.Index(fields=["tenant_id", "subject"]),
|
|
]
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.display_name or self.subject}@{self.tenant_id}"
|