from django.conf import settings from django.db import models from django.db.models import Q from infrasynth.tenancy.mixins import GlobalOrTenantModel, TenantOwnedModel class Role(GlobalOrTenantModel): """A permission role. ``tenant IS NULL`` is a system role; a tenant row overrides it.""" name = models.CharField(max_length=100) slug = models.SlugField(max_length=100) description = models.TextField(blank=True) permissions = models.JSONField(default=list, help_text="List of permission codenames") is_system = models.BooleanField(default=False, help_text="System roles cannot be deleted") users = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name="roles", blank=True, help_text="Users assigned this role", ) class Meta: db_table = "security_role" constraints = [ models.UniqueConstraint(fields=["tenant", "slug"], name="uniq_role_slug_per_tenant"), models.UniqueConstraint( fields=["slug"], condition=Q(tenant__isnull=True), name="uniq_global_role_slug", ), ] def __str__(self): return self.name class Grant(TenantOwnedModel): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="direct_grants", ) codename = models.CharField(max_length=200, db_index=True) granted_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="grants_given", ) reason = models.TextField(blank=True) expires_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = "security_grant" constraints = [ models.UniqueConstraint(fields=["tenant", "user", "codename"], name="uniq_grant_per_tenant_user"), ] class Revoke(TenantOwnedModel): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="direct_revokes", ) codename = models.CharField(max_length=200, db_index=True) revoked_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="revokes_given", ) reason = models.TextField(blank=True) class Meta: db_table = "security_revoke" constraints = [ models.UniqueConstraint(fields=["tenant", "user", "codename"], name="uniq_revoke_per_tenant_user"), ] class APIKey(TenantOwnedModel): """A tenant-scoped service credential (``prefix.secret``, secret hashed).""" name = models.CharField(max_length=200) prefix = models.CharField(max_length=12, help_text="First 8 characters visible in UI") key_hash = models.CharField(max_length=255, help_text="PBKDF2 hash of the full secret") scopes = models.JSONField(default=list, help_text='["read:users", "write:billing"]') created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True) is_active = models.BooleanField(default=True) expires_at = models.DateTimeField(null=True, blank=True) last_used_at = models.DateTimeField(null=True, blank=True) rotated_from = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True) class Meta: db_table = "security_api_key" constraints = [ models.UniqueConstraint(fields=["tenant", "prefix"], name="uniq_api_key_prefix_per_tenant"), ] class TwoFactorConfig(models.Model): METHOD_TOTP = "totp" METHOD_EMAIL = "email" METHOD_BOTH = "both" user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="two_factor_config", ) is_enabled = models.BooleanField(default=False) is_configured = models.BooleanField(default=False) method = models.CharField( max_length=10, choices=[ (METHOD_TOTP, "TOTP"), (METHOD_EMAIL, "Email"), (METHOD_BOTH, "Both"), ], default=METHOD_TOTP, ) secret_key_encrypted = models.CharField(max_length=500, null=True, blank=True) recovery_codes_encrypted = models.TextField(null=True, blank=True) email_verified = models.BooleanField(default=False) email_code = models.CharField(max_length=6, null=True, blank=True) email_code_expires_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = "security_two_factor_config" class ALTCHAChallenge(models.Model): challenge_id = models.CharField(max_length=64, primary_key=True) salt = models.CharField(max_length=32) difficulty = models.IntegerField(default=10000) expires_at = models.DateTimeField(db_index=True) is_verified = models.BooleanField(default=False) class Meta: db_table = "security_altcha_challenge"