Implement NetKingdom identity shell for coulomb.social (CSOC-WP-0002)
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.
This commit is contained in:
parent
2ec7761504
commit
01da195c13
51 changed files with 2215 additions and 41 deletions
100
coulomb_social/apps/members/models.py
Normal file
100
coulomb_social/apps/members/models.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""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}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue