Coverage for src/lexigram/admin/config/auth.py: 23%
62 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:31 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:31 +0800
1"""Authentication, MFA, email OTP, verification, and registration configurations."""
3from __future__ import annotations
5from dataclasses import dataclass
6from typing import Any, Literal
8from lexigram.admin.config.security import AdminPasswordPolicyConfig, AdminSecurityConfig
9from lexigram.domain import DomainModel
10from lexigram.validation import (
11 Field,
12 SecretStr,
13 model_validator,
14)
17@dataclass(init=False)
18class AdminMfaConfig(DomainModel):
19 """Two-factor authentication (TOTP) configuration.
21 Controls whether TOTP 2FA is offered, the issuer label embedded in
22 provisioning URIs, and the allowed clock-skew window for codes.
23 """
25 enabled: bool = Field(default=True, description="Enable TOTP 2FA")
26 factor: str = Field(
27 default="totp",
28 description="Second factor used at login: 'totp' (authenticator app) or 'email' (one-time code)",
29 )
30 issuer: str = Field(
31 default="Lexigram Admin",
32 description="TOTP issuer label shown in authenticator apps",
33 )
34 skew: int = Field(
35 default=1,
36 ge=0,
37 le=2,
38 description="Allowed clock skew in 30 second steps",
39 )
42@dataclass(init=False)
43class AdminEmailOtpConfig(DomainModel):
44 """Email one-time-password (login factor) configuration.
46 Controls whether the email-OTP factor is available, how long a code
47 stays valid, and the minimum interval between sends.
48 """
50 enabled: bool = Field(default=True, description="Enable email OTP factor")
51 ttl_minutes: int = Field(
52 default=10,
53 ge=1,
54 le=60,
55 description="Code validity window in minutes",
56 )
57 resend_cooldown_seconds: int = Field(
58 default=60,
59 ge=5,
60 le=600,
61 description="Minimum seconds between email OTP sends",
62 )
65@dataclass(init=False)
66class AdminEmailVerificationConfig(DomainModel):
67 """Email verification (login gate) configuration.
69 Controls the verify-your-email flow: whether it is offered, whether
70 unverified users are blocked at login, and the verify-link lifetime.
71 """
73 enabled: bool = Field(default=True, description="Enable email verification flow")
74 enforcement: bool = Field(
75 default=True,
76 description="Block login until the email is verified",
77 )
78 token_ttl_hours: int = Field(
79 default=24,
80 ge=1,
81 le=168,
82 description="Verify link validity in hours",
83 )
86@dataclass(init=False)
87class AdminRegistrationConfig(DomainModel):
88 """Self-service registration configuration.
90 Off by default — admin panels are typically invite-only. When enabled,
91 ``GET/POST /admin/register`` becomes available and new accounts receive
92 the configured default role.
93 """
95 enabled: bool = Field(default=False, description="Allow self-service registration")
96 default_role: str = Field(
97 default="admin", description="Role granted to new accounts"
98 )
99 allowed_email_domains: list[str] = Field(
100 default_factory=list,
101 description="Restrict registration to these email domains (empty = any)",
102 )
105@dataclass(init=False)
106class AdminAuthConfig(DomainModel):
107 """Authentication configuration."""
109 enabled: bool = Field(default=True, description="Enable authentication")
110 env: Literal["development", "staging", "production"] = Field(
111 default="development",
112 description="Deployment environment for cookie security defaults",
113 )
114 session_secret: SecretStr = Field(
115 default=SecretStr("change-me-in-production"),
116 description="Session secret for signing",
117 )
118 login_url: str = Field(default="/admin/login")
119 logout_url: str = Field(default="/admin/logout")
120 session_lifetime: int = Field(default=86400, ge=300) # 5 min minimum
121 permission_cache_ttl: int = Field(default=300, ge=0) # 5 minutes
123 # Security settings
124 idle_timeout: int = Field(
125 default=3600, ge=60, description="Session idle timeout in seconds"
126 )
127 csrf_token_lifetime: int = Field(
128 default=3600, ge=60, description="CSRF token expiry in seconds"
129 )
130 password_policy: AdminPasswordPolicyConfig = Field(
131 default_factory=AdminPasswordPolicyConfig,
132 )
133 security: AdminSecurityConfig = Field(
134 default_factory=AdminSecurityConfig,
135 )
136 mfa: AdminMfaConfig = Field(default_factory=AdminMfaConfig)
137 email_otp: AdminEmailOtpConfig = Field(default_factory=AdminEmailOtpConfig)
138 email_verification: AdminEmailVerificationConfig = Field(
139 default_factory=AdminEmailVerificationConfig
140 )
141 registration: AdminRegistrationConfig = Field(
142 default_factory=AdminRegistrationConfig
143 )
145 # Users and Roles (Sync)
146 users: list[Any] = Field(default_factory=list)
147 roles: dict[str, Any] = Field(default_factory=dict)
149 # Identity bridge (spec D3): "internal" = framework admin_users table
150 # (default); "app" = AdminPrincipalProviderProtocol implemented by the app.
151 principal_source: Literal["internal", "app"] = Field(default="internal")
153 # OAuth/SSO (optional)
154 oauth_enabled: bool = Field(default=False)
155 oauth_providers: list[str] = Field(default_factory=list)
157 model_config = {"extra": "allow"}
159 @model_validator(mode="after")
160 def validate_security(self) -> AdminAuthConfig:
161 """Ensure secure settings in production."""
162 if not isinstance(self.session_secret, SecretStr):
163 self.session_secret = SecretStr(self.session_secret)
164 insecure_defaults = (
165 "change-me",
166 "your-secret-key",
167 "secret",
168 "password",
169 "change-me-in-production",
170 )
172 if (
173 self.env in {"production", "staging"}
174 and self.session_secret.get_secret_value().lower() in insecure_defaults
175 ):
176 raise ValueError(
177 "CRITICAL SECURITY ERROR: Default admin session_secret detected in "
178 f"{self.env.upper()}.\n"
179 "You MUST set a secure session secret via LEX_ADMIN__AUTH__SESSION_SECRET.",
180 )
182 if (
183 self.env in {"production", "staging"}
184 and self.oauth_enabled
185 and not self.oauth_providers
186 ):
187 raise ValueError(
188 "oauth_providers must be configured when oauth_enabled=True"
189 )
191 if (
192 self.env in {"production", "staging"}
193 and self.csrf_token_lifetime > self.idle_timeout
194 ):
195 raise ValueError("csrf_token_lifetime must not exceed idle_timeout")
197 return self