from __future__ import annotations from datetime import datetime, timezone from typing import Optional from sqlmodel import Field, SQLModel def _now() -> datetime: return datetime.now(timezone.utc) class OidcConfig(SQLModel, table=True): """Single-row configuration for signing in through an identity provider. Deliberately in the database rather than the environment: the point of the feature request was to configure it from Settings, and a homelab admin should not have to edit a compose file and restart to fix a typo in a client id. """ #: Always 1. A second provider would need a real table and a picker on the #: login screen; one is what installs of this size actually use. id: Optional[int] = Field(default=1, primary_key=True) enabled: bool = False issuer: str = "" client_id: str = "" client_secret: str = "" # encrypted scopes: str = "openid profile email" button_label: str = "Sign in with SSO" #: Claim to take the StackPilot username from; falls back to email, then sub. username_claim: str = "preferred_username" #: Create an account the first time somebody signs in successfully. auto_create: bool = True default_role: str = "user" #: A claim/value pair that grants admin, e.g. groups = "stackpilot-admins". admin_claim: str = "" admin_value: str = "" #: Exact redirect URI registered with the provider. Empty derives it from #: the request, which is right until a reverse proxy rewrites the scheme. redirect_uri: str = "" created_at: datetime = Field(default_factory=_now) updated_at: datetime = Field(default_factory=_now) class OidcState(SQLModel, table=True): """One in-flight login. A row rather than a dict because it has to survive a worker restart between the redirect out and the redirect back, and because two workers must agree that a given ``state`` may be used exactly once. """ state: str = Field(primary_key=True) verifier: str nonce: str redirect_uri: str created_at: datetime = Field(default_factory=_now) # --- API schemas --- class OidcStatus(SQLModel): """What the login screen is allowed to know before anyone signs in.""" enabled: bool button_label: str class OidcConfigRead(SQLModel): enabled: bool issuer: str client_id: str has_client_secret: bool scopes: str button_label: str username_claim: str auto_create: bool default_role: str admin_claim: str admin_value: str redirect_uri: str #: What the redirect URI would be if left empty — shown so it can be pasted #: into the provider without guessing. suggested_redirect_uri: str class OidcConfigWrite(SQLModel): enabled: bool = False issuer: str = "" client_id: str = "" #: Omitted keeps the stored secret. client_secret: Optional[str] = None scopes: str = "openid profile email" button_label: str = "Sign in with SSO" username_claim: str = "preferred_username" auto_create: bool = True default_role: str = "user" admin_claim: str = "" admin_value: str = "" redirect_uri: str = ""