Publish curated Unraid-AppOps-Gateway source from 451646fee806a47e3256731766980a05a262dcdd

This commit is contained in:
Public Source Publisher committed 2026-10-04 05:23:27 +00:00
1 parent 532f3d9f26
commit 0a82153d95
43 files changed
+17722 -93

No files matched your search

+63 -16
View File
@@ -1,13 +1,23 @@
from __future__ import annotations
from typing import Any
from typing import Annotated, Any
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Request
from sqlmodel import Session
from ..connectors.authentik import AuthentikConnector
from ..core.security import require_operator_request
from ..db.session import get_session
from ..schemas.forge_oidc import ForgeOidcProvisionRequestV1, ForgeOidcProvisionResultV1
from ..schemas.forge_secret_delivery import (
ForgeControllerReaderProvisionRequestV1,
ForgeControllerReaderProvisionResultV1,
)
from ..services.forge_controller_reader import provision_forge_controller_reader
from ..services.forge_oidc_provisioning import provision_forge_oidc
from .connector_runtime import load_connector_runtime
router = APIRouter(prefix='/api/apps/authentik', dependencies=[Depends(load_connector_runtime)])
router = APIRouter(prefix='/api/apps/authentik')
def _shape(payload: dict[str, Any]) -> dict[str, Any]:
@@ -27,66 +37,103 @@ def _connector() -> AuthentikConnector:
return AuthentikConnector()
@router.get('/health')
def _require_forge_oidc_operator(request: Request) -> str:
return require_operator_request(request)
@router.get('/health', dependencies=[Depends(load_connector_runtime)])
def health():
return _shape(_connector().health())
@router.get('/system')
@router.get('/system', dependencies=[Depends(load_connector_runtime)])
def system_status():
return _shape(_connector().system_status())
@router.get('/applications')
@router.get('/applications', dependencies=[Depends(load_connector_runtime)])
def applications():
return _shape(_connector().applications())
@router.get('/providers')
@router.get('/providers', dependencies=[Depends(load_connector_runtime)])
def providers():
return _shape(_connector().providers())
@router.get('/providers/proxy')
@router.get('/providers/proxy', dependencies=[Depends(load_connector_runtime)])
def proxy_providers():
return _shape(_connector().proxy_providers())
@router.get('/providers/oauth2')
@router.get('/providers/oauth2', dependencies=[Depends(load_connector_runtime)])
def oauth2_providers():
return _shape(_connector().oauth2_providers())
@router.get('/outposts')
@router.get('/outposts', dependencies=[Depends(load_connector_runtime)])
def outposts():
return _shape(_connector().outposts())
@router.get('/outposts/health')
@router.get('/outposts/health', dependencies=[Depends(load_connector_runtime)])
def outpost_health():
return _shape(_connector().outpost_health())
@router.get('/flows')
@router.get('/flows', dependencies=[Depends(load_connector_runtime)])
def flows_summary():
return _shape(_connector().flows_summary())
@router.get('/policies')
@router.get('/policies', dependencies=[Depends(load_connector_runtime)])
def policies_summary():
return _shape(_connector().policies_summary())
@router.get('/events')
@router.get('/events', dependencies=[Depends(load_connector_runtime)])
def events(limit: int = 25):
return _shape(_connector().events(limit))
@router.get('/auth-coverage')
@router.get('/auth-coverage', dependencies=[Depends(load_connector_runtime)])
def auth_coverage():
return _shape(_connector().auth_coverage())
@router.get('/security-warnings')
@router.get('/security-warnings', dependencies=[Depends(load_connector_runtime)])
def security_warnings():
return _shape(_connector().security_warnings())
@router.post(
'/forge/oidc/provision',
response_model=ForgeOidcProvisionResultV1,
)
def provision_forge_oidc_application(
payload: ForgeOidcProvisionRequestV1,
request: Request,
session: Annotated[Session, Depends(get_session)],
actor_identity: Annotated[str, Depends(_require_forge_oidc_operator)],
):
"""Run the fixed, manually authorized Forge OIDC provisioning action."""
# The payload is intentionally empty: callers cannot select a provider,
# redirect URI, mapping, groups, or secret. The service repeats the exact
# policy check before making any connector request.
_ = payload
return provision_forge_oidc(session, actor_identity=actor_identity)
@router.post(
'/forge/controller-reader/provision',
response_model=ForgeControllerReaderProvisionResultV1,
)
def provision_forge_controller_reader_account(
payload: ForgeControllerReaderProvisionRequestV1,
request: Request,
session: Annotated[Session, Depends(get_session)],
actor_identity: Annotated[str, Depends(_require_forge_oidc_operator)],
):
"""Provision only the fixed read-only Forge controller service account."""
_ = payload, request
return provision_forge_controller_reader(session, actor_identity=actor_identity)
+76
View File
@@ -16,6 +16,11 @@ from ..core.security import require_operator_request
from ..db.session import get_session
from ..schemas.deployment import DeploymentPlanV1
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
from ..schemas.forge_bootstrap_apply import ForgeBootstrapApplyV1
from ..schemas.forge_secret_delivery import (
ForgeSecretDeliveryRequestV1,
ForgeSecretDeliveryResultV1,
)
from ..services.deployment_broker import DeploymentBrokerClient
from ..services.deployment_control_plane import (
DeploymentControlError,
@@ -117,6 +122,62 @@ def preflight_forge_bootstrap(
raise HTTPException(status_code=409, detail=str(exc)) from exc
@router.post(
"/forge-bootstrap/secrets",
response_model=ForgeSecretDeliveryResultV1,
)
def prepare_forge_bootstrap_secrets(
payload: ForgeSecretDeliveryRequestV1,
actor_identity: Annotated[str, Depends(_require_deployment_operator)],
session: DeploymentSession,
idempotency_key: DeploymentIdempotencyKey = None,
):
"""Deliver only the verified protected Forge inputs to the fixed controller."""
from ..services.forge_secret_delivery import prepare_forge_secret_delivery
try:
return prepare_forge_secret_delivery(
session,
profile=payload.profile,
idempotency_key=_idempotency_header(idempotency_key),
actor_identity=actor_identity,
)
except ValueError as exc:
# Service messages are fixed safe codes; never surface upstream text.
raise HTTPException(status_code=409, detail="forge_secret_delivery_failed") from exc
@router.post("/forge-bootstrap/first-install")
def apply_forge_first_install(
payload: ForgeBootstrapApplyV1,
session: DeploymentSession,
idempotency_key: DeploymentIdempotencyKey = None,
):
from ..services.forge_bootstrap_apply import (
ForgeBootstrapApplyError,
ForgeBootstrapApplyService,
)
try:
return ForgeBootstrapApplyService().apply(
session,
payload,
idempotency_key=_idempotency_header(idempotency_key),
)
except ForgeBootstrapApplyError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
@router.post("/forge-bootstrap/first-install/{job_id}/resume")
def resume_forge_first_install(job_id: str, session: DeploymentSession):
from ..services.forge_bootstrap_apply import ForgeBootstrapApplyService
result = ForgeBootstrapApplyService().resume(session, job_id)
if result is None:
raise HTTPException(status_code=404, detail="Forge first-install request not found.")
return result
@router.get("/workmesh/promotions/{promotion_id}")
def get_workmesh_promotion(promotion_id: str, session: DeploymentSession):
from ..services.workmesh_promotion import WorkMeshPromotionService
@@ -137,6 +198,16 @@ def list_workmesh_promotions(
return {"items": WorkMeshPromotionService.list_promotions(session, limit=limit)}
@router.get("/forge-bootstrap/first-install/{job_id}")
def get_forge_first_install(job_id: str, session: DeploymentSession):
from ..services.forge_bootstrap_apply import ForgeBootstrapApplyService
result = ForgeBootstrapApplyService().status(session, job_id)
if result is None:
raise HTTPException(status_code=404, detail="Forge first-install request not found.")
return result
@router.get("")
def list_deployments(session: DeploymentSession, limit: DeploymentLimit = 100):
return {"items": DeploymentControlPlane().list_jobs(session, limit=limit)}
@@ -150,6 +221,11 @@ def get_delivery_status():
@router.get("/{job_id}")
def get_deployment(job_id: str, session: DeploymentSession):
if job_id.startswith("forge-install-"):
raise HTTPException(
status_code=404,
detail="Use the fixed Forge first-install status route for this job.",
)
try:
return DeploymentControlPlane().refresh(session, job_id)
except DeploymentControlError as exc:
File diff suppressed because it is too large. Load diff
+19
View File
@@ -289,6 +289,18 @@ class Settings:
# The reviewed group mapping must emit this exact claim. The
# enrollment schema only accepts its bounded role contract.
"AUTHENTIK_OIDC_ROLE_CLAIM": _env_value("AUTHENTIK_OIDC_ROLE_CLAIM", None),
# Forge OIDC requires a pre-existing RSA keypair selected by ID;
# the key material remains managed inside Authentik.
"AUTHENTIK_FORGE_OIDC_SIGNING_KEY_ID": _env_value("AUTHENTIK_FORGE_OIDC_SIGNING_KEY_ID", None),
# These IDs are loaded only from the encrypted Forge OIDC binding.
# They deliberately have no environment or generic setup input.
"AUTHENTIK_FORGE_OIDC_BINDING_VALID": False,
"AUTHENTIK_FORGE_OIDC_PROVIDER_ID": None,
"AUTHENTIK_FORGE_OIDC_OPENID_CLAIM_MAPPING": None,
"AUTHENTIK_FORGE_OIDC_EMAIL_CLAIM_MAPPING": None,
"AUTHENTIK_FORGE_OIDC_PROFILE_CLAIM_MAPPING": None,
# The trusted Forge namespace owner is explicit and has no guess/default.
"FORGE_PRIVATE_PACKAGE_OWNER": _env_value("FORGE_PRIVATE_PACKAGE_OWNER", None),
"JELLYFIN_URL": _env_value("JELLYFIN_URL", None),
"JELLYFIN_API_KEY": _env_value("JELLYFIN_API_KEY", None),
"WIDEFROG_URL": _env_value("WIDEFROG_URL", None),
@@ -396,6 +408,13 @@ class Settings:
"AUTHENTIK_OIDC_INVALIDATION_FLOW": bool(self.apis["AUTHENTIK_OIDC_INVALIDATION_FLOW"]),
"AUTHENTIK_OIDC_GROUP_CLAIM_MAPPING": bool(self.apis["AUTHENTIK_OIDC_GROUP_CLAIM_MAPPING"]),
"AUTHENTIK_OIDC_ROLE_CLAIM": bool(self.apis["AUTHENTIK_OIDC_ROLE_CLAIM"]),
"AUTHENTIK_FORGE_OIDC_SIGNING_KEY_ID": bool(self.apis["AUTHENTIK_FORGE_OIDC_SIGNING_KEY_ID"]),
"AUTHENTIK_FORGE_OIDC_BINDING_VALID": bool(self.apis["AUTHENTIK_FORGE_OIDC_BINDING_VALID"]),
"AUTHENTIK_FORGE_OIDC_PROVIDER_ID": bool(self.apis["AUTHENTIK_FORGE_OIDC_PROVIDER_ID"]),
"AUTHENTIK_FORGE_OIDC_OPENID_CLAIM_MAPPING": bool(self.apis["AUTHENTIK_FORGE_OIDC_OPENID_CLAIM_MAPPING"]),
"AUTHENTIK_FORGE_OIDC_EMAIL_CLAIM_MAPPING": bool(self.apis["AUTHENTIK_FORGE_OIDC_EMAIL_CLAIM_MAPPING"]),
"AUTHENTIK_FORGE_OIDC_PROFILE_CLAIM_MAPPING": bool(self.apis["AUTHENTIK_FORGE_OIDC_PROFILE_CLAIM_MAPPING"]),
"FORGE_PRIVATE_PACKAGE_OWNER": bool(self.apis["FORGE_PRIVATE_PACKAGE_OWNER"]),
"JELLYFIN_URL": bool(self.apis["JELLYFIN_URL"]),
"WIDEFROG_URL": bool(self.apis["WIDEFROG_URL"]),
"WIDEFROG_VERIFY_TLS": bool(self.apis["WIDEFROG_VERIFY_TLS"]),
+15 -4
View File
@@ -2,13 +2,22 @@ from datetime import UTC, datetime
from sqlalchemy import inspect, text
from sqlmodel import SQLModel, select
from . import session as db_session
from .session import get_session
from ..core.config import Settings
from ..services.app_classifier import classify_container
from ..services.app_registry import category_label
from .models import App, AppContainerMapping, Connector, DiscoveredContainer, NotificationChannel, NotificationEvent, NotificationRule, Policy, WorkMeshPromotion
from . import session as db_session
from .models import (
App,
AppContainerMapping,
Connector,
DiscoveredContainer,
NotificationChannel,
NotificationEvent,
NotificationRule,
Policy,
)
from .session import get_session
settings = Settings()
@@ -939,4 +948,6 @@ def _seed_policies() -> list[Policy]:
Policy(id="policy-authentik-force-sync", action="authentik.force_sync", target="authentik", voice="blocked", manual="blocked", risk="high", reason="Force sync is a mutating operation and blocked in Connector V1."),
Policy(id="policy-authentik-mass-edit", action="authentik.mass_edit", target="authentik", voice="blocked", manual="blocked", risk="critical", reason="Bulk Authentik mutations are blocked."),
Policy(id="policy-authentik-unknown-mutation", action="authentik.unknown_mutation", target="authentik", voice="blocked", manual="blocked", risk="critical", reason="Unknown Authentik mutations fail closed."),
Policy(id="policy-forge-oidc-provision", action="forge.oidc.provision", target="itworx-forge", voice="blocked", manual="allowed", risk="critical", reason="The fixed Forge OIDC client may be provisioned only by an authenticated manual operator route; voice and generic Authentik mutations remain blocked."),
Policy(id="policy-forge-secret-prepare", action="forge.secrets.prepare", target="itworx-forge", voice="blocked", manual="blocked", risk="critical", reason="Preparing the fixed Forge bootstrap secret bundle is default-denied and requires an explicit exact-target manual operator policy; voice remains blocked."),
]
+59
View File
@@ -28,6 +28,7 @@ from .db.session import engine
from .schemas.deployment import DeploymentPlanV1
from .schemas.enrollment import EnrollmentPlanV1
from .schemas.forge_bootstrap import ForgeBootstrapPreflightV1
from .schemas.forge_bootstrap_apply import ForgeBootstrapApplyV1
from .services import knowledge_service, mcp_operations, safety_center_service
from .services.action_execution import mcp_execution_supported
from .services.action_registry import ACTION_RISK, ACTIONS, action_capability
@@ -696,6 +697,59 @@ def deployment_preflight(plan: dict[str, Any], idempotencyKey: str) -> dict[str,
return {"status": "invalid", "executed": False, "reason": redact_message(str(exc))}
@appops_mcp.tool(name="forge.bootstrap.apply", annotations=_OPERATION_RUN_TOOL)
@_tracked("forge.bootstrap.apply")
def forge_bootstrap_apply(profile: dict[str, Any], idempotencyKey: str) -> dict[str, Any]:
"""Apply one passed, exact-profile Forge first install through its fixed broker route."""
if not _MCP_EXECUTION_LOCK.acquire(blocking=False):
raise RuntimeError("server_operation_capacity_busy")
try:
typed_profile = ForgeBootstrapApplyV1.model_validate(profile)
from .services.forge_bootstrap_apply import (
ForgeBootstrapApplyService,
)
with Session(engine) as session:
return _json(
ForgeBootstrapApplyService().apply(
session,
typed_profile,
idempotency_key=idempotencyKey,
)
)
except ValueError as exc:
return {"status": "invalid", "executed": False, "reason": redact_message(str(exc))}
finally:
_MCP_EXECUTION_LOCK.release()
@appops_mcp.tool(name="forge.bootstrap.status", annotations=_READ_ONLY_TOOL)
@_tracked("forge.bootstrap.status")
def forge_bootstrap_status(jobId: str) -> dict[str, Any]:
"""Reconcile one existing Forge first-install job using the signed read-only route."""
from .services.forge_bootstrap_apply import ForgeBootstrapApplyService
with Session(engine) as session:
result = ForgeBootstrapApplyService().status(session, jobId)
return _json(result or {"status": "not_found"})
@appops_mcp.tool(name="forge.bootstrap.resume", annotations=_OPERATION_RUN_TOOL)
@_tracked("forge.bootstrap.resume")
def forge_bootstrap_resume(jobId: str) -> dict[str, Any]:
"""Resume only a signed, pre-migration Forge database-credentials pause."""
if not _MCP_EXECUTION_LOCK.acquire(blocking=False):
raise RuntimeError("server_operation_capacity_busy")
try:
from .services.forge_bootstrap_apply import ForgeBootstrapApplyService
with Session(engine) as session:
result = ForgeBootstrapApplyService().resume(session, jobId)
return _json(result or {"status": "not_found"})
finally:
_MCP_EXECUTION_LOCK.release()
@appops_mcp.tool(name="deployment.capabilities", annotations=_READ_ONLY_TOOL)
@_tracked("deployment.capabilities")
def deployment_capabilities() -> dict[str, Any]:
@@ -770,6 +824,11 @@ def deployment_apply(plan: dict[str, Any], idempotencyKey: str) -> dict[str, Any
@_tracked("deployment.status")
def deployment_status(jobId: str) -> dict[str, Any]:
"""Read durable deployment state and refresh a bounded broker request."""
if jobId.startswith("forge-install-"):
return {
"status": "invalid",
"reason": "Use forge.bootstrap.status for Forge first-install jobs.",
}
with Session(engine) as session:
try:
return _json(DeploymentControlPlane().refresh(session, jobId))
@@ -0,0 +1,52 @@
from __future__ import annotations
import re
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
_REVISION = re.compile(r"^[0-9a-f]{40}$")
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
_PREFLIGHT_JOB = re.compile(r"^forge-preflight-[0-9a-f]{16}$")
class ForgeBootstrapApplyV1(BaseModel):
"""Fixed first-install input; callers cannot choose runtime or secret inputs."""
model_config = ConfigDict(extra="forbid", strict=True)
api_version: Literal["itworx.forge-first-install/v1"]
revision: str = Field(min_length=40, max_length=40)
workspace_image_digest: str = Field(min_length=71, max_length=71)
frontdoor_image_digest: str = Field(min_length=71, max_length=71)
preflight_job_id: str = Field(min_length=32, max_length=32)
preflight_sha256: str = Field(min_length=64, max_length=64)
@field_validator("revision")
@classmethod
def full_lowercase_revision(cls, value: str) -> str:
if not _REVISION.fullmatch(value):
raise ValueError("revision must be a full lowercase Git SHA")
return value
@field_validator("workspace_image_digest", "frontdoor_image_digest")
@classmethod
def immutable_oci_digest(cls, value: str) -> str:
if not _DIGEST.fullmatch(value):
raise ValueError("image digest must be a lowercase sha256 OCI digest")
return value
@field_validator("preflight_job_id")
@classmethod
def fixed_preflight_job_id(cls, value: str) -> str:
if not _PREFLIGHT_JOB.fullmatch(value):
raise ValueError("preflight_job_id must reference a fixed Forge preflight job")
return value
@field_validator("preflight_sha256")
@classmethod
def fixed_preflight_digest(cls, value: str) -> str:
if not _SHA256.fullmatch(value):
raise ValueError("preflight_sha256 must be a lowercase SHA-256 digest")
return value
+135
View File
@@ -0,0 +1,135 @@
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")
@@ -0,0 +1,71 @@
from __future__ import annotations
import re
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr
from .forge_bootstrap import ForgeBootstrapPreflightV1
_SAFE_CODE = re.compile(r"^[a-z][a-z0-9_]{0,95}$")
_REVISION = re.compile(r"^[0-9a-f]{40}$")
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
_CONTROLLER_VERSION = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$")
class ForgeControllerReaderProvisionRequestV1(BaseModel):
"""Empty request for the single fixed Forge controller-reader provisioner."""
model_config = ConfigDict(extra="forbid", frozen=True, strict=True)
class ForgeControllerReaderProvisionResultV1(BaseModel):
"""Safe response projection; the held reader token is never returned."""
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
success: StrictBool
status: Literal["ready", "blocked", "conflict", "failed", "partial_failure"]
message: StrictStr = Field(pattern=_SAFE_CODE.pattern)
ready: StrictBool
secret_ref: Literal["ITWORX_FORGE_AUTHENTIK_READ_TOKEN"] = Field(alias="secretRef")
class ForgeSecretDeliveryRequestV1(BaseModel):
"""Caller supplies only the existing exact-revision Forge profile."""
model_config = ConfigDict(extra="forbid", frozen=True, strict=True)
profile: ForgeBootstrapPreflightV1
class ForgeSecretDeliveryControllerArtifactV1(BaseModel):
"""Strict safe identity projection from the independently guarded controller."""
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True, strict=True)
controller_version: StrictStr = Field(alias="controllerVersion", pattern=_CONTROLLER_VERSION.pattern)
source_repository: Literal["jens/unraid-appops-gateway"] = Field(alias="sourceRepository")
source_revision: StrictStr = Field(alias="sourceRevision", pattern=_REVISION.pattern)
source_tree: StrictStr = Field(alias="sourceTree", pattern=_REVISION.pattern)
artifact_sha256: StrictStr = Field(alias="artifactSha256", pattern=_SHA256.pattern)
class ForgeSecretDeliveryResultV1(BaseModel):
"""Safe operator result; it contains no bundle material or token hashes."""
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True, strict=True)
success: StrictBool
status: Literal["bundle_ready", "blocked", "conflict", "failed", "outcome_unknown"]
message: StrictStr = Field(pattern=_SAFE_CODE.pattern)
target: Literal["itworx-forge"]
revision: StrictStr = Field(pattern=_REVISION.pattern)
controller_artifact: ForgeSecretDeliveryControllerArtifactV1 | None = Field(
default=None, alias="controllerArtifact"
)
secret_files_ready: StrictInt | None = Field(default=None, alias="secretFilesReady")
master_key_backup_verified: StrictBool | None = Field(
default=None, alias="masterKeyBackupVerified"
)
apply_preflight: Literal["not_evaluated"] | None = Field(default=None, alias="applyPreflight")
+8
View File
@@ -176,6 +176,8 @@ ACTIONS = [
'authentik.force_sync',
'authentik.mass_edit',
'authentik.unknown_mutation',
'forge.oidc.provision',
'forge.secrets.prepare',
'tautulli.terminate_session',
'tautulli.delete_newsletter',
'tautulli.delete_notification_log',
@@ -386,6 +388,8 @@ ACTION_RISK = {
'authentik.force_sync': 'high',
'authentik.mass_edit': 'critical',
'authentik.unknown_mutation': 'critical',
'forge.oidc.provision': 'critical',
'forge.secrets.prepare': 'critical',
'tautulli.terminate_session': 'high',
'tautulli.delete_newsletter': 'critical',
'tautulli.delete_notification_log': 'critical',
@@ -531,6 +535,10 @@ def action_category(action: str) -> str:
def action_default_target(action: str) -> str:
if action == 'forge.oidc.provision':
return 'itworx-forge'
if action == 'forge.secrets.prepare':
return 'itworx-forge'
if action == 'controller.artifact.promote':
return 'tower/deployment-controller'
if action == 'controller.managed-storage.initialize':
+194
View File
@@ -8,6 +8,7 @@ runtime topology before it can mutate production.
from __future__ import annotations
import base64
import hashlib
import hmac
import ipaddress
@@ -27,6 +28,7 @@ from ..core.config import settings
from ..core.redaction import redact_message
from ..schemas.deployment import DeploymentPlanV1
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
from ..schemas.forge_bootstrap_apply import ForgeBootstrapApplyV1
_REQUEST_ID = re.compile(r"^[0-9a-f]{64}$")
_MAX_DELIVERY_STATUS_BYTES = 512 * 1024
@@ -35,6 +37,18 @@ _MAX_CONTROLLER_HEALTH_BYTES = 64 * 1024
_WORKMESH_REVISION = re.compile(r"^[0-9a-f]{40}$")
_WORKMESH_PREFLIGHT_SHA256 = re.compile(r"^[0-9a-f]{64}$")
_WORKMESH_IDEMPOTENCY = re.compile(r"^[A-Za-z0-9._:-]{8,128}$")
_FORGE_SECRET_DELIVERY_PATH = "/v1/appops/forge/bootstrap/secrets"
_MAX_FORGE_SECRET_DELIVERY_BODY_BYTES = 16 * 1024
_MAX_FORGE_SECRET_DELIVERY_RESPONSE_BYTES = 64 * 1024
def _unique_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate_json_key")
result[key] = value
return result
class DeploymentBrokerClient:
@@ -197,6 +211,8 @@ class DeploymentBrokerClient:
value = decoded.get(field)
if isinstance(value, str) and len(value) <= 256:
rejected[field] = value
elif field == "noMutation" and isinstance(value, bool):
rejected[field] = value
return rejected
return {"status": str(decoded.get("status") or "ok"), **decoded}
@@ -314,6 +330,184 @@ class DeploymentBrokerClient:
payload={"profile": profile.model_dump(mode="json")},
)
def forge_bootstrap_apply(
self,
profile: ForgeBootstrapApplyV1,
*,
idempotency_key: str,
secret: str,
) -> dict[str, Any]:
"""Submit only the fixed, preflight-bound Forge first-install profile."""
result = self._request(
"POST",
"/v1/appops/forge/bootstrap/apply",
secret=secret,
payload={
"profile": profile.model_dump(mode="json"),
"idempotencyKey": idempotency_key,
},
error_response_fields=frozenset({
"apiVersion", "request", "repository", "revision", "operation",
"workspaceImageDigest", "frontdoorImageDigest", "noMutation",
}),
)
if result.get("status") == "preflight_changed":
result.pop("reason", None)
return result
def forge_secret_delivery(
self,
profile: ForgeBootstrapPreflightV1,
*,
config_bytes: bytes,
oidc_client_secret: bytes,
authentik_read_token: bytes,
secret: str,
) -> dict[str, Any]:
"""Send the single fixed Forge credential bundle request.
Any response after transmission is treated as an unknown outcome unless
it is a bounded, strict JSON object. Caller-side validation still owns
the complete success contract and never echoes this internal response.
"""
configuration_error = self.configuration_error()
if configuration_error is not None:
return {
"status": "missing_config",
"reason": "forge_secret_delivery_broker_configuration_invalid",
}
if (
not isinstance(secret, str)
or len(secret) < 32
or not secret.isascii()
or not secret.isprintable()
or not isinstance(config_bytes, bytes)
or not 1 <= len(config_bytes) <= 8192
or not isinstance(oidc_client_secret, bytes)
or not 32 <= len(oidc_client_secret) <= 255
or any(byte < 0x20 or byte > 0x7E for byte in oidc_client_secret)
or not isinstance(authentik_read_token, bytes)
or not 32 <= len(authentik_read_token) <= 256
or any(byte < 0x21 or byte > 0x7E for byte in authentik_read_token)
):
return {"status": "missing_secret", "reason": "forge_secret_delivery_inputs_invalid"}
payload = {
"apiVersion": "itworx.forge-secret-delivery/v1",
"target": "itworx-forge",
"profile": profile.model_dump(mode="json"),
"configBase64": base64.b64encode(config_bytes).decode("ascii"),
"oidcClientSecretBase64": base64.b64encode(oidc_client_secret).decode("ascii"),
"authentikReadTokenBase64": base64.b64encode(authentik_read_token).decode("ascii"),
}
body = self._canonical_payload(payload)
payload.clear()
if len(body) > _MAX_FORGE_SECRET_DELIVERY_BODY_BYTES:
return {"status": "missing_secret", "reason": "forge_secret_delivery_request_too_large"}
timestamp = str(int(time.time()))
nonce = uuid.uuid4().hex
path = _FORGE_SECRET_DELIVERY_PATH
headers = {
"Accept": "application/json",
"Accept-Encoding": "identity",
"Content-Type": "application/json",
"X-ITWorx-Deployment-Timestamp": timestamp,
"X-ITWorx-Deployment-Nonce": nonce,
"X-ITWorx-Deployment-Signature": self._signature(
secret, "POST", path, timestamp, nonce, body
),
}
response_body = bytearray()
try:
with httpx.Client(
timeout=self.timeout_seconds,
verify=self._verify_option(),
follow_redirects=False,
) as client, client.stream(
"POST",
f"{self.base_url}{path}",
content=body,
headers=headers,
) as response:
if response.status_code != 200:
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_controller_rejected",
}
content_length = response.headers.get("content-length")
if content_length is not None and (
not content_length.isascii()
or not content_length.isdecimal()
or len(content_length) > 10
or int(content_length) > _MAX_FORGE_SECRET_DELIVERY_RESPONSE_BYTES
):
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_response_invalid",
}
for chunk in response.iter_bytes(chunk_size=16 * 1024):
response_body.extend(chunk)
if len(response_body) > _MAX_FORGE_SECRET_DELIVERY_RESPONSE_BYTES:
response_body.clear()
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_response_invalid",
}
except Exception: # noqa: BLE001 - exception text can contain held credentials.
response_body.clear()
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_transport_unknown",
}
try:
decoded = json.loads(
bytes(response_body).decode("utf-8"),
object_pairs_hook=_unique_json_object,
)
except (UnicodeError, ValueError, TypeError):
response_body.clear()
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_response_invalid",
}
response_body.clear()
if not isinstance(decoded, dict):
return {
"status": "outcome_unknown",
"reason": "forge_secret_delivery_response_invalid",
}
return {"status": "response", "response": decoded}
def forge_bootstrap_resume(self, request_id: str, *, secret: str) -> dict[str, Any]:
"""Resume only one recorded pre-migration credentials blocker."""
if not _REQUEST_ID.fullmatch(request_id):
return {"status": "invalid_request_id"}
result = self._request(
"POST",
f"/v1/appops/forge/bootstrap/resume/{request_id}",
secret=secret,
payload={},
error_response_fields=frozenset({
"apiVersion", "request", "repository", "revision", "operation",
"workspaceImageDigest", "frontdoorImageDigest", "noMutation",
}),
)
if result.get("status") == "preflight_changed":
result.pop("reason", None)
return result
def forge_bootstrap_status(self, request_id: str, *, secret: str) -> dict[str, Any]:
"""Read one fixed Forge first-install request; never submits mutations."""
if not _REQUEST_ID.fullmatch(request_id):
return {"status": "invalid_request_id"}
return self._request(
"GET",
f"/v1/appops/forge/bootstrap/status/{request_id}",
secret=secret,
)
def enrollment_capabilities(self, *, secret: str) -> dict[str, Any]:
"""Read the bootstrap-protected enrollment controller capability."""
return self._request(
@@ -0,0 +1,923 @@
from __future__ import annotations
import base64
import hashlib
import json
import re
from datetime import datetime
from typing import Any
from sqlalchemy import update
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, select
from ..core.config import settings
from ..core.time import utc_now
from ..db.models import DeploymentJob, Policy
from ..schemas.enrollment import MANAGED_DOCKER_APPDATA_ROOT
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
from ..schemas.forge_bootstrap_apply import ForgeBootstrapApplyV1
from .audit_logger import log_event
from .deployment_broker import DeploymentBrokerClient
from .enrollment_plane import EnrollmentPlane
from .policy_engine import evaluate_policy
from .secret_store import get_secret
_API_VERSION = "itworx.forge-first-install/v1"
_PREFLIGHT_API_VERSION = "itworx.forge-bootstrap-preflight/v1"
_APPLICATION_ID = "itworx-forge"
_REPOSITORY = "jens/itworx-forge"
_OPERATION = "forge_first_install"
_SECRET_REF = "FORGE_DEPLOYMENT_BROKER"
_FORGE_SOURCE_URL = "https://gitea.example.com/Jens/itworx-forge"
_FORGE_PRIVATE_PACKAGE_OWNER_SETTING = "FORGE_PRIVATE_PACKAGE_OWNER"
_FORGE_PRIVATE_PACKAGE_OWNER_PATTERN = re.compile(r"[a-z0-9][a-z0-9_-]{0,38}\Z")
_FORGE_OIDC_ISSUER = "https://auth.example-host.com/application/o/itworx-forge/"
_FORGE_OIDC_CLIENT_ID = "itworx-forge"
_FORGE_OIDC_CALLBACK = "https://forge.example.com/auth/callback"
_FORGE_OIDC_GROUPS = ["forge-owners", "forge-operators", "forge-viewers"]
_FORGE_OIDC_SCOPES = ["email", "openid", "profile"]
_FORGE_OIDC_SCOPE_MAPPING_EXPRESSION_SHA256 = {
"openid": "38e58da331cc5378d4f269389242aecb80adf30093c405524dc1ab5aab9d452f",
"email": "d1200eabeffc3b7f379e9c1636367b69befb0b1ac2b5d82b7be20e78667f2b94",
"profile": "648b3f6dbf2212940309a581b239c9996562ca017a89be22bcfa93f9aa2c26ae",
}
_FORGE_OIDC_PROFILE_MAPPING_NAME = "ITWorx Forge profile and groups claim v1"
_FORGE_OIDC_OPENID_MAPPING_MANAGED = "goauthentik.io/providers/oauth2/scope-openid"
_FORGE_OIDC_EMAIL_MAPPING_MANAGED = "goauthentik.io/providers/oauth2/scope-email"
_OIDC_ATTESTATION_FIELDS = {
"issuer", "audience", "clientId", "clientType", "callback", "applicationId",
"providerId", "clientSecretMatched", "authorizationFlowId", "invalidationFlowId",
"roleGroups", "scopeMappingIds", "scopeMappingExpressionSha256",
"profileMappingName", "standardMappingsManaged", "supportedScopes",
"authorizationCodeFlowSupported", "signingKeyId", "signingKeyType",
"signingKeyPrivateAvailable", "signingKeyFingerprintSha256", "signingKeyKid",
"tokenSigningAlgorithm", "jwksKid",
}
_AUTHENTIK_FORGE_OIDC_BINDING_VALID_SETTING = "AUTHENTIK_FORGE_OIDC_BINDING_VALID"
_AUTHENTIK_FORGE_OIDC_PROVIDER_ID_SETTING = "AUTHENTIK_FORGE_OIDC_PROVIDER_ID"
_AUTHENTIK_FORGE_OIDC_SCOPE_MAPPING_SETTINGS = {
"openid": "AUTHENTIK_FORGE_OIDC_OPENID_CLAIM_MAPPING",
"email": "AUTHENTIK_FORGE_OIDC_EMAIL_CLAIM_MAPPING",
"profile": "AUTHENTIK_FORGE_OIDC_PROFILE_CLAIM_MAPPING",
}
_AUTHENTIK_SIGNING_KEY_ID_SETTING = "AUTHENTIK_FORGE_OIDC_SIGNING_KEY_ID"
_AUTHENTIK_AUTHORIZATION_FLOW_SETTING = "AUTHENTIK_OIDC_AUTHORIZATION_FLOW"
_AUTHENTIK_INVALIDATION_FLOW_SETTING = "AUTHENTIK_OIDC_INVALIDATION_FLOW"
_IDEMPOTENCY = re.compile(r"^[A-Za-z0-9._:-]{8,128}$")
_HEALTHY_SERVICES = ["api", "dashboard", "itworx-forge", "postgres", "worker"]
_PROFILE_CHECKS = {
"deployment_apply_policy",
"controller_trust",
"managed_appdata_root_selection",
"exact_main",
"source_manifest",
"managed_appdata",
"proxy_network",
"resource_collisions",
"push_validation",
"image_registry",
"oidc",
"secret_delivery",
}
_FAILED_CODES = {"migration_failed", "service_start_failed", "healthcheck_failed"}
_ACTIVE_STATUSES = {"applying", "outcome_unknown", "needs_database_credentials"}
_TERMINAL_STATUSES = {"deployed", "failed", "preflight_changed"}
class ForgeBootstrapApplyError(ValueError):
"""A fail-closed error in the fixed Forge first-install contract."""
def _canonical_json(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
def _sha256(value: Any) -> str:
return hashlib.sha256(_canonical_json(value)).hexdigest()
def canonical_preflight_receipt(job: DeploymentJob) -> str:
"""Hash the immutable preflight receipt as stored in AppOps, not caller data."""
receipt = {
"apiVersion": _PREFLIGHT_API_VERSION,
"jobId": job.id,
"applicationId": job.application_id,
"repository": job.repository,
"revision": job.revision,
"jobStatus": job.status,
"planSha256": job.plan_sha256,
"profile": dict(job.plan_json or {}),
"preflight": dict(job.preflight_json or {}),
"evidence": dict(job.evidence_json or {}),
}
return _sha256(receipt)
def _idempotency_key(value: str) -> str:
normalized = value.strip()
if not _IDEMPOTENCY.fullmatch(normalized):
raise ForgeBootstrapApplyError("Idempotency-Key must contain 8 to 128 safe characters.")
return normalized
def _unknown_summary() -> str:
return "The fixed Forge controller result is unknown; reconcile its status before retrying."
def _preflight_ready(
session: Session, profile: ForgeBootstrapApplyV1
) -> tuple[DeploymentJob, str]:
job = session.get(DeploymentJob, profile.preflight_job_id)
if (
not job
or job.application_id != _APPLICATION_ID
or job.repository != _REPOSITORY
or job.revision != profile.revision
or job.status != "preflight_succeeded"
or not job.completed_at
or job.plan_sha256 != _sha256(dict(job.plan_json or {}))
):
raise ForgeBootstrapApplyError(
"A matching passed Forge preflight receipt is required before first install."
)
try:
stored_profile = ForgeBootstrapPreflightV1.model_validate(job.plan_json)
except ValueError as exc:
raise ForgeBootstrapApplyError("The stored Forge preflight receipt is invalid.") from exc
if (
stored_profile.revision != profile.revision
or stored_profile.workspace_image_digest != profile.workspace_image_digest
or stored_profile.frontdoor_image_digest != profile.frontdoor_image_digest
):
raise ForgeBootstrapApplyError(
"The stored Forge preflight receipt does not match this profile."
)
preflight = dict(job.preflight_json or {})
if (
set(preflight) != {"apiVersion", "status", "blockers", "unknowns", "checks"}
or preflight.get("apiVersion") != _PREFLIGHT_API_VERSION
or preflight.get("status") != "passed"
or preflight.get("blockers") != []
or preflight.get("unknowns") != []
):
raise ForgeBootstrapApplyError(
"Forge first install requires a passed preflight with no unknown checks."
)
checks = preflight.get("checks")
if not isinstance(checks, list) or len(checks) != len(_PROFILE_CHECKS):
raise ForgeBootstrapApplyError("The stored Forge preflight checks are incomplete.")
seen: set[str] = set()
for check in checks:
if (
not isinstance(check, dict)
or set(check) != {"name", "status", "detail"}
or not isinstance(check.get("name"), str)
or check["name"] not in _PROFILE_CHECKS
or check["name"] in seen
or check.get("status") != "passed"
or not isinstance(check.get("detail"), str)
or len(check["detail"]) > 256
):
raise ForgeBootstrapApplyError(
"Forge first install requires every fixed preflight check to pass."
)
seen.add(check["name"])
if seen != _PROFILE_CHECKS:
raise ForgeBootstrapApplyError("The stored Forge preflight checks are incomplete.")
evidence = dict(job.evidence_json or {})
if (
set(evidence)
!= {"profile", "policyId", "workspaceImageDigest", "frontdoorImageDigest", "controller"}
or evidence.get("profile") != "forge-production-bootstrap"
or not isinstance(evidence.get("policyId"), str)
or evidence.get("workspaceImageDigest") != profile.workspace_image_digest
or evidence.get("frontdoorImageDigest") != profile.frontdoor_image_digest
or not isinstance(evidence.get("controller"), dict)
):
raise ForgeBootstrapApplyError(
"The stored Forge preflight evidence does not match this profile."
)
current_policy_id = _allowed_policy_id(session)
if current_policy_id is None or current_policy_id != evidence["policyId"]:
raise ForgeBootstrapApplyError(
"The exact-target Forge deployment policy changed after preflight."
)
if not _attestations_ready(evidence["controller"], profile):
raise ForgeBootstrapApplyError(
"The stored Forge preflight lacks verified controller attestations."
)
receipt_sha = canonical_preflight_receipt(job)
if profile.preflight_sha256 != receipt_sha:
raise ForgeBootstrapApplyError(
"The supplied preflight receipt digest does not match the stored preflight receipt."
)
return job, receipt_sha
def _allowed_policy_id(session: Session) -> str | None:
policies = session.exec(
select(Policy).where(
(Policy.action == "deployment.apply") & (Policy.target == _APPLICATION_ID)
)
).all()
if len(policies) != 1:
return None
policy = policies[0]
decision = evaluate_policy(
session, action="deployment.apply", target=_APPLICATION_ID, source="manual"
)
allowed = bool(
policy.manual == "allowed"
and policy.voice == "blocked"
and policy.risk == "high"
and decision.get("decision") == "allowed"
and decision.get("policy_id") == policy.id
)
return policy.id if allowed else None
def _attestations_ready(controller: dict[str, Any], profile: ForgeBootstrapApplyV1) -> bool:
attestations = controller.get("attestations")
if not isinstance(attestations, dict) or set(attestations) != {
"push_validation", "image_registry", "oidc", "secret_delivery",
}:
return False
push = attestations.get("push_validation")
registry = attestations.get("image_registry")
oidc = attestations.get("oidc")
secret_delivery = attestations.get("secret_delivery")
private_owner = _protected_forge_private_package_owner()
return bool(
isinstance(push, dict)
and set(push) == {"context", "conclusion", "revision"}
and push == {
"context": "Managed validation / full (push)",
"conclusion": "success",
"revision": profile.revision,
}
and isinstance(registry, dict)
and private_owner is not None
and registry
== {
"owner": private_owner,
"ownerVisibility": "private",
"workspaceDigest": profile.workspace_image_digest,
"frontdoorDigest": profile.frontdoor_image_digest,
"workspacePullable": True,
"frontdoorPullable": True,
"workspaceSource": _FORGE_SOURCE_URL,
"workspaceRevision": profile.revision,
"frontdoorSource": _FORGE_SOURCE_URL,
"frontdoorRevision": profile.revision,
}
and forge_oidc_attestation_matches(oidc)
and isinstance(secret_delivery, dict)
and set(secret_delivery) == {"requiredFilesReady", "metadataSha256"}
and secret_delivery.get("requiredFilesReady") is True
and isinstance(secret_delivery.get("metadataSha256"), str)
and re.fullmatch(r"[0-9a-f]{64}", secret_delivery["metadataSha256"])
)
def _protected_forge_private_package_owner() -> str | None:
owner = settings.apis.get(_FORGE_PRIVATE_PACKAGE_OWNER_SETTING)
if not isinstance(owner, str) or _FORGE_PRIVATE_PACKAGE_OWNER_PATTERN.fullmatch(owner) is None:
return None
return owner
def forge_oidc_attestation_matches(value: object) -> bool:
"""Validate the fixed controller proof against protected AppOps Authentik pins."""
if not isinstance(value, dict) or set(value) != _OIDC_ATTESTATION_FIELDS:
return False
if settings.apis.get(_AUTHENTIK_FORGE_OIDC_BINDING_VALID_SETTING) is not True:
return False
provider_id = settings.apis.get(_AUTHENTIK_FORGE_OIDC_PROVIDER_ID_SETTING)
mapping_ids = {
scope: settings.apis.get(setting)
for scope, setting in _AUTHENTIK_FORGE_OIDC_SCOPE_MAPPING_SETTINGS.items()
}
signing_key_id = settings.apis.get(_AUTHENTIK_SIGNING_KEY_ID_SETTING)
authorization_flow = settings.apis.get(_AUTHENTIK_AUTHORIZATION_FLOW_SETTING)
invalidation_flow = settings.apis.get(_AUTHENTIK_INVALIDATION_FLOW_SETTING)
uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
if (
not isinstance(provider_id, str)
or not re.fullmatch(r"[1-9][0-9]{0,19}", provider_id)
or any(not isinstance(item, str) or not re.fullmatch(uuid_pattern, item) for item in mapping_ids.values())
or len(set(mapping_ids.values())) != len(_AUTHENTIK_FORGE_OIDC_SCOPE_MAPPING_SETTINGS)
or not isinstance(signing_key_id, str)
or not re.fullmatch(uuid_pattern, signing_key_id)
or not isinstance(authorization_flow, str)
or not authorization_flow.strip()
or not isinstance(invalidation_flow, str)
or not invalidation_flow.strip()
or value.get("providerId") != provider_id
or value.get("scopeMappingIds") != mapping_ids
or value.get("signingKeyId") != signing_key_id
or value.get("authorizationFlowId") != authorization_flow
or value.get("invalidationFlowId") != invalidation_flow
):
return False
application_id = value.get("applicationId")
if (
not isinstance(application_id, str)
or not re.fullmatch(uuid_pattern, application_id)
or value.get("providerId") != provider_id
):
return False
fingerprint = value.get("signingKeyFingerprintSha256")
if not isinstance(fingerprint, str) or not re.fullmatch(r"[0-9a-f]{64}", fingerprint):
return False
jwks_kid = value.get("jwksKid")
keypair_kid = value.get("signingKeyKid")
return bool(
value.get("issuer") == _FORGE_OIDC_ISSUER
and value.get("audience") == _FORGE_OIDC_CLIENT_ID
and value.get("clientId") == _FORGE_OIDC_CLIENT_ID
and value.get("clientType") == "confidential"
and value.get("callback") == _FORGE_OIDC_CALLBACK
and value.get("clientSecretMatched") is True
and isinstance(value.get("authorizationFlowId"), str)
and isinstance(value.get("invalidationFlowId"), str)
and value.get("roleGroups") == _FORGE_OIDC_GROUPS
and value.get("profileMappingName") == _FORGE_OIDC_PROFILE_MAPPING_NAME
and isinstance(value.get("scopeMappingExpressionSha256"), dict)
and value.get("scopeMappingExpressionSha256") == _FORGE_OIDC_SCOPE_MAPPING_EXPRESSION_SHA256
and value.get("standardMappingsManaged") is True
and value.get("supportedScopes") == _FORGE_OIDC_SCOPES
and value.get("authorizationCodeFlowSupported") is True
and value.get("signingKeyType") == "RSA"
and value.get("signingKeyPrivateAvailable") is True
and value.get("tokenSigningAlgorithm") == "RS256"
and _forge_oidc_kid_is_valid(keypair_kid)
and jwks_kid == keypair_kid
)
def _forge_oidc_kid_is_valid(value: object) -> bool:
if not isinstance(value, str):
return False
if re.fullmatch(r"[0-9a-f]{32}", value):
return True
if re.fullmatch(r"[A-Za-z0-9_-]{86}", value) is None:
return False
try:
decoded = base64.b64decode(value + "==", altchars=b"-_", validate=True)
except (TypeError, ValueError):
return False
return (
len(decoded) == 64
and base64.urlsafe_b64encode(decoded).decode("ascii").rstrip("=") == value
)
def _controller_secret(session: Session) -> str | None:
trust = EnrollmentPlane.get_trust_config(session)
if (
trust is None
# The fixed Tower controller shares the existing enrollment identity
# with RepoPulse; changing global trust would invalidate that binding.
or trust.controller_identity != "unraid-controller"
or trust.trusted_appdata_root != MANAGED_DOCKER_APPDATA_ROOT
or trust.managed_appdata_backup_acknowledged is not True
):
return None
try:
secret = get_secret(session, _SECRET_REF, touch=False)
except Exception: # noqa: BLE001 - never expose secret store errors.
return None
if (
not isinstance(secret, str)
or len(secret) < 32
or not secret.isascii()
or not secret.isprintable()
):
return None
return secret
def _response_identity_valid(
result: dict[str, Any], *, request_id: str, profile: ForgeBootstrapApplyV1
) -> bool:
return bool(
result.get("apiVersion") == _API_VERSION
and result.get("request") == request_id
and result.get("repository") == _REPOSITORY
and result.get("revision") == profile.revision
and result.get("operation") == _OPERATION
and result.get("workspaceImageDigest") == profile.workspace_image_digest
and result.get("frontdoorImageDigest") == profile.frontdoor_image_digest
)
def _classify_controller_result(
result: object,
*,
request_id: str,
profile: ForgeBootstrapApplyV1,
) -> tuple[str, str, str, dict[str, Any]]:
"""Return status/stage/summary and strictly projected controller evidence."""
if not isinstance(result, dict) or not _response_identity_valid(
result, request_id=request_id, profile=profile
):
return (
"outcome_unknown",
"controller_response_unverified",
_unknown_summary(),
{},
)
status = result.get("status")
base_fields = {
"apiVersion",
"status",
"request",
"repository",
"revision",
"operation",
"workspaceImageDigest",
"frontdoorImageDigest",
}
identity = {key: result[key] for key in sorted(base_fields)}
if status in {"accepted", "running"} and set(result) == base_fields:
return (
"applying",
"controller_accepted" if status == "accepted" else "controller_running",
"Forge first install is running on the fixed controller.",
identity,
)
if status == "needs_database_credentials" and set(result) == base_fields:
return (
"needs_database_credentials",
"controller_needs_database_credentials",
"Forge first install is paused before migration until fixed database credentials are provisioned.",
identity,
)
if (
status == "preflight_changed"
and set(result) == base_fields | {"noMutation"}
and result.get("noMutation") is True
):
return (
"preflight_changed",
"controller_preflight_changed",
"Controller rejected changed Forge first-install preflight before mutation.",
{**identity, "noMutation": True},
)
if status == "deployed" and set(result) == base_fields | {"receipt"}:
receipt = result.get("receipt")
if not isinstance(receipt, dict) or set(receipt) != {
"revision",
"workspaceImageDigest",
"frontdoorImageDigest",
"migration",
"health",
}:
return (
"outcome_unknown",
"controller_receipt_unverified",
_unknown_summary(),
{},
)
migration = receipt.get("migration")
health = receipt.get("health")
if (
receipt.get("revision") != profile.revision
or receipt.get("workspaceImageDigest") != profile.workspace_image_digest
or receipt.get("frontdoorImageDigest") != profile.frontdoor_image_digest
or not isinstance(migration, dict)
or set(migration) != {"service", "status", "result"}
or migration
!= {"service": "migrate", "status": "completed", "result": "succeeded"}
or not isinstance(health, dict)
or set(health) != {"status", "services"}
or health.get("status") != "healthy"
or health.get("services") != _HEALTHY_SERVICES
):
return (
"outcome_unknown",
"controller_receipt_unverified",
_unknown_summary(),
{},
)
safe_receipt = {
"revision": profile.revision,
"workspaceImageDigest": profile.workspace_image_digest,
"frontdoorImageDigest": profile.frontdoor_image_digest,
"migration": dict(migration),
"health": {"status": "healthy", "services": list(_HEALTHY_SERVICES)},
}
return (
"deployed",
"controller_deployed",
"Forge first install completed with exact migration and health receipts.",
{**identity, "receipt": safe_receipt},
)
if status == "failed" and set(result) == base_fields | {"failure"}:
failure = result.get("failure")
if (
isinstance(failure, dict)
and set(failure) == {"code", "preservedData"}
and failure.get("code") in _FAILED_CODES
and failure.get("preservedData") is True
):
return (
"failed",
"controller_failed_preserving_data",
"Controller reported a first-install failure and confirmed PostgreSQL data preservation.",
{
**identity,
"failure": {
"code": failure["code"],
"preservedData": True,
},
},
)
return (
"outcome_unknown",
"controller_response_unverified",
_unknown_summary(),
{},
)
def _serialize(job: DeploymentJob, *, replay: bool = False) -> dict[str, Any]:
return {
"id": job.id,
"applicationId": job.application_id,
"repository": job.repository,
"revision": job.revision,
"status": job.status,
"stage": job.stage,
"preflight": dict(job.preflight_json or {}),
"evidence": dict(job.evidence_json or {}),
"result": job.result_summary,
"request": job.broker_request_id,
"createdAt": job.created_at.isoformat(),
"updatedAt": job.updated_at.isoformat(),
"completedAt": job.completed_at.isoformat() if job.completed_at else None,
"idempotentReplay": replay,
}
class ForgeBootstrapApplyService:
"""One fixed Forge first-install operation with durable, non-replaying apply."""
def __init__(self, *, broker: DeploymentBrokerClient | None = None):
self.broker = broker or DeploymentBrokerClient()
@staticmethod
def request_id(profile: ForgeBootstrapApplyV1, idempotency_key: str) -> str:
normalized = _idempotency_key(idempotency_key)
payload = {
"repository": _REPOSITORY,
"operation": _OPERATION,
"profile": profile.model_dump(mode="json"),
"idempotencyKey": normalized,
}
return _sha256(payload)
def apply(
self,
session: Session,
profile: ForgeBootstrapApplyV1,
*,
idempotency_key: str,
) -> dict[str, Any]:
normalized_key = _idempotency_key(idempotency_key)
idempotency_digest = hashlib.sha256(
f"forge-first-install:{normalized_key}".encode()
).hexdigest()
profile_json = profile.model_dump(mode="json")
plan_sha = _sha256(profile_json)
existing = session.exec(
select(DeploymentJob).where(
DeploymentJob.idempotency_sha256 == idempotency_digest
)
).first()
if existing:
if (
existing.application_id != _APPLICATION_ID
or existing.repository != _REPOSITORY
):
raise ForgeBootstrapApplyError(
"Idempotency-Key already belongs to a different application or repository."
)
if existing.plan_sha256 != plan_sha:
raise ForgeBootstrapApplyError(
"Idempotency-Key was already used for a different Forge first-install profile."
)
return self._refresh_job(session, existing, replay=True)
preflight_job, receipt_sha = _preflight_ready(session, profile)
if _allowed_policy_id(session) is None:
raise ForgeBootstrapApplyError(
"An allowed exact-target deployment.apply policy is required for Forge first install."
)
secret = _controller_secret(session)
if secret is None:
raise ForgeBootstrapApplyError(
"Stored managed-root trust and the fixed Forge deployment secret are required."
)
request_id = self.request_id(profile, normalized_key)
job_id = f"forge-install-{idempotency_digest}"
now = utc_now()
job = DeploymentJob(
id=job_id,
application_id=_APPLICATION_ID,
repository=_REPOSITORY,
revision=profile.revision,
plan_sha256=plan_sha,
idempotency_sha256=idempotency_digest,
status="applying",
stage="controller_request_pending",
plan_json=profile_json,
preflight_json={
"apiVersion": _PREFLIGHT_API_VERSION,
"jobId": preflight_job.id,
"sha256": receipt_sha,
},
evidence_json={"profile": "forge-first-install", "controller": {}},
broker_request_id=request_id,
result_summary="Forge first install is awaiting the fixed controller response.",
created_at=now,
updated_at=now,
)
session.add(job)
try:
# The durable request intent must exist before the only POST attempt.
session.commit()
session.refresh(job)
except IntegrityError:
session.rollback()
raced = session.get(DeploymentJob, job_id)
if raced and (
raced.application_id != _APPLICATION_ID
or raced.repository != _REPOSITORY
):
raise ForgeBootstrapApplyError(
"Idempotency-Key already belongs to a different application or repository."
) from None
if raced and raced.plan_sha256 == plan_sha:
return self._refresh_job(session, raced, replay=True)
raise ForgeBootstrapApplyError(
"The Forge first-install request could not be durably claimed."
) from None
log_event(
session,
source="deployment-control-plane",
identity="operator",
action_id="deployment.apply",
target=_APPLICATION_ID,
target_type="deployment",
risk="high",
policy_decision="allowed",
reason="Submitted one fixed Forge first-install request after exact preflight validation.",
result="submitted",
execution_plan={
"request": request_id,
"planSha256": plan_sha,
"preflightSha256": receipt_sha,
},
redacted_metadata={"profile": "forge-first-install", "jobId": job_id},
)
try:
response = self.broker.forge_bootstrap_apply(
profile,
idempotency_key=normalized_key,
secret=secret,
)
except Exception: # noqa: BLE001 - durable intent is reconciled with GET only.
response = {"status": "unreachable"}
return self._record_response(
session,
job,
response,
profile,
expected_updated_at=job.updated_at,
)
def status(self, session: Session, job_id: str) -> dict[str, Any] | None:
if not re.fullmatch(r"forge-install-[0-9a-f]{64}", job_id):
return None
job = session.get(DeploymentJob, job_id)
if (
not job
or job.application_id != _APPLICATION_ID
or job.repository != _REPOSITORY
or not re.fullmatch(r"[0-9a-f]{64}", job.broker_request_id or "")
):
return None
return self._refresh_job(session, job)
def resume(self, session: Session, job_id: str) -> dict[str, Any] | None:
"""Explicitly resume only after a signed read proves pre-migration pause."""
if not re.fullmatch(r"forge-install-[0-9a-f]{64}", job_id):
return None
job = session.get(DeploymentJob, job_id)
if (
not job
or job.application_id != _APPLICATION_ID
or job.repository != _REPOSITORY
or not re.fullmatch(r"[0-9a-f]{64}", job.broker_request_id or "")
):
return None
# A caller cannot turn an old pending/unknown record into another POST.
# First reconcile with the controller, then proceed only from its exact
# safe pre-migration credential state.
refreshed = self._refresh_job(session, job)
if refreshed.get("status") != "needs_database_credentials":
return refreshed
job = session.get(DeploymentJob, job_id)
if job is None:
return refreshed
session.refresh(job)
try:
profile = ForgeBootstrapApplyV1.model_validate(job.plan_json)
except ValueError:
return refreshed
preflight_receipt = dict(job.preflight_json or {})
if (
set(preflight_receipt) != {"apiVersion", "jobId", "sha256"}
or preflight_receipt.get("apiVersion") != _PREFLIGHT_API_VERSION
or preflight_receipt.get("jobId") != profile.preflight_job_id
or preflight_receipt.get("sha256") != profile.preflight_sha256
):
return refreshed
try:
_preflight_ready(session, profile)
except ForgeBootstrapApplyError:
return refreshed
secret = _controller_secret(session)
if not secret:
return refreshed
expected_updated_at = job.updated_at
resume_pending = "Forge database credentials passed the signed pre-migration gate; resume is pending."
claimed_at = utc_now()
# Compare-and-swap the observed, signed pre-migration state. Concurrent
# callers can both reconcile it, but only one can persist the resume claim.
claim = session.exec(
update(DeploymentJob)
.where(
DeploymentJob.id == job.id,
DeploymentJob.application_id == _APPLICATION_ID,
DeploymentJob.repository == _REPOSITORY,
DeploymentJob.status == "needs_database_credentials",
DeploymentJob.stage == "controller_needs_database_credentials",
DeploymentJob.updated_at == expected_updated_at,
)
.values(
status="applying",
stage="resume_request_pending",
result_summary=resume_pending,
updated_at=claimed_at,
)
)
if claim.rowcount != 1:
session.rollback()
current = session.get(DeploymentJob, job_id)
if current is None:
return refreshed
session.refresh(current)
return _serialize(current)
# The one-shot intent is durable before the only resume POST attempt.
session.commit()
session.refresh(job)
resume_intent_version = job.updated_at
log_event(
session,
source="deployment-control-plane",
identity="operator",
action_id="deployment.apply",
target=_APPLICATION_ID,
target_type="deployment",
risk="high",
policy_decision="allowed",
reason="Requested one explicit Forge first-install resume after signed pre-migration credential status.",
result="resume_submitted",
execution_plan={"request": job.broker_request_id, "jobId": job.id},
redacted_metadata={"profile": "forge-first-install"},
)
try:
response = self.broker.forge_bootstrap_resume(
job.broker_request_id or "", secret=secret
)
except Exception: # noqa: BLE001 - durable resume intent is reconciled with GET only.
response = {"status": "unreachable"}
return self._record_response(
session,
job,
response,
profile,
expected_updated_at=resume_intent_version,
resume_claimed=True,
)
def _refresh_job(
self,
session: Session,
job: DeploymentJob,
*,
replay: bool = False,
) -> dict[str, Any]:
if job.status not in _ACTIVE_STATUSES:
return _serialize(job, replay=replay)
try:
profile = ForgeBootstrapApplyV1.model_validate(job.plan_json)
except ValueError:
return _serialize(job, replay=replay)
secret = _controller_secret(session)
expected_updated_at = job.updated_at
resume_claimed = job.stage.startswith("resume")
if not secret:
return self._record_response(
session,
job,
{"status": "unreachable"},
profile,
replay=replay,
expected_updated_at=expected_updated_at,
resume_claimed=resume_claimed,
)
try:
response = self.broker.forge_bootstrap_status(
job.broker_request_id or "", secret=secret
)
except Exception: # noqa: BLE001 - reconciliation failures remain unknown.
response = {"status": "unreachable"}
return self._record_response(
session,
job,
response,
profile,
replay=replay,
expected_updated_at=expected_updated_at,
resume_claimed=resume_claimed,
)
@staticmethod
def _record_response(
session: Session,
job: DeploymentJob,
response: object,
profile: ForgeBootstrapApplyV1,
*,
replay: bool = False,
expected_updated_at: datetime | None = None,
resume_claimed: bool = False,
) -> dict[str, Any]:
expected_updated_at = expected_updated_at or job.updated_at
status, stage, result, evidence = _classify_controller_result(
response,
request_id=job.broker_request_id or "",
profile=profile,
)
resume_claimed = resume_claimed or job.stage.startswith("resume")
if resume_claimed:
if status == "needs_database_credentials":
status = "outcome_unknown"
stage = "resume_status_unresolved"
result = (
"Resume was already claimed but the controller still reports its pre-migration pause; "
"reconcile by status without submitting another resume."
)
else:
stage = f"resume_{stage}"
next_updated_at = utc_now()
next_completed_at = (
utc_now() if status in _TERMINAL_STATUSES else job.completed_at
)
values = {
"status": status,
"stage": stage,
"result_summary": result,
"evidence_json": {"profile": "forge-first-install", "controller": evidence},
"updated_at": next_updated_at,
"completed_at": next_completed_at,
}
statement = update(DeploymentJob).where(
DeploymentJob.id == job.id,
DeploymentJob.updated_at == expected_updated_at,
)
if status == "needs_database_credentials" and not resume_claimed:
# A status GET started after the local resume claim may still race a
# controller that has not processed the POST. It cannot reopen the gate.
statement = statement.where(DeploymentJob.stage.not_like("resume%"))
result_update = session.exec(statement.values(**values))
if result_update.rowcount != 1:
session.rollback()
current = session.get(DeploymentJob, job.id)
if current is None:
return _serialize(job, replay=replay)
session.refresh(current)
return _serialize(current, replay=replay)
session.commit()
session.refresh(job)
return _serialize(job, replay=replay)
@@ -14,6 +14,12 @@ from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
from .audit_logger import log_event
from .deployment_broker import DeploymentBrokerClient
from .enrollment_plane import EnrollmentPlane
from .forge_bootstrap_apply import (
_FORGE_SOURCE_URL,
_protected_forge_private_package_owner,
canonical_preflight_receipt,
forge_oidc_attestation_matches,
)
from .policy_engine import evaluate_policy
from .secret_store import get_secret, has_secret
@@ -71,6 +77,21 @@ _STATUS_DETAILS = {
"blocked": "A configured required push validation context rejected this exact commit.",
"unknown": "Exact-commit push validation is not configured or cannot be verified.",
},
"image_registry": {
"passed": "The fixed private Forge image owner and both immutable image digests are authenticated and pullable.",
"blocked": "One or both immutable Forge image digests are unavailable from the registry.",
"unknown": "Registry publication or pull authorization could not be verified.",
},
"oidc": {
"passed": "The fixed Forge OIDC provider, application, and role groups are ready.",
"blocked": "The Forge OIDC proof is missing, mismatched, or differs from the protected AppOps mapping and signing-key IDs.",
"unknown": "The fixed Forge OIDC configuration could not be verified.",
},
"secret_delivery": {
"passed": "The fixed Forge secret files have the required safe metadata.",
"blocked": "One or more fixed Forge secret files are missing or unsafe.",
"unknown": "The fixed Forge secret-file metadata could not be verified.",
},
}
@@ -114,10 +135,6 @@ def _checked_controller_result(
raw = raw_checks.get(name)
if not isinstance(raw, dict) or set(raw) != {"status"} or raw.get("status") not in {"passed", "blocked", "unknown"}:
return None
# CI, registry access, OIDC and secret delivery are intentionally
# unknown until a configured, source-authenticated proof exists.
if name in {"image_registry", "oidc", "secret_delivery"} and raw["status"] != "unknown":
return None
checks.append(_check(name, raw["status"]))
statuses = [item["status"] for item in checks]
expected_status = "preflight_blocked" if "blocked" in statuses else "preflight_unknown" if "unknown" in statuses else "preflight_ok"
@@ -126,7 +143,7 @@ def _checked_controller_result(
raw_evidence = result.get("evidence", {})
if not isinstance(raw_evidence, dict) or set(raw_evidence) - {
"mainRevision", "sourceManifestSha256", "resourceCollisionCount",
"workspaceImagePresent", "frontdoorImagePresent",
"workspaceImagePresent", "frontdoorImagePresent", "attestations",
}:
return None
evidence: dict[str, Any] = {}
@@ -148,6 +165,84 @@ def _checked_controller_result(
if not isinstance(value, bool):
return None
evidence[name] = value
attestations = raw_evidence.get("attestations")
oidc_rejected = False
validated_attestations: dict[str, dict[str, Any]] = {}
if attestations is not None:
if not isinstance(attestations, dict) or set(attestations) - {
"push_validation", "image_registry", "oidc", "secret_delivery",
}:
return None
push = attestations.get("push_validation")
if push is not None and (
not isinstance(push, dict)
or set(push) != {"context", "conclusion", "revision"}
or push.get("context") != "Managed validation / full (push)"
or push.get("conclusion") != "success"
or push.get("revision") != profile.revision
):
return None
if push is not None:
validated_attestations["push_validation"] = dict(push)
registry = attestations.get("image_registry")
private_owner = _protected_forge_private_package_owner()
if registry is not None and (
not isinstance(registry, dict)
or private_owner is None
or registry.get("workspacePullable") is not True
or registry.get("frontdoorPullable") is not True
or registry != {
"owner": private_owner,
"ownerVisibility": "private",
"workspaceDigest": profile.workspace_image_digest,
"frontdoorDigest": profile.frontdoor_image_digest,
"workspacePullable": True,
"frontdoorPullable": True,
"workspaceSource": _FORGE_SOURCE_URL,
"workspaceRevision": profile.revision,
"frontdoorSource": _FORGE_SOURCE_URL,
"frontdoorRevision": profile.revision,
}
):
return None
if registry is not None:
validated_attestations["image_registry"] = dict(registry)
oidc = attestations.get("oidc")
if oidc is not None:
if raw_checks["oidc"]["status"] != "passed":
return None
if forge_oidc_attestation_matches(oidc):
validated_attestations["oidc"] = dict(oidc)
else:
oidc_rejected = True
elif raw_checks["oidc"]["status"] == "passed":
oidc_rejected = True
secret_delivery = attestations.get("secret_delivery")
if secret_delivery is not None and (
not isinstance(secret_delivery, dict)
or set(secret_delivery) != {"requiredFilesReady", "metadataSha256"}
or secret_delivery.get("requiredFilesReady") is not True
or not isinstance(secret_delivery.get("metadataSha256"), str)
or not re.fullmatch(r"^[0-9a-f]{64}$", secret_delivery["metadataSha256"])
):
return None
if secret_delivery is not None:
validated_attestations["secret_delivery"] = dict(secret_delivery)
if any(
raw_checks[name]["status"] == "passed" and
(not isinstance(attestations, dict) or name not in attestations)
for name in ("push_validation", "image_registry", "secret_delivery")
):
return None
if raw_checks["oidc"]["status"] == "passed" and "oidc" not in validated_attestations:
oidc_rejected = True
if oidc_rejected:
for check in checks:
if check["name"] == "oidc":
check.update(_check("oidc", "blocked"))
break
if validated_attestations:
evidence["attestations"] = validated_attestations
return checks, evidence
@@ -260,6 +355,7 @@ class ForgeBootstrapPreflightService:
},
evidence_json={
"profile": "forge-production-bootstrap",
"policyId": exact[0].id if policy_ready else None,
"workspaceImageDigest": profile.workspace_image_digest,
"frontdoorImageDigest": profile.frontdoor_image_digest,
"controller": controller_evidence,
@@ -302,6 +398,7 @@ class ForgeBootstrapPreflightService:
"status": job.status,
"stage": job.stage,
"preflight": job.preflight_json,
"preflightSha256": canonical_preflight_receipt(job),
"evidence": job.evidence_json,
"result": job.result_summary,
"brokerRequestId": job.broker_request_id,
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,834 @@
from __future__ import annotations
import json
import os
import secrets
import stat
import time
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from threading import Lock
from typing import Any
from sqlmodel import Session, select
from ..connectors.authentik import AuthentikConnector
from ..core.config import settings
from ..db.models import Policy
from ..schemas.forge_oidc import (
FORGE_OIDC_ACTION,
FORGE_OIDC_BINDING_SECRET_REF,
FORGE_OIDC_CLIENT_ID,
FORGE_OIDC_SECRET_REF,
FORGE_OIDC_TARGET,
ForgeOidcBindingV1,
ForgeOidcProvisionResultV1,
ForgeOidcScopeMappingIdsV1,
)
from .audit_logger import log_event
from .policy_engine import evaluate_policy
from .secret_store import get_secret, has_secret, secret_storage_status, set_secret
_SECRET_LOCK = Lock()
_PROVISION_LOCK_FILENAME = ".forge-oidc-provision.lock"
_PROVISION_LOCK_TIMEOUT_SECONDS = 30.0
_PROVISION_LOCK_POLL_INTERVAL_SECONDS = 0.05
_FORGE_OIDC_LOCK_ROOT = Path("/run/itworx-appops/forge-oidc-provision")
class _ProvisionLockUnavailable(RuntimeError):
pass
def _is_reparse_point(stat_result: os.stat_result) -> bool:
attributes = getattr(stat_result, "st_file_attributes", 0)
reparse_flag = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
return bool(attributes & reparse_flag)
def _windows_current_user_sid() -> bytes:
"""Return the effective Windows token user SID without shelling out."""
import ctypes
from ctypes import wintypes
advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
advapi32.OpenProcessToken.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)]
advapi32.OpenProcessToken.restype = wintypes.BOOL
advapi32.GetTokenInformation.argtypes = [
wintypes.HANDLE,
wintypes.DWORD,
ctypes.c_void_p,
wintypes.DWORD,
ctypes.POINTER(wintypes.DWORD),
]
advapi32.GetTokenInformation.restype = wintypes.BOOL
advapi32.GetLengthSid.argtypes = [ctypes.c_void_p]
advapi32.GetLengthSid.restype = wintypes.DWORD
kernel32.GetCurrentProcess.argtypes = []
kernel32.GetCurrentProcess.restype = wintypes.HANDLE
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
kernel32.CloseHandle.restype = wintypes.BOOL
token = wintypes.HANDLE()
if not advapi32.OpenProcessToken(
kernel32.GetCurrentProcess(), 0x0008, ctypes.byref(token)
):
raise OSError(ctypes.get_last_error(), "OpenProcessToken failed")
try:
required = wintypes.DWORD()
advapi32.GetTokenInformation(token, 1, None, 0, ctypes.byref(required))
if not required.value:
raise OSError(ctypes.get_last_error(), "GetTokenInformation failed")
token_user = ctypes.create_string_buffer(required.value)
if not advapi32.GetTokenInformation(
token, 1, token_user, required.value, ctypes.byref(required)
):
raise OSError(ctypes.get_last_error(), "GetTokenInformation failed")
sid = ctypes.cast(token_user, ctypes.POINTER(ctypes.c_void_p)).contents.value
sid_length = advapi32.GetLengthSid(ctypes.c_void_p(sid))
if not sid or not sid_length:
raise OSError("invalid current-user SID")
return ctypes.string_at(sid, sid_length)
finally:
kernel32.CloseHandle(token)
def _windows_private_path(path: Path, *, require_current_owner: bool) -> None:
"""Reject Windows paths whose owner/DACL lets an untrusted principal mutate them."""
import ctypes
from ctypes import wintypes
advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
advapi32.GetNamedSecurityInfoW.argtypes = [
wintypes.LPWSTR,
wintypes.DWORD,
wintypes.DWORD,
ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.c_void_p),
]
advapi32.GetNamedSecurityInfoW.restype = wintypes.DWORD
advapi32.GetLengthSid.argtypes = [ctypes.c_void_p]
advapi32.GetLengthSid.restype = wintypes.DWORD
advapi32.ConvertStringSidToSidW.argtypes = [wintypes.LPWSTR, ctypes.POINTER(ctypes.c_void_p)]
advapi32.ConvertStringSidToSidW.restype = wintypes.BOOL
advapi32.GetAclInformation.argtypes = [
ctypes.c_void_p,
ctypes.c_void_p,
wintypes.DWORD,
wintypes.DWORD,
]
advapi32.GetAclInformation.restype = wintypes.BOOL
advapi32.GetAce.argtypes = [ctypes.c_void_p, wintypes.DWORD, ctypes.POINTER(ctypes.c_void_p)]
advapi32.GetAce.restype = wintypes.BOOL
kernel32.LocalFree.argtypes = [ctypes.c_void_p]
kernel32.LocalFree.restype = ctypes.c_void_p
owner = ctypes.c_void_p()
dacl = ctypes.c_void_p()
descriptor = ctypes.c_void_p()
# SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
result = advapi32.GetNamedSecurityInfoW(
str(path),
1,
0x1 | 0x4,
ctypes.byref(owner),
None,
ctypes.byref(dacl),
None,
ctypes.byref(descriptor),
)
if result != 0 or not owner.value or not dacl.value:
raise OSError(int(result or ctypes.get_last_error()), "security descriptor unavailable")
try:
current_sid = _windows_current_user_sid()
sid_size = advapi32.GetLengthSid(owner)
owner_sid = ctypes.string_at(owner.value, sid_size)
if require_current_owner and owner_sid != current_sid:
raise OSError("path owner is not the current user")
# SYSTEM and local Administrators are trusted operating-system principals.
trusted_sids = {current_sid}
for sid_text in (
"S-1-5-18", # LocalSystem
"S-1-5-32-544", # Builtin Administrators
"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464", # TrustedInstaller
):
sid_buffer = ctypes.c_void_p()
if not advapi32.ConvertStringSidToSidW(sid_text, ctypes.byref(sid_buffer)):
raise OSError(ctypes.get_last_error(), "trusted SID unavailable")
try:
sid_size = advapi32.GetLengthSid(sid_buffer)
trusted_sids.add(ctypes.string_at(sid_buffer.value, sid_size))
finally:
kernel32.LocalFree(sid_buffer)
# Reject any explicit or inherited allow ACE outside the trusted set that
# can create/replace/delete this path or write to a lock file.
class _AclSizeInformation(ctypes.Structure):
_fields_ = [
("AceCount", wintypes.DWORD),
("AclBytesInUse", wintypes.DWORD),
("AclBytesFree", wintypes.DWORD),
]
acl_info = _AclSizeInformation()
if not advapi32.GetAclInformation(
dacl,
ctypes.byref(acl_info),
ctypes.sizeof(acl_info),
2,
):
raise OSError(ctypes.get_last_error(), "ACL unavailable")
write_rights = (
0x00000002 # FILE_WRITE_DATA / FILE_ADD_FILE
| 0x00000004 # FILE_APPEND_DATA / FILE_ADD_SUBDIRECTORY
| 0x00000010 # FILE_WRITE_EA
| 0x00000040 # FILE_DELETE_CHILD
| 0x00000100 # FILE_WRITE_ATTRIBUTES
| 0x00010000 # DELETE
| 0x00040000 # WRITE_DAC
| 0x00080000 # WRITE_OWNER
| 0x10000000 # GENERIC_ALL
| 0x40000000 # GENERIC_WRITE
)
for index in range(acl_info.AceCount):
ace = ctypes.c_void_p()
if not advapi32.GetAce(dacl, index, ctypes.byref(ace)):
raise OSError(ctypes.get_last_error(), "ACL entry unavailable")
header = ctypes.cast(ace, ctypes.POINTER(ctypes.c_ubyte * 2)).contents
ace_type = int(header[0])
if ace_type != 0: # ACCESS_ALLOWED_ACE
if ace_type not in {1, 2, 3, 6, 7, 8, 10, 12, 13, 14}:
# Deny and audit entries do not grant rights. Unknown ACEs
# could grant access and therefore make this path untrusted.
raise OSError("unsupported ACL entry")
continue
mask = ctypes.cast(
ctypes.c_void_p(ace.value + 4), ctypes.POINTER(wintypes.DWORD)
).contents.value
sid_address = ace.value + 8
sid_size = advapi32.GetLengthSid(ctypes.c_void_p(sid_address))
ace_sid = ctypes.string_at(sid_address, sid_size)
if mask & write_rights and ace_sid not in trusted_sids:
raise OSError("path ACL grants write access to an untrusted principal")
finally:
if descriptor.value:
kernel32.LocalFree(descriptor)
def _lock_root() -> Path:
# Linux containers share this private runtime path among their worker
# processes. Windows keeps the equivalent lock beside its protected data.
return Path(settings.data_dir) if os.name == "nt" else _FORGE_OIDC_LOCK_ROOT
def _validate_lock_data_dir(data_dir: Path) -> None:
if not data_dir.is_absolute():
raise OSError("lock directory must be absolute")
# Inspect each existing path component without following symlinks/reparse points.
components = [Path(data_dir.anchor)]
for part in data_dir.parts[1:]:
components.append(components[-1] / part)
private_components = {data_dir, data_dir.parent}
for index, component in enumerate(components):
try:
component_stat = os.lstat(component)
except FileNotFoundError:
if os.name == "nt" or component not in private_components:
raise OSError("lock directory path is missing") from None
try:
os.mkdir(component, 0o700)
except FileExistsError:
# Another worker may have created the same fixed private child;
# validate its metadata below before using it.
pass
component_stat = os.lstat(component)
if _is_reparse_point(component_stat) or not stat.S_ISDIR(component_stat.st_mode):
raise OSError("lock directory contains an unsafe path component")
is_private_component = component in private_components
if os.name == "nt":
_windows_private_path(component, require_current_owner=component == data_dir)
continue
effective_uid = os.geteuid()
if component_stat.st_uid not in {effective_uid, 0}:
raise OSError("lock directory path has an untrusted owner")
mode = stat.S_IMODE(component_stat.st_mode)
if is_private_component:
if component_stat.st_uid != effective_uid or mode != 0o700:
raise OSError("lock directory is not private to its owner")
elif mode & 0o022 and not (
component_stat.st_uid == 0
and mode & stat.S_ISVTX
and mode & 0o002
):
# Root-owned sticky directories such as /tmp safely contain private,
# owner-checked children; other writable ancestors are not trusted.
raise OSError("lock directory has a writable untrusted ancestor")
def _validate_lock_file(lock_fd: int, lock_path: Path) -> None:
opened = os.fstat(lock_fd)
named = os.lstat(lock_path)
if (
_is_reparse_point(named)
or not stat.S_ISREG(opened.st_mode)
or not stat.S_ISREG(named.st_mode)
or not os.path.samestat(opened, named)
or opened.st_nlink != 1
or opened.st_size > 1
):
raise OSError("lock file is not a private regular file")
if os.name == "nt":
_windows_private_path(lock_path, require_current_owner=True)
return
if opened.st_uid != os.geteuid() or stat.S_IMODE(opened.st_mode) & 0o077:
raise OSError("lock file is not private to its owner")
@contextmanager
def _forge_oidc_process_lock() -> Iterator[None]:
"""Serialize provisioning across workers in one AppOps runtime."""
deadline = time.monotonic() + _PROVISION_LOCK_TIMEOUT_SECONDS
thread_locked = _SECRET_LOCK.acquire(
timeout=max(0.0, deadline - time.monotonic())
)
if not thread_locked:
raise _ProvisionLockUnavailable from None
lock_fd: int | None = None
locked = False
try:
try:
lock_root = _lock_root()
_validate_lock_data_dir(lock_root)
lock_path = lock_root / _PROVISION_LOCK_FILENAME
flags = os.O_RDWR
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
if hasattr(os, "O_CLOEXEC"):
flags |= os.O_CLOEXEC
try:
lock_fd = os.open(str(lock_path), flags | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError:
lock_fd = os.open(str(lock_path), flags)
_validate_lock_file(lock_fd, lock_path)
if os.name == "nt":
import msvcrt
if os.fstat(lock_fd).st_size == 0:
os.write(lock_fd, b"\0")
_validate_lock_file(lock_fd, lock_path)
while True:
os.lseek(lock_fd, 0, os.SEEK_SET)
try:
msvcrt.locking(lock_fd, msvcrt.LK_NBLCK, 1)
break
except OSError:
if time.monotonic() >= deadline:
raise TimeoutError("provision_lock_timeout") from None
time.sleep(
min(
_PROVISION_LOCK_POLL_INTERVAL_SECONDS,
max(0.0, deadline - time.monotonic()),
)
)
else:
import fcntl
while True:
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except BlockingIOError:
if time.monotonic() >= deadline:
raise TimeoutError("provision_lock_timeout") from None
time.sleep(
min(
_PROVISION_LOCK_POLL_INTERVAL_SECONDS,
max(0.0, deadline - time.monotonic()),
)
)
locked = True
except Exception: # noqa: BLE001 - all lock setup errors fail closed.
# Keep platform-specific path/ACL errors internal and fail closed
# with the stable service error emitted by the caller.
if lock_fd is not None:
try:
os.close(lock_fd)
except OSError:
pass
lock_fd = None
raise _ProvisionLockUnavailable from None
yield
finally:
if lock_fd is not None:
if locked:
try:
if os.name == "nt":
import msvcrt
os.lseek(lock_fd, 0, os.SEEK_SET)
msvcrt.locking(lock_fd, msvcrt.LK_UNLCK, 1)
else:
import fcntl
fcntl.flock(lock_fd, fcntl.LOCK_UN)
except OSError:
# Closing the descriptor also releases its OS lock.
pass
try:
os.close(lock_fd)
except OSError:
pass
if thread_locked:
_SECRET_LOCK.release()
def _new_client_secret() -> str:
return secrets.token_urlsafe(48)
def _result(
*,
status: str,
message: str,
success: bool = False,
secret_created: bool = False,
application_id: str | None = None,
provider_id: str | None = None,
) -> dict[str, Any]:
return ForgeOidcProvisionResultV1(
success=success,
status=status,
message=message,
secret_created=secret_created,
application_id=application_id,
provider_id=provider_id,
).model_dump(mode="json", by_alias=True)
def _exact_policy_allowed(session: Session) -> bool:
rows = session.exec(
select(Policy).where(
(Policy.action == FORGE_OIDC_ACTION)
& (Policy.target == FORGE_OIDC_TARGET)
)
).all()
if len(rows) != 1:
return False
policy = rows[0]
if policy.manual != "allowed" or policy.voice != "blocked" or policy.risk != "critical":
return False
decision = evaluate_policy(
session,
action=FORGE_OIDC_ACTION,
target=FORGE_OIDC_TARGET,
source="manual",
)
return (
decision.get("decision") == "allowed"
and decision.get("scope_decision") == "allowed"
and decision.get("policy_id") == policy.id
)
def _safe_connector_id(value: Any) -> str | None:
candidate = str(value or "")
if not candidate or len(candidate) > 128:
return None
if not all(character.isalnum() or character in "._:-" for character in candidate):
return None
return candidate
def _audit(
session: Session,
*,
identity: str,
decision: str,
reason: str,
result: str,
secret_created: bool = False,
) -> None:
log_event(
session,
source="manual",
identity=identity,
action_id=FORGE_OIDC_ACTION,
target=FORGE_OIDC_TARGET,
target_type="authentik_application",
risk="critical",
policy_decision=decision,
reason=reason,
result=result,
redacted_metadata={
"clientId": FORGE_OIDC_CLIENT_ID,
"secretRef": FORGE_OIDC_SECRET_REF,
"secretCreated": secret_created,
},
)
def _blocked(
session: Session,
*,
identity: str,
message: str,
status: str = "blocked",
decision: str = "blocked",
secret_created: bool = False,
) -> dict[str, Any]:
_audit(
session,
identity=identity,
decision=decision,
reason=message,
result=status,
secret_created=secret_created,
)
return _result(status=status, message=message, secret_created=secret_created)
def _existing_secret(session: Session) -> str | None:
if not has_secret(session, FORGE_OIDC_SECRET_REF):
return None
return get_secret(session, FORGE_OIDC_SECRET_REF, touch=False)
def _persist_forge_oidc_binding(
session: Session,
*,
scope_mapping_ids: Any,
provider_id: str | None = None,
) -> bool:
"""Persist one validated mapping tuple while preserving a known provider ID."""
from .setup_service import _apply_forge_oidc_binding_to_runtime
try:
mappings = ForgeOidcScopeMappingIdsV1.model_validate(scope_mapping_ids)
except Exception: # noqa: BLE001 - invalid binding data is never serialized.
return False
binding_exists = has_secret(session, FORGE_OIDC_BINDING_SECRET_REF)
binding_valid = _apply_forge_oidc_binding_to_runtime(session)
if binding_exists and not binding_valid:
return False
current_ids = {
"openid": settings.apis.get("AUTHENTIK_FORGE_OIDC_OPENID_CLAIM_MAPPING"),
"email": settings.apis.get("AUTHENTIK_FORGE_OIDC_EMAIL_CLAIM_MAPPING"),
"profile": settings.apis.get("AUTHENTIK_FORGE_OIDC_PROFILE_CLAIM_MAPPING"),
}
new_ids = {key: str(getattr(mappings, key)) for key in ("openid", "email", "profile")}
if binding_valid and current_ids != new_ids:
return False
current_provider_id = settings.apis.get("AUTHENTIK_FORGE_OIDC_PROVIDER_ID") if binding_valid else None
if current_provider_id is not None and provider_id is not None and current_provider_id != provider_id:
return False
selected_provider_id = provider_id if provider_id is not None else current_provider_id
try:
binding = ForgeOidcBindingV1(
version=1,
providerId=selected_provider_id,
scopeMappingIds=new_ids,
)
set_secret(
session,
FORGE_OIDC_BINDING_SECRET_REF,
json.dumps(binding.model_dump(mode="json", by_alias=True), separators=(",", ":"), sort_keys=True),
used_by=["authentik:itworx-forge"],
metadata={"managedBy": "appops-forge-oidc-binding/v1"},
)
except Exception: # noqa: BLE001 - storage and serialization errors stay private.
session.rollback()
return False
return _apply_forge_oidc_binding_to_runtime(session)
def _connector_outcome(
session: Session,
*,
identity: str,
outcome: Any,
secret_created: bool,
) -> dict[str, Any]:
if not isinstance(outcome, dict):
status, message = "failed", "forge_oidc_connector_result_invalid"
success = False
else:
raw_status = outcome.get("status")
mappings = {
"created": ("created", "forge_oidc_created", True),
"already_configured": ("already_configured", "forge_oidc_already_configured", True),
"blocked": ("blocked", "forge_oidc_connector_blocked", False),
"conflict": ("conflict", "forge_oidc_resource_conflict", False),
"failed": ("failed", "forge_oidc_provisioning_failed", False),
"partial_failure": ("partial_failure", "forge_oidc_partial_failure", False),
}
status, message, success = mappings.get(
raw_status,
("failed", "forge_oidc_connector_result_invalid", False),
)
if outcome.get("success") is not True:
success = False
elif status not in {"created", "already_configured"}:
status, message, success = "failed", "forge_oidc_connector_result_invalid", False
application_id = _safe_connector_id(outcome.get("applicationId")) if isinstance(outcome, dict) else None
provider_id = _safe_connector_id(outcome.get("providerId")) if isinstance(outcome, dict) else None
audit_result = "success" if success else status
_audit(
session,
identity=identity,
decision="allowed",
reason=message,
result=audit_result,
secret_created=secret_created,
)
return _result(
status=status,
message=message,
success=success,
secret_created=secret_created,
application_id=application_id,
provider_id=provider_id,
)
def provision_forge_oidc(
session: Session,
*,
actor_identity: str,
authentik: Any | None = None,
) -> dict[str, Any]:
"""Provision only the fixed Forge OIDC client after exact-target governance."""
identity = actor_identity.strip() if isinstance(actor_identity, str) else ""
if not identity:
return _blocked(
session,
identity="system",
message="forge_oidc_operator_identity_required",
)
try:
with _forge_oidc_process_lock():
# Re-evaluate governance after entering the runtime lock so concurrent
# Forge provisioning attempts cannot interleave preflight and writes.
if not _exact_policy_allowed(session):
return _blocked(
session,
identity=identity,
message="forge_oidc_exact_target_policy_required",
)
if secret_storage_status().get("productionReady") is not True:
return _blocked(
session,
identity=identity,
message="forge_oidc_secret_store_not_production_ready",
)
return _provision_forge_oidc_locked(
session,
identity=identity,
authentik=authentik,
)
except _ProvisionLockUnavailable:
return _blocked(
session,
identity=identity,
message="forge_oidc_provision_lock_unavailable",
status="failed",
)
def _provision_forge_oidc_locked(
session: Session,
*,
identity: str,
authentik: Any | None,
) -> dict[str, Any]:
if authentik is None:
try:
from .setup_service import apply_stored_connector_settings
apply_stored_connector_settings(session, {"authentik"})
except Exception: # noqa: BLE001 - never expose connector or stored-secret errors.
return _blocked(
session,
identity=identity,
message="forge_oidc_connector_configuration_unavailable",
status="failed",
decision="allowed",
)
connector = authentik or AuthentikConnector()
# A present but unreadable or tampered binding must never be repaired from
# live discovery: its IDs are protected state, not caller-selected inputs.
from .setup_service import _apply_forge_oidc_binding_to_runtime
binding_exists = has_secret(session, FORGE_OIDC_BINDING_SECRET_REF)
binding_valid = _apply_forge_oidc_binding_to_runtime(session)
if binding_exists and not binding_valid:
return _blocked(
session,
identity=identity,
message="forge_oidc_binding_invalid",
status="conflict",
decision="allowed",
)
secret_created = False
client_secret = _existing_secret(session)
if client_secret is None and has_secret(session, FORGE_OIDC_SECRET_REF):
return _blocked(
session,
identity=identity,
message="forge_oidc_existing_client_secret_unavailable",
status="conflict",
decision="allowed",
)
if settings.apis.get("AUTHENTIK_FORGE_OIDC_PROVIDER_ID") is not None and client_secret is None:
return _blocked(
session,
identity=identity,
message="forge_oidc_existing_client_secret_missing",
status="conflict",
decision="allowed",
)
try:
prerequisites = connector.ensure_forge_oidc_prerequisites(
expected_client_secret=client_secret,
)
except Exception: # noqa: BLE001 - remote connector errors are returned only as a safe status.
prerequisites = {"success": False, "status": "failed"}
if not isinstance(prerequisites, dict) or prerequisites.get("success") is not True:
return _connector_outcome(
session,
identity=identity,
outcome=prerequisites,
secret_created=False,
)
mapping_ids = prerequisites.get("scopeMappingIds")
try:
validated_mapping_ids = ForgeOidcScopeMappingIdsV1.model_validate(mapping_ids)
except Exception: # noqa: BLE001 - connector output is treated as untrusted.
return _connector_outcome(
session,
identity=identity,
outcome={"success": False, "status": "conflict", "message": "forge_oidc_scope_mapping_id_invalid"},
secret_created=False,
)
serialized_mapping_ids = {
key: str(getattr(validated_mapping_ids, key))
for key in ("openid", "email", "profile")
}
if not _persist_forge_oidc_binding(session, scope_mapping_ids=serialized_mapping_ids):
return _connector_outcome(
session,
identity=identity,
outcome={
"success": False,
"status": "partial_failure",
"message": "forge_oidc_binding_persist_failed",
},
secret_created=False,
)
try:
preflight = connector.inspect_forge_oidc_application(
expected_client_secret=client_secret,
)
except Exception: # noqa: BLE001 - remote connector errors are returned only as a safe status.
preflight = {"success": False, "status": "failed"}
if not isinstance(preflight, dict) or preflight.get("success") is not True:
return _connector_outcome(
session,
identity=identity,
outcome=preflight,
secret_created=False,
)
if preflight.get("status") not in {"ready", "provider_orphan", "already_configured"}:
return _connector_outcome(
session,
identity=identity,
outcome={"success": False, "status": "failed"},
secret_created=False,
)
if preflight.get("status") != "ready" and client_secret is None:
return _blocked(
session,
identity=identity,
message="forge_oidc_existing_client_secret_missing",
status="conflict",
decision="allowed",
)
if client_secret is None:
client_secret = _new_client_secret()
if not 32 <= len(client_secret) <= 255 or not client_secret.isascii() or not client_secret.isprintable():
return _blocked(
session,
identity=identity,
message="forge_oidc_client_secret_generation_failed",
status="failed",
decision="allowed",
)
try:
set_secret(
session,
FORGE_OIDC_SECRET_REF,
client_secret,
used_by=["authentik:itworx-forge"],
metadata={"managedBy": "appops-forge-oidc/v1"},
)
except Exception: # noqa: BLE001 - secret-store errors are intentionally sanitized.
session.rollback()
return _blocked(
session,
identity=identity,
message="forge_oidc_client_secret_store_failed",
status="failed",
decision="allowed",
)
secret_created = True
elif (
not isinstance(client_secret, str)
or not 32 <= len(client_secret) <= 255
or not client_secret.isascii()
or not client_secret.isprintable()
):
return _blocked(
session,
identity=identity,
message="forge_oidc_existing_client_secret_unavailable",
status="conflict",
decision="allowed",
)
try:
outcome = connector.ensure_forge_oidc_application(
client_secret=client_secret,
persist_provider_id=lambda provider_id: _persist_forge_oidc_binding(
session,
scope_mapping_ids=serialized_mapping_ids,
provider_id=provider_id,
),
)
except Exception: # noqa: BLE001 - no connector response/body crosses the route boundary.
outcome = {"success": False, "status": "failed"}
return _connector_outcome(
session,
identity=identity,
outcome=outcome,
secret_created=secret_created,
)
@@ -0,0 +1,599 @@
from __future__ import annotations
import hashlib
import hmac
import json
import re
from typing import Any
from uuid import RFC_4122, UUID
from sqlmodel import Session, select
from ..connectors.authentik import AuthentikConnector
from ..core.config import settings
from ..db.models import AppSetting, Policy
from ..schemas.enrollment import MANAGED_DOCKER_APPDATA_ROOT
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
from ..schemas.forge_oidc import FORGE_OIDC_ISSUER, FORGE_OIDC_LAUNCH_URL
from ..schemas.forge_secret_delivery import (
ForgeSecretDeliveryControllerArtifactV1,
ForgeSecretDeliveryResultV1,
)
from .audit_logger import log_event
from .deployment_broker import DeploymentBrokerClient
from .enrollment_plane import EnrollmentPlane
from .forge_oidc_provisioning import _forge_oidc_process_lock
from .policy_engine import evaluate_policy
from .secret_store import get_secret, has_secret, secret_storage_status
_ACTION = "forge.secrets.prepare"
_TARGET = "itworx-forge"
_BROKER_SECRET_REF = "FORGE_DEPLOYMENT_BROKER"
_FORGE_OIDC_BINDING_REF = "AUTHENTIK_FORGE_OIDC_BINDING"
_FORGE_OIDC_CLIENT_SECRET_REF = "ITWORX_FORGE_OIDC_CLIENT_SECRET"
_FORGE_READER_BINDING_REF = "AUTHENTIK_FORGE_READER_BINDING"
_FORGE_READER_TOKEN_REF = "ITWORX_FORGE_AUTHENTIK_READ_TOKEN"
_FLOW = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\Z")
_PROVIDER_ID = re.compile(r"[1-9][0-9]{0,9}\Z")
_UUID = re.compile(
r"[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\Z"
)
_IDEMPOTENCY = re.compile(r"[A-Za-z0-9._:-]{8,128}\Z")
_CONFIG_KEYS = {
"schema",
"issuer",
"audience",
"client_id",
"authentik_api_url",
"launch_url",
"authorization_flow",
"invalidation_flow",
"authentik_openid_mapping_id",
"authentik_email_mapping_id",
"authentik_profile_mapping_id",
"authentik_provider_id",
"authentik_signing_key_id",
"private_package_owner",
}
_RESPONSE_KEYS = {
"apiVersion",
"target",
"status",
"profile",
"controllerArtifact",
"secretFilesReady",
"masterKeyBackupVerified",
"applyPreflight",
}
_ARTIFACT_KEYS = {
"controllerVersion",
"sourceRepository",
"sourceRevision",
"sourceTree",
"artifactSha256",
}
def _exact_secret_prepare_policy_allowed(session: Session) -> str | None:
rows = session.exec(select(Policy).where(Policy.action == _ACTION)).all()
if len(rows) != 1:
return None
policy = rows[0]
if (
policy.target != _TARGET
or policy.manual != "allowed"
or policy.voice != "blocked"
or policy.risk != "critical"
):
return None
decision = evaluate_policy(
session,
action=_ACTION,
target=_TARGET,
source="manual",
)
if (
decision.get("decision") != "allowed"
or decision.get("scope_decision") != "allowed"
or decision.get("policy_id") != policy.id
):
return None
return policy.id
def _secret_store_ready() -> bool:
try:
status = secret_storage_status()
except Exception: # noqa: BLE001 - store diagnostics never enter delivery errors.
return False
return isinstance(status, dict) and status.get("productionReady") is True
def _canonical_uuid(value: Any) -> bool:
if not isinstance(value, str) or _UUID.fullmatch(value) is None:
return False
try:
parsed = UUID(value)
except (TypeError, ValueError, AttributeError):
return False
return str(parsed) == value and parsed.variant == RFC_4122 and parsed.version in range(1, 9)
def _protected_config(context: dict[str, Any]) -> bytes | None:
if not isinstance(context, dict) or set(context) != {
"applicationId", "providerId", "scopeMappingIds", "groupIds", "signingKeyId"
}:
return None
provider_id = context.get("providerId")
signing_key_id = context.get("signingKeyId")
mappings = context.get("scopeMappingIds")
if (
_PROVIDER_ID.fullmatch(provider_id or "") is None
or not _canonical_uuid(signing_key_id)
or not isinstance(mappings, dict)
or set(mappings) != {"openid", "email", "profile"}
or any(not _canonical_uuid(item) for item in mappings.values())
or len(set(mappings.values())) != 3
):
return None
if not _canonical_uuid(context.get("applicationId")):
return None
owner = _protected_forge_private_package_owner()
if owner is None:
return None
authorization_flow = settings.apis.get("AUTHENTIK_OIDC_AUTHORIZATION_FLOW")
invalidation_flow = settings.apis.get("AUTHENTIK_OIDC_INVALIDATION_FLOW")
if (
not isinstance(authorization_flow, str)
or _FLOW.fullmatch(authorization_flow) is None
or not isinstance(invalidation_flow, str)
or _FLOW.fullmatch(invalidation_flow) is None
):
return None
values = {
"schema": "itworx.forge-first-install-config/v4",
"issuer": FORGE_OIDC_ISSUER,
"audience": "itworx-forge",
"client_id": "itworx-forge",
"authentik_api_url": "https://auth.example-host.com",
"launch_url": FORGE_OIDC_LAUNCH_URL,
"authorization_flow": authorization_flow,
"invalidation_flow": invalidation_flow,
"authentik_openid_mapping_id": mappings["openid"],
"authentik_email_mapping_id": mappings["email"],
"authentik_profile_mapping_id": mappings["profile"],
"authentik_provider_id": provider_id,
"authentik_signing_key_id": signing_key_id,
"private_package_owner": owner,
}
if set(values) != _CONFIG_KEYS or any(
not isinstance(value, str) or not value or len(value) > 512
for value in values.values()
):
return None
encoded = json.dumps(values, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
return encoded if len(encoded) <= 8192 else None
def _checked_controller_response(
value: object,
*,
profile: ForgeBootstrapPreflightV1,
) -> ForgeSecretDeliveryControllerArtifactV1 | None:
if not isinstance(value, dict) or set(value) != _RESPONSE_KEYS:
return None
if (
value.get("apiVersion") != "itworx.forge-secret-delivery/v1"
or value.get("target") != _TARGET
or value.get("status") != "bundle_ready"
or value.get("profile") != profile.model_dump(mode="json")
or type(value.get("secretFilesReady")) is not int
or value.get("secretFilesReady") != 7
or value.get("masterKeyBackupVerified") is not True
or value.get("applyPreflight") != "not_evaluated"
):
return None
artifact = value.get("controllerArtifact")
if not isinstance(artifact, dict) or set(artifact) != _ARTIFACT_KEYS:
return None
try:
return ForgeSecretDeliveryControllerArtifactV1.model_validate(artifact)
except Exception: # noqa: BLE001 - controller response data is untrusted.
return None
def _result(
*,
profile: ForgeBootstrapPreflightV1,
status: str,
message: str,
artifact: ForgeSecretDeliveryControllerArtifactV1 | None = None,
) -> dict[str, Any]:
ready = status == "bundle_ready"
payload = ForgeSecretDeliveryResultV1(
success=ready,
status=status,
message=message,
target=_TARGET,
revision=profile.revision,
controllerArtifact=artifact,
secretFilesReady=7 if ready else None,
masterKeyBackupVerified=True if ready else None,
applyPreflight="not_evaluated" if ready else None,
)
return payload.model_dump(mode="json", by_alias=True, exclude_none=True)
def _audit(
session: Session,
*,
identity: str,
decision: str,
result: dict[str, Any],
policy_id: str | None,
idempotency_key: str,
) -> bool:
try:
artifact = result.get("controllerArtifact")
log_event(
session,
source="manual",
identity=identity,
action_id=_ACTION,
target=_TARGET,
target_type="forge_secret_delivery",
risk="critical",
policy_decision=decision,
reason=result["message"],
result=result["status"],
after_state={
"status": result["status"],
"revision": result["revision"],
"secretFilesReady": result.get("secretFilesReady"),
"masterKeyBackupVerified": result.get("masterKeyBackupVerified"),
"applyPreflight": result.get("applyPreflight"),
"controllerArtifact": artifact,
},
redacted_metadata={
"policyId": policy_id,
"revision": result["revision"],
"idempotencyKeySha256": hashlib.sha256(idempotency_key.encode("ascii")).hexdigest(),
"secretRefs": [
_BROKER_SECRET_REF,
_FORGE_OIDC_CLIENT_SECRET_REF,
_FORGE_READER_TOKEN_REF,
],
},
)
except Exception: # noqa: BLE001 - an audit exception may carry held secrets.
try:
session.rollback()
except Exception: # noqa: BLE001 - keep DB error detail out of the response.
pass
return False
return True
def _protected_forge_private_package_owner() -> str | None:
from .forge_bootstrap_apply import (
_protected_forge_private_package_owner as protected_owner,
)
return protected_owner()
def _fixed_authentik_connector(session: Session) -> AuthentikConnector | None:
"""Use only already-loaded, fixed Authentik runtime configuration.
The generic setup loader also migrates legacy plaintext connector secrets.
Secret delivery must not trigger that write as a side effect, so this path
verifies the existing runtime against protected/read-only inputs instead.
"""
try:
legacy_secret = session.get(AppSetting, "connector:authentik:secret")
if legacy_secret is not None:
if legacy_secret.is_secret is not True or not isinstance(legacy_secret.value_json, dict):
return None
legacy_value = legacy_secret.value_json.get("value")
if isinstance(legacy_value, str) and legacy_value.strip():
return None
if legacy_secret.value_json not in (
{},
{"secretRef": "AUTHENTIK_TOKEN", "hasSecret": True},
{"secretRef": "AUTHENTIK_TOKEN", "hasSecret": True, "migrated": True},
):
return None
connector_config = session.get(AppSetting, "connector:authentik:config")
if connector_config is not None:
if connector_config.is_secret is not False or not isinstance(connector_config.value_json, dict):
return None
if not set(connector_config.value_json).issubset(
{"url", "verifyTls", "outpostUrl", "expectEmbeddedOutpost"}
):
return None
connector = AuthentikConnector()
config = connector._config()
if (
config is None
or config.url.rstrip("/") != "https://auth.example-host.com"
or config.verify_tls is not True
or not isinstance(config.token, str)
or not config.token
):
return None
stored_admin_token = get_secret(session, "AUTHENTIK_TOKEN", touch=False)
if not isinstance(stored_admin_token, str) or not stored_admin_token:
return None
normalized = stored_admin_token.strip()
if normalized.lower().startswith("bearer "):
normalized = normalized[7:].strip()
if not hmac.compare_digest(normalized, config.token):
return None
if connector_config is not None:
stored_config = connector_config.value_json
runtime_values = {
"url": config.url,
"verifyTls": config.verify_tls,
"outpostUrl": config.outpost_url,
"expectEmbeddedOutpost": config.expect_embedded_outpost,
}
if any(stored_config.get(key) != runtime_values[key] for key in stored_config):
return None
return connector
except Exception: # noqa: BLE001 - config/store failures remain a fixed denial.
return None
def prepare_forge_secret_delivery(
session: Session,
*,
profile: ForgeBootstrapPreflightV1,
idempotency_key: str,
actor_identity: str,
) -> dict[str, Any]:
"""Prepare only the fixed Forge credential bundle through the trusted broker."""
identity = actor_identity.strip() if isinstance(actor_identity, str) else ""
request_key = idempotency_key.strip() if isinstance(idempotency_key, str) else ""
if not identity:
return _result(
profile=profile,
status="blocked",
message="forge_secret_operator_identity_required",
)
if _IDEMPOTENCY.fullmatch(request_key) is None:
outcome = _result(
profile=profile,
status="blocked",
message="forge_secret_idempotency_key_invalid",
)
_audit(
session,
identity=identity,
decision="blocked",
result=outcome,
policy_id=None,
idempotency_key="invalid-key",
)
return outcome
policy_id = _exact_secret_prepare_policy_allowed(session)
if policy_id is None:
outcome = _result(
profile=profile,
status="blocked",
message="forge_secret_exact_target_policy_required",
)
_audit(
session,
identity=identity,
decision="blocked",
result=outcome,
policy_id=None,
idempotency_key=request_key,
)
return outcome
if not _secret_store_ready():
outcome = _result(
profile=profile,
status="blocked",
message="forge_secret_store_not_production_ready",
)
audit_ok = _audit(
session,
identity=identity,
decision="allowed",
result=outcome,
policy_id=policy_id,
idempotency_key=request_key,
)
return outcome if audit_ok else _result(
profile=profile, status="failed", message="forge_secret_audit_persist_failed"
)
attempted_delivery = False
try:
with _forge_oidc_process_lock():
if _exact_secret_prepare_policy_allowed(session) != policy_id:
outcome = _result(
profile=profile,
status="blocked",
message="forge_secret_exact_target_policy_required",
)
elif not _secret_store_ready():
outcome = _result(
profile=profile,
status="blocked",
message="forge_secret_store_not_production_ready",
)
else:
trust = EnrollmentPlane.get_trust_config(session)
if (
trust is None
or trust.trusted_appdata_root != MANAGED_DOCKER_APPDATA_ROOT
or trust.managed_appdata_backup_acknowledged is not True
):
outcome = _result(
profile=profile,
status="blocked",
message="forge_secret_controller_trust_required",
)
elif not all(
has_secret(session, reference)
for reference in (
_FORGE_OIDC_BINDING_REF,
_FORGE_OIDC_CLIENT_SECRET_REF,
_FORGE_READER_BINDING_REF,
_FORGE_READER_TOKEN_REF,
)
):
outcome = _result(
profile=profile,
status="blocked",
message="forge_secret_verified_reader_required",
)
else:
from .forge_bootstrap_apply import _controller_secret
from .forge_controller_reader import (
_ReaderFailure,
_verify_forge_delivery_inputs,
)
broker_secret = _controller_secret(session)
if not isinstance(broker_secret, str) or not broker_secret:
outcome = _result(
profile=profile,
status="blocked",
message="forge_secret_controller_trust_required",
)
else:
try:
connector = _fixed_authentik_connector(session)
if connector is None:
outcome = _result(
profile=profile,
status="blocked",
message="forge_secret_authentik_configuration_required",
)
connector = None
if connector is None:
verified = None
else:
verified = _verify_forge_delivery_inputs(session, connector)
if verified is None:
pass
else:
config_bytes = _protected_config(verified.context)
if config_bytes is None:
outcome = _result(
profile=profile,
status="conflict",
message="forge_secret_protected_config_invalid",
)
elif (
verified.oidc_client_secret in config_bytes
or verified.authentik_read_token in config_bytes
):
outcome = _result(
profile=profile,
status="conflict",
message="forge_secret_config_contains_secret",
)
else:
attempted_delivery = True
broker_result = DeploymentBrokerClient().forge_secret_delivery(
profile,
config_bytes=config_bytes,
oidc_client_secret=verified.oidc_client_secret,
authentik_read_token=verified.authentik_read_token,
secret=broker_secret,
)
if (
isinstance(broker_result, dict)
and broker_result.get("status") in {"missing_config", "missing_secret"}
):
outcome = _result(
profile=profile,
status="blocked",
message="forge_secret_controller_delivery_unavailable",
)
attempted_delivery = False
elif (
isinstance(broker_result, dict)
and broker_result.get("status") == "response"
):
artifact = _checked_controller_response(
broker_result.get("response"),
profile=profile,
)
if artifact is None:
outcome = _result(
profile=profile,
status="outcome_unknown",
message="forge_secret_controller_outcome_unknown",
)
else:
outcome = _result(
profile=profile,
status="bundle_ready",
message="forge_secret_bundle_ready",
artifact=artifact,
)
else:
outcome = _result(
profile=profile,
status="outcome_unknown",
message="forge_secret_controller_outcome_unknown",
)
except _ReaderFailure as exc:
outcome = _result(
profile=profile,
status="blocked" if exc.status == "blocked" else "conflict",
message="forge_secret_reader_proof_invalid",
)
except Exception: # noqa: BLE001 - never surface connector or held input details.
outcome = _result(
profile=profile,
status=("outcome_unknown" if attempted_delivery else "failed"),
message=(
"forge_secret_controller_outcome_unknown"
if attempted_delivery
else "forge_secret_input_verification_failed"
),
)
except Exception: # noqa: BLE001 - lock and DB errors are fixed safe codes.
outcome = _result(
profile=profile,
status="outcome_unknown" if attempted_delivery else "failed",
message=(
"forge_secret_controller_outcome_unknown"
if attempted_delivery
else "forge_secret_preparation_unavailable"
),
)
audit_ok = _audit(
session,
identity=identity,
decision="allowed",
result=outcome,
policy_id=policy_id,
idempotency_key=request_key,
)
if not audit_ok:
if attempted_delivery or outcome["status"] == "bundle_ready":
return _result(
profile=profile,
status="outcome_unknown",
message="forge_secret_delivery_audit_persist_failed",
)
return _result(
profile=profile,
status="failed",
message="forge_secret_audit_persist_failed",
)
return outcome
+23
View File
@@ -164,6 +164,29 @@ def evaluate_policy(
"scope_decision": "allowed" if allowed else None,
}
if action == "forge.oidc.provision":
# This is the only manual Authentik mutation exposed by AppOps. Requiring
# exactly one row for the action prevents a wildcard or duplicate row
# from becoming an alternate authorization path.
policies = session.exec(select(Policy).where(Policy.action == action)).all()
policy = policies[0] if len(policies) == 1 else None
allowed = bool(
source == "manual"
and target == "itworx-forge"
and policy is not None
and policy.target == "itworx-forge"
and policy.manual == "allowed"
and policy.voice == "blocked"
and policy.risk == "critical"
)
return {
"decision": "allowed" if allowed else "blocked",
"reason": "Forge OIDC provisioning requires one exact-target manual policy; voice is always blocked.",
"risk": "critical",
"policy_id": policy.id if allowed and policy is not None else None,
"scope_decision": "allowed" if allowed else None,
}
if action in _DEFAULT_DECISIONS:
reason = _DEFAULT_REASONS.get(action, 'Action uses default governance policy.')
return {
+57
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import json
from typing import Any
from sqlmodel import Session, select
@@ -11,6 +12,7 @@ from ..core.redaction import redact_message
from ..core.time import utc_now
from ..core.network_security import validate_connector_url
from ..db.models import AppContainerMapping, AppSetting, AuditEvent, Connector, DiscoveryRun, Secret
from ..schemas.forge_oidc import FORGE_OIDC_BINDING_SECRET_REF, ForgeOidcBindingV1
from .audit_logger import log_event
from .secret_store import empty_secret_status, get_secret, has_secret, secret_status, secret_storage_status, set_secret
from . import safety_center_service
@@ -219,6 +221,61 @@ def _apply_connector_config_to_runtime(session: Session, connector_id: str) -> N
settings.apis[secret_env] = stored_secret
elif secret_row and secret_row.value_json.get('value'):
settings.apis[secret_env] = secret_row.value_json.get('value')
if connector_id == 'authentik':
_apply_forge_oidc_binding_to_runtime(session)
def _unique_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise ValueError('duplicate_json_key')
result[key] = value
return result
def _apply_forge_oidc_binding_to_runtime(session: Session) -> bool:
"""Load protected Forge resource IDs atomically and fail closed on bad state.
This fixed secret reference is intentionally absent from connector setup
fields and response schemas. Generic Authentik settings can only update the
Authentik URL/TLS/token inputs, never these remote resource bindings.
"""
internal_keys = (
'AUTHENTIK_FORGE_OIDC_PROVIDER_ID',
'AUTHENTIK_FORGE_OIDC_OPENID_CLAIM_MAPPING',
'AUTHENTIK_FORGE_OIDC_EMAIL_CLAIM_MAPPING',
'AUTHENTIK_FORGE_OIDC_PROFILE_CLAIM_MAPPING',
)
settings.apis.update({key: None for key in internal_keys})
settings.apis['AUTHENTIK_FORGE_OIDC_BINDING_VALID'] = False
try:
raw = get_secret(session, FORGE_OIDC_BINDING_SECRET_REF, touch=False)
if not isinstance(raw, str) or not raw:
return False
payload = json.loads(raw, object_pairs_hook=_unique_json_object)
if (
not isinstance(payload, dict)
or set(payload) != {'version', 'providerId', 'scopeMappingIds'}
or not isinstance(payload.get('scopeMappingIds'), dict)
or set(payload['scopeMappingIds']) != {'openid', 'email', 'profile'}
):
return False
binding = ForgeOidcBindingV1.model_validate(payload)
except Exception: # noqa: BLE001 - binding failures never expose encrypted-store contents.
return False
# Only publish values after the whole versioned record and distinct UUIDs
# have passed validation. Keep the stored JSON out of runtime settings.
values = {
'AUTHENTIK_FORGE_OIDC_PROVIDER_ID': binding.provider_id,
'AUTHENTIK_FORGE_OIDC_OPENID_CLAIM_MAPPING': str(binding.scope_mapping_ids.openid),
'AUTHENTIK_FORGE_OIDC_EMAIL_CLAIM_MAPPING': str(binding.scope_mapping_ids.email),
'AUTHENTIK_FORGE_OIDC_PROFILE_CLAIM_MAPPING': str(binding.scope_mapping_ids.profile),
}
settings.apis.update(values)
settings.apis['AUTHENTIK_FORGE_OIDC_BINDING_VALID'] = True
return True
def apply_stored_connector_settings(session: Session, connector_ids: set[str] | None = None) -> None: