Files
Unraid-AppOps-Gateway-Public/backend/app/schemas/forge_oidc.py
T

136 lines
5.9 KiB
Python

from __future__ import annotations
import re
from typing import Any, Literal
from uuid import RFC_4122, UUID
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, model_validator
FORGE_OIDC_ACTION = "forge.oidc.provision"
FORGE_OIDC_TARGET = "itworx-forge"
FORGE_OIDC_CLIENT_ID = "itworx-forge"
FORGE_OIDC_APPLICATION_NAME = "ITWorx Forge"
FORGE_OIDC_PROVIDER_NAME = "ITWorx Forge OIDC"
FORGE_OIDC_LAUNCH_URL = "https://forge.example.com"
FORGE_OIDC_CALLBACK_URL = "https://forge.example.com/auth/callback"
FORGE_OIDC_ISSUER = "https://auth.example-host.com/application/o/itworx-forge/"
FORGE_OIDC_DISCOVERY_URL = (
"https://auth.example-host.com/application/o/itworx-forge/.well-known/openid-configuration"
)
FORGE_OIDC_SECRET_REF = "ITWORX_FORGE_OIDC_CLIENT_SECRET"
FORGE_OIDC_BINDING_SECRET_REF = "AUTHENTIK_FORGE_OIDC_BINDING"
FORGE_OIDC_GROUPS = ("forge-viewers", "forge-operators", "forge-owners")
FORGE_OIDC_MANAGED_DESCRIPTION = "Managed by AppOps Forge OIDC v1."
FORGE_OIDC_PROFILE_MAPPING_NAME = "ITWorx Forge profile and groups claim v1"
FORGE_OIDC_PROFILE_MAPPING_EXPRESSION = '''allowed_groups = {"forge-viewers", "forge-operators", "forge-owners"}
return delete_none_values({
"name": request.user.name,
"preferred_username": request.user.username,
"groups": sorted(group.name for group in request.user.ak_groups.all() if group.name in allowed_groups),
})'''
FORGE_OIDC_PROFILE_MAPPING_EXPRESSION_SHA256 = (
"648b3f6dbf2212940309a581b239c9996562ca017a89be22bcfa93f9aa2c26ae"
)
FORGE_OIDC_OPENID_MAPPING_MANAGED = "goauthentik.io/providers/oauth2/scope-openid"
FORGE_OIDC_EMAIL_MAPPING_MANAGED = "goauthentik.io/providers/oauth2/scope-email"
FORGE_OIDC_OPENID_MAPPING_EXPRESSION_SHA256 = (
"38e58da331cc5378d4f269389242aecb80adf30093c405524dc1ab5aab9d452f"
)
FORGE_OIDC_EMAIL_MAPPING_EXPRESSION_SHA256 = (
"d1200eabeffc3b7f379e9c1636367b69befb0b1ac2b5d82b7be20e78667f2b94"
)
class ForgeOidcProvisionRequestV1(BaseModel):
"""Empty, strict request contract; the target and resource fields are fixed."""
model_config = ConfigDict(extra="forbid", frozen=True)
class ForgeOidcScopeMappingIdsV1(BaseModel):
"""The only three scopes attached to the fixed Forge provider."""
model_config = ConfigDict(extra="forbid", frozen=True)
openid: UUID
email: UUID
profile: UUID
@model_validator(mode="before")
@classmethod
def mapping_ids_are_canonical_rfc_uuids(cls, value: Any) -> Any:
if not isinstance(value, dict) or set(value) != {"openid", "email", "profile"}:
raise ValueError("forge_oidc_scope_mapping_ids_invalid")
for field in ("openid", "email", "profile"):
raw = value[field]
if not isinstance(raw, str):
# Pydantic converts ValueError into structured ValidationError output.
raise ValueError("forge_oidc_scope_mapping_id_invalid") # noqa: TRY004
try:
parsed = UUID(raw)
except (TypeError, ValueError, AttributeError):
raise ValueError("forge_oidc_scope_mapping_id_invalid") from None
if (
str(parsed) != raw
or parsed.version not in {1, 2, 3, 4, 5, 6, 7, 8}
or parsed.variant != RFC_4122
):
raise ValueError("forge_oidc_scope_mapping_id_invalid")
return value
@model_validator(mode="after")
def mapping_ids_are_distinct(self) -> ForgeOidcScopeMappingIdsV1:
values = (self.openid, self.email, self.profile)
if len(set(values)) != len(values):
raise ValueError("forge_oidc_scope_mapping_ids_must_be_distinct")
return self
class ForgeOidcBindingV1(BaseModel):
"""Protected, encrypted reference to the remote resources owned by Forge OIDC."""
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
version: StrictInt
provider_id: StrictStr | None = Field(default=None, alias="providerId")
scope_mapping_ids: ForgeOidcScopeMappingIdsV1 = Field(alias="scopeMappingIds")
@model_validator(mode="after")
def provider_id_is_positive_decimal(self) -> ForgeOidcBindingV1:
if self.version != 1:
raise ValueError("forge_oidc_binding_version_unsupported")
if self.provider_id is not None and not self.provider_id.isascii():
raise ValueError("forge_oidc_provider_id_must_be_positive_decimal")
if self.provider_id is not None and (
not re.fullmatch(r"[1-9][0-9]{0,9}", self.provider_id)
):
raise ValueError("forge_oidc_provider_id_must_be_positive_decimal")
return self
class ForgeOidcProvisionResultV1(BaseModel):
"""Safe result projection. It intentionally has no client-secret field."""
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
success: bool
status: Literal["created", "already_configured", "blocked", "conflict", "failed", "partial_failure"]
message: str
client_id: Literal["itworx-forge"] = Field(default=FORGE_OIDC_CLIENT_ID, alias="clientId")
callback_url: Literal["https://forge.example.com/auth/callback"] = Field(
default=FORGE_OIDC_CALLBACK_URL,
alias="callbackUrl",
)
issuer: Literal["https://auth.example-host.com/application/o/itworx-forge/"] = FORGE_OIDC_ISSUER
discovery_url: Literal[
"https://auth.example-host.com/application/o/itworx-forge/.well-known/openid-configuration"
] = Field(default=FORGE_OIDC_DISCOVERY_URL, alias="discoveryUrl")
groups: tuple[Literal["forge-viewers", "forge-operators", "forge-owners"], ...] = FORGE_OIDC_GROUPS
secret_ref: Literal["ITWORX_FORGE_OIDC_CLIENT_SECRET"] = Field(
default=FORGE_OIDC_SECRET_REF,
alias="secretRef",
)
secret_created: bool = Field(default=False, alias="secretCreated")
application_id: str | None = Field(default=None, alias="applicationId")
provider_id: str | None = Field(default=None, alias="providerId")