1682 lines
66 KiB
Python
1682 lines
66 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import hmac
|
|
import json
|
|
import math
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
from urllib.parse import parse_qs, quote, urlparse
|
|
from uuid import RFC_4122, UUID, uuid4
|
|
|
|
import httpx
|
|
from cryptography import x509
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from sqlmodel import Session
|
|
|
|
from ..connectors.authentik import (
|
|
AuthentikConnector,
|
|
_forge_oidc_group_contract_matches,
|
|
_forge_oidc_scope_mapping_matches,
|
|
)
|
|
from ..core.config import settings
|
|
from ..schemas.forge_oidc import (
|
|
FORGE_OIDC_ACTION,
|
|
FORGE_OIDC_APPLICATION_NAME,
|
|
FORGE_OIDC_BINDING_SECRET_REF,
|
|
FORGE_OIDC_CLIENT_ID,
|
|
FORGE_OIDC_DISCOVERY_URL,
|
|
FORGE_OIDC_GROUPS,
|
|
FORGE_OIDC_ISSUER,
|
|
FORGE_OIDC_LAUNCH_URL,
|
|
FORGE_OIDC_MANAGED_DESCRIPTION,
|
|
FORGE_OIDC_SECRET_REF,
|
|
FORGE_OIDC_TARGET,
|
|
ForgeOidcBindingV1,
|
|
ForgeOidcScopeMappingIdsV1,
|
|
)
|
|
from .audit_logger import log_event
|
|
from .forge_oidc_provisioning import _exact_policy_allowed, _forge_oidc_process_lock
|
|
from .secret_store import get_secret, has_secret, secret_storage_status, set_secret
|
|
from .setup_service import _apply_forge_oidc_binding_to_runtime
|
|
|
|
FORGE_READER_BINDING_REF = "AUTHENTIK_FORGE_READER_BINDING"
|
|
FORGE_READER_TOKEN_REF = "ITWORX_FORGE_AUTHENTIK_READ_TOKEN"
|
|
FORGE_READER_USERNAME = "itworx-forge-controller-reader"
|
|
FORGE_READER_NAME = "ITWorx Forge controller reader v1"
|
|
FORGE_READER_PATH = "service-accounts/itworx-forge"
|
|
FORGE_READER_ROLE = "ITWorx Forge controller reader v1"
|
|
FORGE_READER_TOKEN_ID = "itworx-forge-controller-reader-v1"
|
|
FORGE_READER_TOKEN_DESCRIPTION = "Managed by AppOps Forge controller reader v1."
|
|
FORGE_READER_OWNER_ATTRIBUTE = "appops_forge_controller_reader_v1"
|
|
_AUTHENTIK_BASE = "https://auth.example-host.com"
|
|
_JOURNAL_KEYS = {
|
|
"version",
|
|
"nonce",
|
|
"resourceContext",
|
|
"permissionManifest",
|
|
"userId",
|
|
"roleId",
|
|
"tokenId",
|
|
"phase",
|
|
"intent",
|
|
"permissionIndex",
|
|
}
|
|
_RESOURCE_KEYS = {"applicationId", "providerId", "scopeMappingIds", "groupIds", "signingKeyId"}
|
|
_PHASES = {"role", "permissions", "user", "membership", "token", "ready"}
|
|
_INTENTS = {None, "role", "permission", "user", "membership", "token"}
|
|
_TOKEN_PATTERN = re.compile(r"[\x21-\x7e]{32,256}\Z")
|
|
_USER_ID_PATTERN = re.compile(r"[1-9][0-9]{0,18}\Z")
|
|
_PROVIDER_ID_PATTERN = re.compile(r"[1-9][0-9]{0,9}\Z")
|
|
_GROUP_LIST_PARAMS = {
|
|
"include_users": "false",
|
|
"include_children": "true",
|
|
"include_parents": "true",
|
|
"include_inherited_roles": "true",
|
|
"page_size": 100,
|
|
}
|
|
_MAX_RESPONSE_BYTES = 256 * 1024
|
|
_HTTP_TIMEOUT_MIN_SECONDS = 0.1
|
|
_HTTP_TIMEOUT_MAX_SECONDS = 10.0
|
|
|
|
|
|
class _ReaderFailure(RuntimeError):
|
|
def __init__(self, status: str, code: str):
|
|
super().__init__(code)
|
|
self.status = status
|
|
self.code = code
|
|
|
|
|
|
def _fail(code: str, *, status: str = "conflict") -> None:
|
|
raise _ReaderFailure(status, code)
|
|
|
|
|
|
def _unique_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 _uuid(value: Any, *, version4: bool = False) -> str | None:
|
|
if not isinstance(value, str):
|
|
return None
|
|
try:
|
|
parsed = UUID(value)
|
|
except (TypeError, ValueError, AttributeError):
|
|
return None
|
|
if (
|
|
str(parsed) != value
|
|
or parsed.variant != RFC_4122
|
|
or parsed.version not in range(1, 9)
|
|
or (version4 and parsed.version != 4)
|
|
):
|
|
return None
|
|
return value
|
|
|
|
|
|
def _positive_id(value: Any, *, provider: bool = False) -> str | None:
|
|
if type(value) is int:
|
|
candidate = str(value)
|
|
elif isinstance(value, str):
|
|
candidate = value
|
|
else:
|
|
return None
|
|
pattern = _PROVIDER_ID_PATTERN if provider else _USER_ID_PATTERN
|
|
return candidate if pattern.fullmatch(candidate) else None
|
|
|
|
|
|
def _remote_id(value: Any) -> Any:
|
|
if isinstance(value, dict):
|
|
return value.get("pk", value.get("id"))
|
|
return value
|
|
|
|
|
|
def _json_items(payload: Any) -> tuple[list[dict[str, Any]], str | None]:
|
|
if isinstance(payload, list):
|
|
if any(not isinstance(item, dict) for item in payload):
|
|
_fail("forge_reader_remote_collection_invalid", status="failed")
|
|
return (payload, None)
|
|
if not isinstance(payload, dict):
|
|
_fail("forge_reader_remote_collection_invalid", status="failed")
|
|
collection_keys = [key for key in ("results", "data", "items") if key in payload]
|
|
if len(collection_keys) != 1:
|
|
_fail("forge_reader_remote_collection_invalid", status="failed")
|
|
items = payload[collection_keys[0]]
|
|
if not isinstance(items, list) or any(not isinstance(item, dict) for item in items):
|
|
_fail("forge_reader_remote_collection_invalid", status="failed")
|
|
|
|
pagination = payload.get("pagination")
|
|
if "pagination" in payload and not isinstance(pagination, dict):
|
|
_fail("forge_reader_remote_collection_invalid", status="failed")
|
|
nested_next = pagination.get("next") if isinstance(pagination, dict) and "next" in pagination else None
|
|
direct_next = payload.get("next") if "next" in payload else None
|
|
if "next" in payload and isinstance(pagination, dict) and "next" in pagination and direct_next != nested_next:
|
|
_fail("forge_reader_remote_collection_invalid", status="failed")
|
|
next_value = direct_next if "next" in payload else nested_next
|
|
if next_value is None:
|
|
return items, None
|
|
if type(next_value) is int and next_value > 0:
|
|
next_value = str(next_value)
|
|
if not isinstance(next_value, str) or not next_value:
|
|
_fail("forge_reader_remote_collection_invalid", status="failed")
|
|
return items, next_value
|
|
|
|
|
|
def _bounded_timeout(value: Any) -> float:
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
_fail("forge_reader_timeout_invalid", status="blocked")
|
|
try:
|
|
seconds = float(value)
|
|
except (OverflowError, ValueError):
|
|
_fail("forge_reader_timeout_invalid", status="blocked")
|
|
if not math.isfinite(seconds):
|
|
_fail("forge_reader_timeout_invalid", status="blocked")
|
|
return min(max(seconds, _HTTP_TIMEOUT_MIN_SECONDS), _HTTP_TIMEOUT_MAX_SECONDS)
|
|
|
|
|
|
def _http_json(
|
|
method: str,
|
|
url: str,
|
|
*,
|
|
token: str | None,
|
|
timeout: float,
|
|
params: dict[str, Any] | None = None,
|
|
payload: dict[str, Any] | None = None,
|
|
) -> Any:
|
|
headers = {"Accept": "application/json"}
|
|
if token is not None:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
try:
|
|
with httpx.Client(timeout=_bounded_timeout(timeout), verify=True, follow_redirects=False) as client:
|
|
with client.stream(method, url, params=params, json=payload, headers=headers) as response:
|
|
if response.status_code < 200 or response.status_code >= 300:
|
|
_fail("forge_reader_remote_request_failed", status="failed")
|
|
content_length = response.headers.get("content-length")
|
|
if content_length is not None:
|
|
if not content_length.isascii() or not content_length.isdecimal() or int(content_length) > _MAX_RESPONSE_BYTES:
|
|
_fail("forge_reader_remote_response_too_large", status="failed")
|
|
chunks: list[bytes] = []
|
|
total_bytes = 0
|
|
for chunk in response.iter_bytes(chunk_size=16 * 1024):
|
|
total_bytes += len(chunk)
|
|
if total_bytes > _MAX_RESPONSE_BYTES:
|
|
_fail("forge_reader_remote_response_too_large", status="failed")
|
|
chunks.append(chunk)
|
|
response_body = b"".join(chunks)
|
|
chunks.clear()
|
|
if not response_body:
|
|
response_body = None
|
|
return {}
|
|
try:
|
|
value = json.loads(response_body)
|
|
except (ValueError, TypeError, UnicodeError):
|
|
response_body = None
|
|
_fail("forge_reader_remote_response_invalid", status="failed")
|
|
response_body = None
|
|
return value
|
|
except _ReaderFailure:
|
|
raise
|
|
except (httpx.RequestError, OSError):
|
|
_fail("forge_reader_remote_unavailable", status="failed")
|
|
|
|
|
|
def _admin_json(
|
|
connector: Any,
|
|
method: str,
|
|
path: str,
|
|
*,
|
|
params: dict[str, Any] | None = None,
|
|
payload: dict[str, Any] | None = None,
|
|
) -> Any:
|
|
config = connector._config()
|
|
if (
|
|
config is None
|
|
or str(getattr(config, "url", "")).rstrip("/") != _AUTHENTIK_BASE
|
|
or getattr(config, "verify_tls", None) is not True
|
|
or not isinstance(getattr(config, "token", None), str)
|
|
or not config.token
|
|
or not path.startswith("/")
|
|
or path.startswith("//")
|
|
):
|
|
_fail("forge_reader_authentik_configuration_required", status="blocked")
|
|
return _http_json(
|
|
method,
|
|
f"{_AUTHENTIK_BASE}/api/v3{path}",
|
|
token=config.token,
|
|
timeout=config.timeout_seconds,
|
|
params=params,
|
|
payload=payload,
|
|
)
|
|
|
|
|
|
def _reader_json(connector: Any, reader_token: str, path: str, *, params=None) -> Any:
|
|
config = connector._config()
|
|
if (
|
|
config is None
|
|
or str(getattr(config, "url", "")).rstrip("/") != _AUTHENTIK_BASE
|
|
or getattr(config, "verify_tls", None) is not True
|
|
or not path.startswith("/")
|
|
or path.startswith("//")
|
|
):
|
|
_fail("forge_reader_authentik_configuration_required", status="blocked")
|
|
return _http_json(
|
|
"GET",
|
|
f"{_AUTHENTIK_BASE}/api/v3{path}",
|
|
token=reader_token,
|
|
timeout=config.timeout_seconds,
|
|
params=params,
|
|
)
|
|
|
|
|
|
def _public_json(url: str) -> Any:
|
|
parsed = urlparse(url)
|
|
if parsed.scheme != "https" or parsed.netloc != urlparse(_AUTHENTIK_BASE).netloc:
|
|
_fail("forge_reader_public_metadata_url_invalid", status="conflict")
|
|
return _http_json("GET", url, token=None, timeout=10.0)
|
|
|
|
|
|
def _paged(
|
|
request,
|
|
path: str,
|
|
params: dict[str, Any] | None = None,
|
|
*,
|
|
max_pages: int = 100,
|
|
max_items: int = 10000,
|
|
) -> list[dict[str, Any]]:
|
|
base_params = dict(params or {})
|
|
if not isinstance(path, str) or not path.startswith("/") or path.startswith("//") or urlparse(path).path != path:
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
if "page" in base_params:
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
base_query = {key: [str(value)] for key, value in base_params.items()}
|
|
page_path = path
|
|
page_params: dict[str, Any] | None = dict(base_params)
|
|
items: list[dict[str, Any]] = []
|
|
current_page = 1
|
|
seen_pages = {current_page}
|
|
for _ in range(max_pages):
|
|
response = request(page_path, params=page_params)
|
|
page_items, next_value = _json_items(response)
|
|
items.extend(page_items)
|
|
if len(items) > max_items:
|
|
_fail("forge_reader_remote_collection_limit", status="failed")
|
|
if not next_value:
|
|
return items
|
|
if re.fullmatch(r"[1-9][0-9]*", next_value):
|
|
next_page = int(next_value)
|
|
else:
|
|
if "#" in next_value:
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
parsed = urlparse(next_value)
|
|
if parsed.username is not None or parsed.password is not None or parsed.fragment:
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
if parsed.scheme or parsed.netloc:
|
|
expected_host = urlparse(_AUTHENTIK_BASE)
|
|
if parsed.scheme != "https" or parsed.netloc != expected_host.netloc:
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
elif next_value.startswith("//"):
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
expected_paths = {path, f"/api/v3{path}"}
|
|
if parsed.path not in expected_paths:
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
try:
|
|
query_params = parse_qs(parsed.query, keep_blank_values=True, strict_parsing=True)
|
|
except ValueError:
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
if set(query_params) != set(base_query) | {"page"} or any(
|
|
query_params.get(key) != values for key, values in base_query.items()
|
|
):
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
page_values = query_params.get("page", [])
|
|
if len(page_values) != 1 or not re.fullmatch(r"[1-9][0-9]*", page_values[0]):
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
next_page = int(page_values[0])
|
|
if next_page < 1 or next_page > max_pages or next_page <= current_page or next_page in seen_pages:
|
|
_fail("forge_reader_remote_pagination_invalid", status="failed")
|
|
current_page = next_page
|
|
seen_pages.add(current_page)
|
|
page_path = path
|
|
page_params = {**base_params, "page": current_page}
|
|
_fail("forge_reader_remote_pagination_incomplete", status="failed")
|
|
|
|
|
|
def _manifest(context: dict[str, Any]) -> list[dict[str, str]]:
|
|
app_id = context["applicationId"]
|
|
provider_id = context["providerId"]
|
|
mappings = context["scopeMappingIds"]
|
|
groups = context["groupIds"]
|
|
key_id = context["signingKeyId"]
|
|
rows = [
|
|
{"app": "authentik_core", "model": "application", "codename": "view_application", "objectPk": app_id},
|
|
{"app": "authentik_providers_oauth2", "model": "oauth2provider", "codename": "view_oauth2provider", "objectPk": provider_id},
|
|
*[
|
|
{
|
|
"app": "authentik_providers_oauth2",
|
|
"model": "scopemapping",
|
|
"codename": "view_scopemapping",
|
|
"objectPk": mappings[scope],
|
|
}
|
|
for scope in ("openid", "email", "profile")
|
|
],
|
|
*[
|
|
{
|
|
"app": "authentik_core",
|
|
"model": "group",
|
|
"codename": "view_group",
|
|
"objectPk": groups[name],
|
|
}
|
|
for name in FORGE_OIDC_GROUPS
|
|
],
|
|
{"app": "authentik_crypto", "model": "certificatekeypair", "codename": "view_certificatekeypair", "objectPk": key_id},
|
|
{
|
|
"app": "authentik_crypto",
|
|
"model": "certificatekeypair",
|
|
"codename": "view_certificatekeypair_certificate",
|
|
"objectPk": key_id,
|
|
},
|
|
]
|
|
return sorted(rows, key=lambda row: (row["app"], row["model"], row["codename"], row["objectPk"]))
|
|
|
|
|
|
def _journal_bytes(journal: dict[str, Any]) -> str:
|
|
return json.dumps(journal, ensure_ascii=True, separators=(",", ":"), sort_keys=True)
|
|
|
|
|
|
def _valid_journal(value: Any) -> bool:
|
|
if not isinstance(value, dict) or set(value) != _JOURNAL_KEYS:
|
|
return False
|
|
if type(value.get("version")) is not int or value["version"] != 1:
|
|
return False
|
|
if _uuid(value.get("nonce"), version4=True) is None:
|
|
return False
|
|
context = value.get("resourceContext")
|
|
if not isinstance(context, dict) or set(context) != _RESOURCE_KEYS:
|
|
return False
|
|
if (
|
|
_uuid(context.get("applicationId")) is None
|
|
or not isinstance(context.get("providerId"), str)
|
|
or _positive_id(context.get("providerId"), provider=True) is None
|
|
or _uuid(context.get("signingKeyId")) is None
|
|
):
|
|
return False
|
|
try:
|
|
mapping_ids = ForgeOidcScopeMappingIdsV1.model_validate(context.get("scopeMappingIds"))
|
|
except Exception: # noqa: BLE001 - journal is untrusted encrypted state.
|
|
return False
|
|
if not isinstance(context.get("groupIds"), dict) or set(context["groupIds"]) != set(FORGE_OIDC_GROUPS):
|
|
return False
|
|
if any(_uuid(context["groupIds"][name]) is None for name in FORGE_OIDC_GROUPS):
|
|
return False
|
|
if len({str(mapping_ids.openid), str(mapping_ids.email), str(mapping_ids.profile)}) != 3:
|
|
return False
|
|
if len(set(context["groupIds"].values())) != 3:
|
|
return False
|
|
if not isinstance(value.get("permissionManifest"), list) or len(value["permissionManifest"]) != 10:
|
|
return False
|
|
for row in value["permissionManifest"]:
|
|
if (
|
|
not isinstance(row, dict)
|
|
or set(row) != {"app", "model", "codename", "objectPk"}
|
|
or any(not isinstance(row[key], str) or not row[key] for key in row)
|
|
):
|
|
return False
|
|
user_id = value.get("userId")
|
|
if user_id is not None and (not isinstance(user_id, str) or _positive_id(user_id) is None):
|
|
return False
|
|
for key in ("roleId", "tokenId"):
|
|
if value.get(key) is not None and _uuid(value[key]) is None:
|
|
return False
|
|
if not isinstance(value.get("phase"), str) or value["phase"] not in _PHASES:
|
|
return False
|
|
if value.get("intent") is not None and (
|
|
not isinstance(value.get("intent"), str) or value["intent"] not in _INTENTS
|
|
):
|
|
return False
|
|
index = value.get("permissionIndex")
|
|
if index is not None and (type(index) is not int or not 0 <= index < 10):
|
|
return False
|
|
if (value.get("intent") == "permission") != (index is not None):
|
|
return False
|
|
phase = value["phase"]
|
|
intent = value["intent"]
|
|
role_id = value["roleId"]
|
|
token_id = value["tokenId"]
|
|
if phase == "role":
|
|
return (
|
|
intent == "role"
|
|
and role_id is None
|
|
and user_id is None
|
|
and token_id is None
|
|
and index is None
|
|
)
|
|
if phase == "permissions":
|
|
return (
|
|
intent in {None, "permission"}
|
|
and role_id is not None
|
|
and user_id is None
|
|
and token_id is None
|
|
)
|
|
if phase == "user":
|
|
return (
|
|
intent == "user"
|
|
and role_id is not None
|
|
and token_id is None
|
|
and index is None
|
|
)
|
|
if phase == "membership":
|
|
return (
|
|
intent == "membership"
|
|
and role_id is not None
|
|
and user_id is not None
|
|
and token_id is None
|
|
and index is None
|
|
)
|
|
if phase == "token":
|
|
return (
|
|
intent == "token"
|
|
and role_id is not None
|
|
and user_id is not None
|
|
and index is None
|
|
)
|
|
return (
|
|
phase == "ready"
|
|
and intent is None
|
|
and role_id is not None
|
|
and user_id is not None
|
|
and token_id is not None
|
|
and index is None
|
|
)
|
|
|
|
|
|
def _persist_journal(session: Session, journal: dict[str, Any]) -> bool:
|
|
if not _valid_journal(journal):
|
|
return False
|
|
raw = _journal_bytes(journal)
|
|
try:
|
|
set_secret(
|
|
session,
|
|
FORGE_READER_BINDING_REF,
|
|
raw,
|
|
used_by=["authentik:itworx-forge-controller-reader"],
|
|
metadata={"managedBy": "appops-forge-controller-reader/v1"},
|
|
)
|
|
stored = get_secret(session, FORGE_READER_BINDING_REF, touch=False)
|
|
except Exception: # noqa: BLE001 - never expose database or encrypted payload errors.
|
|
session.rollback()
|
|
return False
|
|
return isinstance(stored, str) and hmac.compare_digest(stored, raw)
|
|
|
|
|
|
def _load_or_create_journal(
|
|
session: Session,
|
|
context: dict[str, Any],
|
|
manifest: list[dict[str, str]],
|
|
) -> dict[str, Any]:
|
|
if has_secret(session, FORGE_READER_BINDING_REF):
|
|
raw = get_secret(session, FORGE_READER_BINDING_REF, touch=False)
|
|
if not isinstance(raw, str) or not raw:
|
|
_fail("forge_reader_binding_unreadable")
|
|
try:
|
|
journal = json.loads(raw, object_pairs_hook=_unique_object)
|
|
except (ValueError, TypeError):
|
|
_fail("forge_reader_binding_invalid")
|
|
if not _valid_journal(journal):
|
|
_fail("forge_reader_binding_invalid")
|
|
if not hmac.compare_digest(raw, _journal_bytes(journal)):
|
|
_fail("forge_reader_binding_not_canonical")
|
|
if journal["resourceContext"] != context or journal["permissionManifest"] != manifest:
|
|
_fail("forge_reader_binding_resource_mismatch")
|
|
return journal
|
|
if has_secret(session, FORGE_READER_TOKEN_REF):
|
|
_fail("forge_reader_token_without_binding")
|
|
journal = {
|
|
"version": 1,
|
|
"nonce": str(uuid4()),
|
|
"resourceContext": context,
|
|
"permissionManifest": manifest,
|
|
"userId": None,
|
|
"roleId": None,
|
|
"tokenId": None,
|
|
"phase": "role",
|
|
"intent": "role",
|
|
"permissionIndex": None,
|
|
}
|
|
if not _persist_journal(session, journal):
|
|
_fail("forge_reader_binding_persist_failed", status="partial_failure")
|
|
return journal
|
|
|
|
|
|
def _persist_token(session: Session, token: str) -> bool:
|
|
try:
|
|
set_secret(
|
|
session,
|
|
FORGE_READER_TOKEN_REF,
|
|
token,
|
|
used_by=["authentik:itworx-forge-controller-reader"],
|
|
metadata={"managedBy": "appops-forge-controller-reader/v1"},
|
|
)
|
|
stored = get_secret(session, FORGE_READER_TOKEN_REF, touch=False)
|
|
except Exception: # noqa: BLE001 - token material stays out of every error channel.
|
|
session.rollback()
|
|
return False
|
|
return isinstance(stored, str) and hmac.compare_digest(stored, token)
|
|
|
|
|
|
def _reader_secret(session: Session) -> str | None:
|
|
if not has_secret(session, FORGE_READER_TOKEN_REF):
|
|
return None
|
|
token = get_secret(session, FORGE_READER_TOKEN_REF, touch=False)
|
|
if not isinstance(token, str) or not _TOKEN_PATTERN.fullmatch(token):
|
|
_fail("forge_reader_token_unreadable")
|
|
return token
|
|
|
|
|
|
def _oidc_resource_context(session: Session, connector: Any) -> tuple[dict[str, Any], str]:
|
|
if not has_secret(session, FORGE_OIDC_BINDING_SECRET_REF):
|
|
_fail("forge_reader_oidc_binding_required", status="blocked")
|
|
if not _apply_forge_oidc_binding_to_runtime(session):
|
|
_fail("forge_reader_oidc_binding_invalid")
|
|
raw_binding = get_secret(session, FORGE_OIDC_BINDING_SECRET_REF, touch=False)
|
|
try:
|
|
decoded_binding = json.loads(raw_binding, object_pairs_hook=_unique_object)
|
|
binding = ForgeOidcBindingV1.model_validate(decoded_binding)
|
|
except Exception: # noqa: BLE001 - stored binding is untrusted.
|
|
_fail("forge_reader_oidc_binding_invalid")
|
|
provider_id = binding.provider_id
|
|
if not isinstance(provider_id, str) or _positive_id(provider_id, provider=True) is None:
|
|
_fail("forge_reader_oidc_provider_binding_required", status="blocked")
|
|
if settings.apis.get("AUTHENTIK_FORGE_OIDC_PROVIDER_ID") != provider_id:
|
|
_fail("forge_reader_oidc_provider_binding_mismatch")
|
|
oidc_secret = get_secret(session, FORGE_OIDC_SECRET_REF, touch=False)
|
|
if not isinstance(oidc_secret, str) or not 32 <= len(oidc_secret) <= 255 or not oidc_secret.isascii() or not oidc_secret.isprintable():
|
|
_fail("forge_reader_oidc_client_secret_unavailable", status="conflict")
|
|
config = connector._config()
|
|
if (
|
|
config is None
|
|
or str(getattr(config, "url", "")).rstrip("/") != _AUTHENTIK_BASE
|
|
or getattr(config, "verify_tls", None) is not True
|
|
):
|
|
_fail("forge_reader_authentik_configuration_required", status="blocked")
|
|
|
|
maps = {
|
|
"openid": str(binding.scope_mapping_ids.openid),
|
|
"email": str(binding.scope_mapping_ids.email),
|
|
"profile": str(binding.scope_mapping_ids.profile),
|
|
}
|
|
key_id = str(settings.apis.get("AUTHENTIK_FORGE_OIDC_SIGNING_KEY_ID") or "")
|
|
if _uuid(key_id) is None:
|
|
_fail("forge_reader_signing_key_binding_invalid", status="blocked")
|
|
|
|
applications = _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/core/applications/",
|
|
{"page_size": 100},
|
|
)
|
|
app_matches = [
|
|
item
|
|
for item in applications
|
|
if item.get("slug") == FORGE_OIDC_CLIENT_ID or item.get("name") == FORGE_OIDC_APPLICATION_NAME
|
|
]
|
|
if len(app_matches) != 1:
|
|
_fail("forge_reader_oidc_application_missing_or_ambiguous")
|
|
application_id = _uuid(_remote_id(app_matches[0]))
|
|
if application_id is None:
|
|
_fail("forge_reader_oidc_application_id_invalid")
|
|
application = _admin_json(connector, "GET", f"/core/applications/{FORGE_OIDC_CLIENT_ID}/")
|
|
if (
|
|
not isinstance(application, dict)
|
|
or _uuid(_remote_id(application)) != application_id
|
|
or application.get("name") != FORGE_OIDC_APPLICATION_NAME
|
|
or application.get("slug") != FORGE_OIDC_CLIENT_ID
|
|
or str(application.get("meta_launch_url") or application.get("launch_url") or "").rstrip("/") != FORGE_OIDC_LAUNCH_URL
|
|
or application.get("meta_description") != FORGE_OIDC_MANAGED_DESCRIPTION
|
|
or _positive_id(_remote_id(application.get("provider")), provider=True) != provider_id
|
|
):
|
|
_fail("forge_reader_oidc_application_contract_mismatch")
|
|
application = None
|
|
|
|
provider = _admin_json(connector, "GET", f"/providers/oauth2/{quote(provider_id, safe='')}/")
|
|
if not isinstance(provider, dict) or _positive_id(_remote_id(provider), provider=True) != provider_id:
|
|
_fail("forge_reader_oidc_provider_contract_mismatch")
|
|
if not AuthentikConnector._forge_oidc_provider_matches(provider, signing_key_id=key_id):
|
|
provider = None
|
|
_fail("forge_reader_oidc_provider_contract_mismatch")
|
|
actual_client_secret = provider.get("client_secret")
|
|
secret_matches = (
|
|
isinstance(actual_client_secret, str)
|
|
and actual_client_secret.isascii()
|
|
and oidc_secret.isascii()
|
|
and hmac.compare_digest(actual_client_secret, oidc_secret)
|
|
)
|
|
actual_client_secret = None
|
|
provider = None
|
|
if secret_matches is not True:
|
|
_fail("forge_reader_oidc_client_secret_mismatch")
|
|
|
|
for scope, mapping_id in maps.items():
|
|
mapping = _admin_json(
|
|
connector,
|
|
"GET",
|
|
f"/propertymappings/provider/scope/{quote(mapping_id, safe='')}/",
|
|
)
|
|
if not _forge_oidc_scope_mapping_matches(mapping, mapping_id=mapping_id, scope=scope):
|
|
_fail("forge_reader_oidc_scope_mapping_mismatch")
|
|
mapping = None
|
|
|
|
all_groups = _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/core/groups/",
|
|
_GROUP_LIST_PARAMS,
|
|
)
|
|
group_ids: dict[str, str] = {}
|
|
for name in FORGE_OIDC_GROUPS:
|
|
matches = [item for item in all_groups if item.get("name") == name]
|
|
if len(matches) != 1:
|
|
_fail("forge_reader_oidc_group_missing_or_ambiguous")
|
|
group_id = _uuid(_remote_id(matches[0]))
|
|
if group_id is None:
|
|
_fail("forge_reader_oidc_group_id_invalid")
|
|
group = _admin_json(
|
|
connector,
|
|
"GET",
|
|
f"/core/groups/{quote(group_id, safe='')}/",
|
|
params={
|
|
"include_users": "true",
|
|
"include_children": "true",
|
|
"include_parents": "true",
|
|
"include_inherited_roles": "true",
|
|
},
|
|
)
|
|
if (
|
|
not isinstance(group, dict)
|
|
or _uuid(_remote_id(group)) != group_id
|
|
or group.get("name") != name
|
|
or not _forge_oidc_group_contract_matches(group, require_users=True)
|
|
or group.get("inherited_roles_obj") != []
|
|
):
|
|
_fail("forge_reader_oidc_group_contract_mismatch")
|
|
group_ids[name] = group_id
|
|
group = None
|
|
if len(set(group_ids.values())) != 3:
|
|
_fail("forge_reader_oidc_group_id_collision")
|
|
|
|
signing_key = _admin_json(
|
|
connector,
|
|
"GET",
|
|
f"/crypto/certificatekeypairs/{quote(key_id, safe='')}/",
|
|
)
|
|
if (
|
|
not isinstance(signing_key, dict)
|
|
or _uuid(_remote_id(signing_key)) != key_id
|
|
or str(signing_key.get("key_type") or "").lower() != "rsa"
|
|
or signing_key.get("private_key_available") is not True
|
|
):
|
|
_fail("forge_reader_signing_key_contract_mismatch")
|
|
signing_key = None
|
|
|
|
context = {
|
|
"applicationId": application_id,
|
|
"providerId": provider_id,
|
|
"scopeMappingIds": maps,
|
|
"groupIds": group_ids,
|
|
"signingKeyId": key_id,
|
|
}
|
|
return context, oidc_secret
|
|
|
|
|
|
def _role_list(connector: Any, *, name: str | None = None, role_id: str | None = None) -> list[dict[str, Any]]:
|
|
if role_id is not None:
|
|
value = _admin_json(connector, "GET", f"/rbac/roles/{quote(role_id, safe='')}/")
|
|
return [value] if isinstance(value, dict) else []
|
|
params: dict[str, Any] = {"page_size": 100}
|
|
if name is not None:
|
|
params["name"] = name
|
|
return _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/rbac/roles/",
|
|
params,
|
|
)
|
|
|
|
|
|
def _role_users(connector: Any, role_id: str) -> list[dict[str, Any]]:
|
|
return _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/core/users/",
|
|
{"roles_by_pk": role_id, "page_size": 100},
|
|
)
|
|
|
|
|
|
def _role_object_permissions(connector: Any, role_id: str) -> list[dict[str, Any]]:
|
|
return _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/rbac/permissions/roles/",
|
|
{"uuid": role_id, "page_size": 100},
|
|
)
|
|
|
|
|
|
def _global_permissions(connector: Any, role_id: str) -> list[dict[str, Any]]:
|
|
return _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/rbac/permissions/",
|
|
{"role": role_id, "page_size": 100},
|
|
)
|
|
|
|
|
|
def _group_has_role(rows: list[dict[str, Any]], role_id: str) -> bool:
|
|
for group in rows:
|
|
direct = group.get("roles")
|
|
inherited = group.get("inherited_roles_obj")
|
|
if not isinstance(direct, list) or not isinstance(inherited, list):
|
|
_fail("forge_reader_group_role_readback_invalid", status="failed")
|
|
if any(str(_remote_id(item)) == role_id for item in direct):
|
|
return True
|
|
if any(str(_remote_id(item)) == role_id for item in inherited):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _role_object_permission_keys(rows: list[dict[str, Any]]) -> set[tuple[str, str, str, str]]:
|
|
keys: set[tuple[str, str, str, str]] = set()
|
|
for row in rows:
|
|
if not isinstance(row, dict):
|
|
_fail("forge_reader_role_permission_readback_invalid", status="failed")
|
|
key = (
|
|
str(row.get("app_label") or ""),
|
|
str(row.get("model") or ""),
|
|
str(row.get("codename") or ""),
|
|
str(row.get("object_pk") or ""),
|
|
)
|
|
if not all(key) or key in keys:
|
|
_fail("forge_reader_role_permission_readback_conflict")
|
|
keys.add(key)
|
|
return keys
|
|
|
|
|
|
def _manifest_keys(rows: list[dict[str, Any]]) -> set[tuple[str, str, str, str]]:
|
|
return {(row["app"], row["model"], row["codename"], row["objectPk"]) for row in rows}
|
|
|
|
|
|
def _verify_permission_catalog(connector: Any, manifest: list[dict[str, Any]]) -> None:
|
|
for row in manifest:
|
|
items = _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/rbac/permissions/",
|
|
{
|
|
"content_type__app_label": row["app"],
|
|
"content_type__model": row["model"],
|
|
"codename": row["codename"],
|
|
"page_size": 100,
|
|
},
|
|
)
|
|
if (
|
|
len(items) != 1
|
|
or items[0].get("app_label") != row["app"]
|
|
or items[0].get("model") != row["model"]
|
|
or items[0].get("codename") != row["codename"]
|
|
):
|
|
_fail("forge_reader_permission_catalog_mismatch")
|
|
|
|
|
|
def _journal_store(
|
|
session: Session,
|
|
journal: dict[str, Any],
|
|
*,
|
|
phase: str | None = None,
|
|
intent: str | None | object = ...,
|
|
permission_index: int | None | object = ...,
|
|
user_id: str | None | object = ...,
|
|
role_id: str | None | object = ...,
|
|
token_id: str | None | object = ...,
|
|
) -> None:
|
|
updated = dict(journal)
|
|
if phase is not None:
|
|
updated["phase"] = phase
|
|
if intent is not ...:
|
|
updated["intent"] = intent
|
|
if permission_index is not ...:
|
|
updated["permissionIndex"] = permission_index
|
|
if user_id is not ...:
|
|
updated["userId"] = user_id
|
|
if role_id is not ...:
|
|
updated["roleId"] = role_id
|
|
if token_id is not ...:
|
|
updated["tokenId"] = token_id
|
|
if not _persist_journal(session, updated):
|
|
_fail("forge_reader_binding_persist_failed", status="partial_failure")
|
|
journal.clear()
|
|
journal.update(updated)
|
|
|
|
|
|
def _prove_empty_role(connector: Any, role_id: str) -> None:
|
|
object_rows = _role_object_permissions(connector, role_id)
|
|
global_rows = _global_permissions(connector, role_id)
|
|
users = _role_users(connector, role_id)
|
|
groups = _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/core/groups/",
|
|
_GROUP_LIST_PARAMS,
|
|
)
|
|
if object_rows or global_rows or users or _group_has_role(groups, role_id):
|
|
_fail("forge_reader_existing_role_not_empty")
|
|
|
|
|
|
def _resolve_role(session: Session, connector: Any, journal: dict[str, Any]) -> str:
|
|
role_id = journal["roleId"]
|
|
if role_id is None:
|
|
if journal["phase"] != "role" or journal["intent"] != "role":
|
|
_fail("forge_reader_role_binding_missing")
|
|
matches = _role_list(connector, name=FORGE_READER_ROLE)
|
|
matches = [row for row in matches if row.get("name") == FORGE_READER_ROLE]
|
|
if len(matches) > 1:
|
|
_fail("forge_reader_role_ambiguous")
|
|
if matches:
|
|
role_id = _uuid(_remote_id(matches[0]))
|
|
if role_id is None:
|
|
_fail("forge_reader_role_id_invalid")
|
|
_prove_empty_role(connector, role_id)
|
|
_journal_store(session, journal, phase="permissions", intent=None, permission_index=None, role_id=role_id)
|
|
return role_id
|
|
|
|
# The protected role intent is durable before this only remote POST.
|
|
try:
|
|
_admin_json(connector, "POST", "/rbac/roles/", payload={"name": FORGE_READER_ROLE})
|
|
except _ReaderFailure:
|
|
pass
|
|
matches = _role_list(connector, name=FORGE_READER_ROLE)
|
|
matches = [row for row in matches if row.get("name") == FORGE_READER_ROLE]
|
|
if len(matches) != 1:
|
|
_fail("forge_reader_role_creation_unconfirmed", status="partial_failure")
|
|
role_id = _uuid(_remote_id(matches[0]))
|
|
if role_id is None:
|
|
_fail("forge_reader_role_id_invalid", status="partial_failure")
|
|
_prove_empty_role(connector, role_id)
|
|
_journal_store(session, journal, phase="permissions", intent=None, permission_index=None, role_id=role_id)
|
|
return role_id
|
|
|
|
details = _role_list(connector, role_id=role_id)
|
|
if len(details) != 1 or details[0].get("name") != FORGE_READER_ROLE or _uuid(_remote_id(details[0])) != role_id:
|
|
_fail("forge_reader_role_binding_mismatch")
|
|
return role_id
|
|
|
|
|
|
def _validate_existing_user(
|
|
user: dict[str, Any],
|
|
*,
|
|
journal: dict[str, Any],
|
|
role_id: str,
|
|
) -> str:
|
|
user_id = _positive_id(_remote_id(user))
|
|
if user_id is None:
|
|
_fail("forge_reader_user_id_invalid")
|
|
if journal["userId"] is not None and journal["userId"] != user_id:
|
|
_fail("forge_reader_user_binding_mismatch")
|
|
attributes = user.get("attributes")
|
|
if (
|
|
user.get("username") != FORGE_READER_USERNAME
|
|
or user.get("name") != FORGE_READER_NAME
|
|
or user.get("type") != "service_account"
|
|
or user.get("path") != FORGE_READER_PATH
|
|
or user.get("is_active") is not True
|
|
or user.get("is_superuser") is not False
|
|
or attributes != {FORGE_READER_OWNER_ATTRIBUTE: journal["nonce"]}
|
|
):
|
|
_fail("forge_reader_existing_user_conflict")
|
|
groups = user.get("groups")
|
|
roles = user.get("roles")
|
|
if not isinstance(groups, list) or not isinstance(roles, list) or groups:
|
|
_fail("forge_reader_user_membership_conflict")
|
|
role_ids = {str(_remote_id(item)) for item in roles}
|
|
if journal["phase"] == "membership" and journal["intent"] == "membership":
|
|
allowed_role_states = (set(), {role_id})
|
|
elif journal["phase"] in {"token", "ready"}:
|
|
allowed_role_states = ({role_id},)
|
|
else:
|
|
allowed_role_states = (set(),)
|
|
if role_ids not in allowed_role_states or len(roles) != len(role_ids):
|
|
_fail("forge_reader_user_role_conflict")
|
|
return user_id
|
|
|
|
|
|
def _user_matches_by_username(connector: Any) -> list[dict[str, Any]]:
|
|
users = _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/core/users/",
|
|
{"username": FORGE_READER_USERNAME, "include_groups": "true", "include_roles": "true", "page_size": 100},
|
|
)
|
|
return [user for user in users if user.get("username") == FORGE_READER_USERNAME]
|
|
|
|
|
|
def _ensure_role_permissions(
|
|
session: Session,
|
|
connector: Any,
|
|
journal: dict[str, Any],
|
|
manifest: list[dict[str, str]],
|
|
) -> str:
|
|
role_id = journal["roleId"]
|
|
if role_id is None:
|
|
_fail("forge_reader_role_binding_missing")
|
|
_verify_permission_catalog(connector, manifest)
|
|
required = _manifest_keys(manifest)
|
|
current = _role_object_permission_keys(_role_object_permissions(connector, role_id))
|
|
globals_ = _global_permissions(connector, role_id)
|
|
if globals_:
|
|
_fail("forge_reader_global_permission_conflict")
|
|
if not current.issubset(required):
|
|
_fail("forge_reader_object_permission_conflict")
|
|
if journal["phase"] not in {"permissions"} and current != required:
|
|
_fail("forge_reader_permissions_incomplete")
|
|
if journal["phase"] == "permissions" and journal["intent"] not in {None, "permission"}:
|
|
_fail("forge_reader_journal_stage_conflict")
|
|
|
|
current_list = current
|
|
members = _role_users(connector, role_id)
|
|
if journal["phase"] in {"permissions", "user"} and members:
|
|
_fail("forge_reader_role_has_other_users")
|
|
groups = _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/core/groups/",
|
|
_GROUP_LIST_PARAMS,
|
|
)
|
|
if _group_has_role(groups, role_id):
|
|
_fail("forge_reader_role_assigned_to_group")
|
|
for index, row in enumerate(manifest):
|
|
key = (row["app"], row["model"], row["codename"], row["objectPk"])
|
|
if key in current_list:
|
|
continue
|
|
if journal["phase"] != "permissions":
|
|
_journal_store(session, journal, phase="permissions", intent="permission", permission_index=index)
|
|
else:
|
|
_journal_store(session, journal, phase="permissions", intent="permission", permission_index=index)
|
|
model = f"{row['app']}.{row['model']}"
|
|
try:
|
|
_admin_json(
|
|
connector,
|
|
"POST",
|
|
f"/rbac/permissions/assigned_by_roles/{quote(role_id, safe='')}/assign/",
|
|
payload={"permissions": [row["codename"]], "model": model, "object_pk": row["objectPk"]},
|
|
)
|
|
except _ReaderFailure:
|
|
# A transport failure is reconciled by GET; this invocation never blindly repeats.
|
|
pass
|
|
current = _role_object_permission_keys(_role_object_permissions(connector, role_id))
|
|
globals_ = _global_permissions(connector, role_id)
|
|
if globals_ or not current.issubset(required):
|
|
_fail("forge_reader_permission_assignment_conflict")
|
|
if key not in current:
|
|
_fail("forge_reader_permission_assignment_unconfirmed", status="partial_failure")
|
|
_journal_store(session, journal, phase="permissions", intent=None, permission_index=None)
|
|
current_list = current
|
|
final = _role_object_permission_keys(_role_object_permissions(connector, role_id))
|
|
if final != required or _global_permissions(connector, role_id):
|
|
_fail("forge_reader_permissions_incomplete", status="partial_failure")
|
|
if journal["phase"] == "permissions":
|
|
_journal_store(session, journal, phase="user", intent="user", permission_index=None)
|
|
return role_id
|
|
|
|
|
|
def _user_detail(connector: Any, user_id: str) -> dict[str, Any]:
|
|
value = _admin_json(
|
|
connector,
|
|
"GET",
|
|
f"/core/users/{quote(user_id, safe='')}/",
|
|
params={"include_groups": "true", "include_roles": "true"},
|
|
)
|
|
if not isinstance(value, dict):
|
|
_fail("forge_reader_user_readback_invalid", status="failed")
|
|
return value
|
|
|
|
|
|
def _ensure_user(session: Session, connector: Any, journal: dict[str, Any], role_id: str) -> str:
|
|
matches = _user_matches_by_username(connector)
|
|
if len(matches) > 1:
|
|
_fail("forge_reader_user_ambiguous")
|
|
if matches:
|
|
user_id = _validate_existing_user(
|
|
matches[0],
|
|
journal=journal,
|
|
role_id=role_id,
|
|
)
|
|
detail = _user_detail(connector, user_id)
|
|
observed_id = _validate_existing_user(
|
|
detail,
|
|
journal=journal,
|
|
role_id=role_id,
|
|
)
|
|
if observed_id != user_id:
|
|
_fail("forge_reader_user_readback_mismatch")
|
|
if journal["userId"] is None:
|
|
if journal["phase"] != "user" or journal["intent"] != "user":
|
|
_fail("forge_reader_unbound_user_conflict")
|
|
_journal_store(session, journal, phase="membership", intent="membership", user_id=user_id)
|
|
elif journal["phase"] == "user":
|
|
_journal_store(session, journal, phase="membership", intent="membership", user_id=user_id)
|
|
return user_id
|
|
|
|
if journal["userId"] is not None:
|
|
_fail("forge_reader_bound_user_missing")
|
|
if journal["phase"] != "user" or journal["intent"] != "user":
|
|
_fail("forge_reader_user_creation_not_authorized")
|
|
payload = {
|
|
"username": FORGE_READER_USERNAME,
|
|
"name": FORGE_READER_NAME,
|
|
"type": "service_account",
|
|
"path": FORGE_READER_PATH,
|
|
"attributes": {FORGE_READER_OWNER_ATTRIBUTE: journal["nonce"]},
|
|
"groups": [],
|
|
"roles": [],
|
|
}
|
|
try:
|
|
_admin_json(connector, "POST", "/core/users/", payload=payload)
|
|
except _ReaderFailure:
|
|
pass
|
|
matches = _user_matches_by_username(connector)
|
|
if len(matches) != 1:
|
|
_fail("forge_reader_user_creation_unconfirmed", status="partial_failure")
|
|
user_id = _validate_existing_user(matches[0], journal=journal, role_id=role_id)
|
|
detail = _user_detail(connector, user_id)
|
|
if _validate_existing_user(detail, journal=journal, role_id=role_id) != user_id:
|
|
_fail("forge_reader_user_readback_mismatch", status="partial_failure")
|
|
_journal_store(session, journal, phase="membership", intent="membership", user_id=user_id)
|
|
return user_id
|
|
|
|
|
|
def _users_with_role(connector: Any, role_id: str) -> list[dict[str, Any]]:
|
|
return _role_users(connector, role_id)
|
|
|
|
|
|
def _user_roles(connector: Any, user_id: str) -> list[dict[str, Any]]:
|
|
return _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/rbac/roles/",
|
|
{"users": user_id, "inherited": "true", "page_size": 100},
|
|
)
|
|
|
|
|
|
def _ensure_membership(session: Session, connector: Any, journal: dict[str, Any], user_id: str, role_id: str) -> None:
|
|
all_groups = _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/core/groups/",
|
|
_GROUP_LIST_PARAMS,
|
|
)
|
|
if _group_has_role(all_groups, role_id):
|
|
_fail("forge_reader_role_assigned_to_group")
|
|
members = _users_with_role(connector, role_id)
|
|
member_ids = [_positive_id(_remote_id(item)) for item in members]
|
|
if any(value is None for value in member_ids) or len(set(member_ids)) != len(member_ids):
|
|
_fail("forge_reader_role_user_readback_invalid")
|
|
if member_ids not in ([], [user_id]):
|
|
_fail("forge_reader_role_has_other_users")
|
|
detail = _user_detail(connector, user_id)
|
|
_validate_existing_user(
|
|
detail,
|
|
journal=journal,
|
|
role_id=role_id,
|
|
)
|
|
roles = _user_roles(connector, user_id)
|
|
role_ids = [_uuid(_remote_id(item)) for item in roles]
|
|
if any(value is None for value in role_ids) or len(set(role_ids)) != len(role_ids):
|
|
_fail("forge_reader_inherited_role_readback_invalid")
|
|
if role_ids not in ([], [role_id]):
|
|
_fail("forge_reader_inherited_role_conflict")
|
|
has_role_membership = member_ids == [user_id]
|
|
user_has_role = role_ids == [role_id]
|
|
if has_role_membership != user_has_role:
|
|
_fail("forge_reader_role_membership_readback_conflict")
|
|
if has_role_membership:
|
|
if journal["phase"] == "membership":
|
|
_journal_store(session, journal, phase="token", intent="token")
|
|
return
|
|
if journal["phase"] != "membership" or journal["intent"] != "membership":
|
|
_fail("forge_reader_role_membership_not_authorized")
|
|
_journal_store(session, journal, phase="membership", intent="membership")
|
|
try:
|
|
_admin_json(
|
|
connector,
|
|
"POST",
|
|
f"/rbac/roles/{quote(role_id, safe='')}/add_user/",
|
|
payload={"pk": int(user_id)},
|
|
)
|
|
except _ReaderFailure:
|
|
pass
|
|
members = _users_with_role(connector, role_id)
|
|
member_ids = [_positive_id(_remote_id(item)) for item in members]
|
|
roles = _user_roles(connector, user_id)
|
|
role_ids = [_uuid(_remote_id(item)) for item in roles]
|
|
if member_ids != [user_id] or role_ids != [role_id]:
|
|
_fail("forge_reader_role_membership_unconfirmed", status="partial_failure")
|
|
_journal_store(session, journal, phase="token", intent="token")
|
|
|
|
|
|
def _token_metadata(connector: Any) -> list[dict[str, Any]]:
|
|
return _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/core/tokens/",
|
|
{"identifier": FORGE_READER_TOKEN_ID, "page_size": 100},
|
|
)
|
|
|
|
|
|
def _validate_token_metadata(token: dict[str, Any], *, user_id: str, journal: dict[str, Any]) -> str:
|
|
token_id = _uuid(_remote_id(token))
|
|
token_user_id = _positive_id(_remote_id(token.get("user")))
|
|
if (
|
|
token_id is None
|
|
or token_user_id != user_id
|
|
or token.get("identifier") != FORGE_READER_TOKEN_ID
|
|
or token.get("intent") != "api"
|
|
or token.get("description") != FORGE_READER_TOKEN_DESCRIPTION
|
|
or token.get("expiring") is not False
|
|
):
|
|
_fail("forge_reader_token_metadata_conflict")
|
|
if journal["tokenId"] is not None and journal["tokenId"] != token_id:
|
|
_fail("forge_reader_token_binding_mismatch")
|
|
return token_id
|
|
|
|
|
|
def _ensure_token(
|
|
session: Session,
|
|
connector: Any,
|
|
journal: dict[str, Any],
|
|
user_id: str,
|
|
admin_token: str,
|
|
) -> str:
|
|
existing_secret = _reader_secret(session)
|
|
rows = _token_metadata(connector)
|
|
rows = [row for row in rows if row.get("identifier") == FORGE_READER_TOKEN_ID]
|
|
if len(rows) > 1:
|
|
_fail("forge_reader_token_ambiguous")
|
|
if rows:
|
|
if journal["phase"] not in {"token", "ready"} or journal["intent"] != "token" and journal["phase"] != "ready":
|
|
_fail("forge_reader_unbound_token_conflict")
|
|
token_id = _validate_token_metadata(rows[0], user_id=user_id, journal=journal)
|
|
if journal["tokenId"] is None:
|
|
_journal_store(session, journal, phase="token", intent="token", token_id=token_id)
|
|
else:
|
|
if existing_secret is not None:
|
|
_fail("forge_reader_secret_without_remote_token")
|
|
if journal["phase"] != "token" or journal["intent"] != "token":
|
|
_fail("forge_reader_token_creation_not_authorized")
|
|
try:
|
|
_admin_json(
|
|
connector,
|
|
"POST",
|
|
"/core/tokens/",
|
|
payload={
|
|
"identifier": FORGE_READER_TOKEN_ID,
|
|
"intent": "api",
|
|
"user": int(user_id),
|
|
"description": FORGE_READER_TOKEN_DESCRIPTION,
|
|
"expiring": False,
|
|
},
|
|
)
|
|
except _ReaderFailure:
|
|
pass
|
|
rows = _token_metadata(connector)
|
|
rows = [row for row in rows if row.get("identifier") == FORGE_READER_TOKEN_ID]
|
|
if len(rows) != 1:
|
|
_fail("forge_reader_token_creation_unconfirmed", status="partial_failure")
|
|
token_id = _validate_token_metadata(rows[0], user_id=user_id, journal=journal)
|
|
_journal_store(session, journal, phase="token", intent="token", token_id=token_id)
|
|
|
|
if journal["tokenId"] is None:
|
|
_fail("forge_reader_token_binding_missing")
|
|
token = _admin_json(
|
|
connector,
|
|
"GET",
|
|
f"/core/tokens/{quote(FORGE_READER_TOKEN_ID, safe='')}/view_key/",
|
|
)
|
|
raw_key = token.get("key") if isinstance(token, dict) else None
|
|
if not isinstance(raw_key, str) or not _TOKEN_PATTERN.fullmatch(raw_key):
|
|
token = None
|
|
raw_key = None
|
|
_fail("forge_reader_token_key_invalid", status="partial_failure")
|
|
if existing_secret is None:
|
|
if has_secret(session, FORGE_READER_TOKEN_REF):
|
|
raw_key = None
|
|
_fail("forge_reader_token_unreadable")
|
|
stored = _persist_token(session, raw_key)
|
|
if stored is not True:
|
|
raw_key = None
|
|
_fail("forge_reader_token_persist_failed", status="partial_failure")
|
|
elif not hmac.compare_digest(existing_secret, raw_key):
|
|
raw_key = None
|
|
_fail("forge_reader_original_token_mismatch")
|
|
raw_key = None
|
|
token = None
|
|
if existing_secret is None:
|
|
existing_secret = _reader_secret(session)
|
|
if not isinstance(existing_secret, str):
|
|
_fail("forge_reader_token_persist_failed", status="partial_failure")
|
|
if hmac.compare_digest(existing_secret, admin_token):
|
|
_fail("forge_reader_administrative_token_reuse")
|
|
return existing_secret
|
|
|
|
|
|
def _b64url_int(value: Any) -> int | None:
|
|
if not isinstance(value, str) or not value:
|
|
return None
|
|
try:
|
|
padded = value + "=" * (-len(value) % 4)
|
|
decoded = base64.urlsafe_b64decode(padded.encode("ascii"))
|
|
return int.from_bytes(decoded, "big") if decoded else None
|
|
except (ValueError, TypeError, UnicodeError):
|
|
return None
|
|
|
|
|
|
def _verify_public_reader(
|
|
connector: Any,
|
|
reader_token: str,
|
|
oidc_secret: str,
|
|
context: dict[str, Any],
|
|
role_id: str,
|
|
user_id: str,
|
|
) -> None:
|
|
self_response = _reader_json(connector, reader_token, "/core/users/me/")
|
|
reader = self_response.get("user") if isinstance(self_response, dict) else None
|
|
if (
|
|
not isinstance(reader, dict)
|
|
or _positive_id(_remote_id(reader)) != user_id
|
|
or reader.get("username") != FORGE_READER_USERNAME
|
|
or reader.get("name") != FORGE_READER_NAME
|
|
or reader.get("is_active") is not True
|
|
or reader.get("is_superuser") is not False
|
|
or reader.get("type") != "service_account"
|
|
or reader.get("system_permissions") != []
|
|
or reader.get("groups") != []
|
|
):
|
|
_fail("forge_reader_self_readback_mismatch")
|
|
roles = reader.get("roles")
|
|
if not isinstance(roles, list) or len(roles) != 1 or _uuid(_remote_id(roles[0])) != role_id:
|
|
_fail("forge_reader_self_role_mismatch")
|
|
|
|
apps = _paged(
|
|
lambda path, params=None: _reader_json(connector, reader_token, path, params=params),
|
|
"/core/applications/",
|
|
{"page_size": 100, "search": FORGE_OIDC_CLIENT_ID},
|
|
)
|
|
app_matches = [
|
|
app
|
|
for app in apps
|
|
if app.get("slug") == FORGE_OIDC_CLIENT_ID and _uuid(_remote_id(app)) == context["applicationId"]
|
|
]
|
|
if len(app_matches) != 1:
|
|
_fail("forge_reader_application_visibility_mismatch")
|
|
application = _reader_json(connector, reader_token, f"/core/applications/{FORGE_OIDC_CLIENT_ID}/")
|
|
if (
|
|
not isinstance(application, dict)
|
|
or _uuid(_remote_id(application)) != context["applicationId"]
|
|
or application.get("name") != FORGE_OIDC_APPLICATION_NAME
|
|
or application.get("slug") != FORGE_OIDC_CLIENT_ID
|
|
or str(application.get("meta_launch_url") or application.get("launch_url") or "").rstrip("/") != FORGE_OIDC_LAUNCH_URL
|
|
or application.get("meta_description") != FORGE_OIDC_MANAGED_DESCRIPTION
|
|
or _positive_id(_remote_id(application.get("provider")), provider=True) != context["providerId"]
|
|
):
|
|
_fail("forge_reader_application_detail_mismatch")
|
|
application = None
|
|
|
|
provider = _reader_json(
|
|
connector,
|
|
reader_token,
|
|
f"/providers/oauth2/{quote(context['providerId'], safe='')}/",
|
|
)
|
|
if (
|
|
not isinstance(provider, dict)
|
|
or not AuthentikConnector._forge_oidc_provider_matches(provider, signing_key_id=context["signingKeyId"])
|
|
or _positive_id(_remote_id(provider), provider=True) != context["providerId"]
|
|
):
|
|
_fail("forge_reader_provider_visibility_mismatch")
|
|
actual_secret = provider.get("client_secret")
|
|
secret_matches = (
|
|
isinstance(actual_secret, str)
|
|
and actual_secret.isascii()
|
|
and oidc_secret.isascii()
|
|
and hmac.compare_digest(actual_secret, oidc_secret)
|
|
)
|
|
actual_secret = None
|
|
provider = None
|
|
if secret_matches is not True:
|
|
_fail("forge_reader_provider_secret_readback_mismatch")
|
|
|
|
for scope, mapping_id in context["scopeMappingIds"].items():
|
|
mapping = _reader_json(
|
|
connector,
|
|
reader_token,
|
|
f"/propertymappings/provider/scope/{quote(mapping_id, safe='')}/",
|
|
)
|
|
if not _forge_oidc_scope_mapping_matches(mapping, mapping_id=mapping_id, scope=scope):
|
|
_fail("forge_reader_scope_mapping_visibility_mismatch")
|
|
|
|
visible_groups = _paged(
|
|
lambda path, params=None: _reader_json(connector, reader_token, path, params=params),
|
|
"/core/groups/",
|
|
_GROUP_LIST_PARAMS,
|
|
)
|
|
visible_by_id = {_uuid(_remote_id(group)): group for group in visible_groups}
|
|
if len(visible_by_id) != 3 or set(visible_by_id) != set(context["groupIds"].values()):
|
|
_fail("forge_reader_group_visibility_mismatch")
|
|
for name in FORGE_OIDC_GROUPS:
|
|
group_id = context["groupIds"][name]
|
|
group = visible_by_id[group_id]
|
|
if (
|
|
group.get("name") != name
|
|
or not _forge_oidc_group_contract_matches(group, require_users=True)
|
|
or group.get("inherited_roles_obj") != []
|
|
):
|
|
_fail("forge_reader_group_readback_mismatch")
|
|
|
|
key_id = context["signingKeyId"]
|
|
key = _reader_json(
|
|
connector,
|
|
reader_token,
|
|
f"/crypto/certificatekeypairs/{quote(key_id, safe='')}/",
|
|
)
|
|
if (
|
|
not isinstance(key, dict)
|
|
or _uuid(_remote_id(key)) != key_id
|
|
or str(key.get("key_type") or "").lower() != "rsa"
|
|
or key.get("private_key_available") is not True
|
|
):
|
|
_fail("forge_reader_signing_key_readback_mismatch")
|
|
cert_response = _reader_json(
|
|
connector,
|
|
reader_token,
|
|
f"/crypto/certificatekeypairs/{quote(key_id, safe='')}/view_certificate/",
|
|
)
|
|
certificate_data = cert_response.get("data") if isinstance(cert_response, dict) else None
|
|
if not isinstance(certificate_data, str):
|
|
_fail("forge_reader_public_certificate_readback_invalid")
|
|
try:
|
|
certificate = x509.load_pem_x509_certificate(certificate_data.encode("ascii"))
|
|
public_key = certificate.public_key()
|
|
except (ValueError, TypeError, UnicodeError):
|
|
_fail("forge_reader_public_certificate_readback_invalid")
|
|
if not isinstance(public_key, rsa.RSAPublicKey):
|
|
_fail("forge_reader_signing_key_not_rsa")
|
|
numbers = public_key.public_numbers()
|
|
|
|
discovery = _public_json(FORGE_OIDC_DISCOVERY_URL)
|
|
if (
|
|
not isinstance(discovery, dict)
|
|
or discovery.get("issuer") != FORGE_OIDC_ISSUER
|
|
or "code" not in discovery.get("response_types_supported", [])
|
|
or "authorization_code" not in discovery.get("grant_types_supported", [])
|
|
or not {"openid", "email", "profile"}.issubset(set(discovery.get("scopes_supported", [])))
|
|
or "RS256" not in discovery.get("id_token_signing_alg_values_supported", [])
|
|
or discovery.get("jwks_uri") != f"{FORGE_OIDC_ISSUER}jwks/"
|
|
):
|
|
_fail("forge_reader_discovery_contract_mismatch")
|
|
jwks_url = discovery["jwks_uri"]
|
|
jwks = _public_json(jwks_url)
|
|
keys = jwks.get("keys") if isinstance(jwks, dict) else None
|
|
if not isinstance(keys, list):
|
|
_fail("forge_reader_jwks_invalid")
|
|
matches = [
|
|
item
|
|
for item in keys
|
|
if isinstance(item, dict)
|
|
and item.get("kty") == "RSA"
|
|
and item.get("alg") == "RS256"
|
|
and item.get("use") == "sig"
|
|
and _b64url_int(item.get("n")) == numbers.n
|
|
and _b64url_int(item.get("e")) == numbers.e
|
|
]
|
|
if len(matches) != 1:
|
|
_fail("forge_reader_jwks_signing_key_mismatch")
|
|
certificate_data = None
|
|
public_key = None
|
|
certificate = None
|
|
jwks = None
|
|
discovery = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, repr=False)
|
|
class _VerifiedForgeDeliveryInputs:
|
|
"""Held delivery inputs; deliberately cannot print secret-bearing fields."""
|
|
|
|
context: dict[str, Any]
|
|
oidc_client_secret: bytes = field(repr=False)
|
|
authentik_read_token: bytes = field(repr=False)
|
|
|
|
|
|
def _verify_forge_delivery_inputs(
|
|
session: Session,
|
|
connector: Any,
|
|
) -> _VerifiedForgeDeliveryInputs:
|
|
"""Revalidate the complete fixed Forge reader contract without writes."""
|
|
if not has_secret(session, FORGE_OIDC_BINDING_SECRET_REF):
|
|
_fail("forge_reader_oidc_binding_required", status="blocked")
|
|
if not has_secret(session, FORGE_READER_BINDING_REF):
|
|
_fail("forge_reader_binding_required", status="blocked")
|
|
if not has_secret(session, FORGE_READER_TOKEN_REF):
|
|
_fail("forge_reader_token_required", status="blocked")
|
|
|
|
raw_journal = get_secret(session, FORGE_READER_BINDING_REF, touch=False)
|
|
try:
|
|
journal = json.loads(raw_journal, object_pairs_hook=_unique_object)
|
|
except (TypeError, ValueError, UnicodeError):
|
|
_fail("forge_reader_binding_invalid")
|
|
if not _valid_journal(journal):
|
|
_fail("forge_reader_binding_invalid")
|
|
if not hmac.compare_digest(raw_journal, _journal_bytes(journal)):
|
|
_fail("forge_reader_binding_not_canonical")
|
|
if (
|
|
journal["phase"] != "ready"
|
|
or journal["intent"] is not None
|
|
or journal["permissionIndex"] is not None
|
|
or not journal["userId"]
|
|
or not journal["roleId"]
|
|
or not journal["tokenId"]
|
|
):
|
|
_fail("forge_reader_binding_not_ready", status="blocked")
|
|
|
|
context, oidc_secret = _oidc_resource_context(session, connector)
|
|
manifest = _manifest(context)
|
|
if journal["resourceContext"] != context or journal["permissionManifest"] != manifest:
|
|
_fail("forge_reader_binding_resource_mismatch")
|
|
|
|
role_id = journal["roleId"]
|
|
user_id = journal["userId"]
|
|
role_rows = _role_list(connector, role_id=role_id)
|
|
if (
|
|
len(role_rows) != 1
|
|
or role_rows[0].get("name") != FORGE_READER_ROLE
|
|
or _uuid(_remote_id(role_rows[0])) != role_id
|
|
):
|
|
_fail("forge_reader_role_binding_mismatch")
|
|
|
|
expected_permissions = _manifest_keys(manifest)
|
|
actual_permissions = _role_object_permission_keys(
|
|
_role_object_permissions(connector, role_id)
|
|
)
|
|
if actual_permissions != expected_permissions:
|
|
_fail("forge_reader_permissions_incomplete")
|
|
if _global_permissions(connector, role_id):
|
|
_fail("forge_reader_global_permission_conflict")
|
|
|
|
members = _role_users(connector, role_id)
|
|
member_ids = [_positive_id(_remote_id(item)) for item in members]
|
|
if member_ids != [user_id]:
|
|
_fail("forge_reader_role_membership_readback_conflict")
|
|
all_groups = _paged(
|
|
lambda path, params=None: _admin_json(connector, "GET", path, params=params),
|
|
"/core/groups/",
|
|
_GROUP_LIST_PARAMS,
|
|
)
|
|
if _group_has_role(all_groups, role_id):
|
|
_fail("forge_reader_role_assigned_to_group")
|
|
|
|
users = _user_matches_by_username(connector)
|
|
if len(users) != 1:
|
|
_fail("forge_reader_user_ambiguous")
|
|
user = _user_detail(connector, user_id)
|
|
if _validate_existing_user(user, journal=journal, role_id=role_id) != user_id:
|
|
_fail("forge_reader_user_readback_mismatch")
|
|
if _positive_id(_remote_id(users[0])) != user_id:
|
|
_fail("forge_reader_user_binding_mismatch")
|
|
user_roles = _user_roles(connector, user_id)
|
|
user_role_ids = [_uuid(_remote_id(item)) for item in user_roles]
|
|
if user_role_ids != [role_id]:
|
|
_fail("forge_reader_user_role_conflict")
|
|
|
|
tokens = [
|
|
item for item in _token_metadata(connector)
|
|
if item.get("identifier") == FORGE_READER_TOKEN_ID
|
|
]
|
|
if len(tokens) != 1:
|
|
_fail("forge_reader_token_ambiguous")
|
|
if _validate_token_metadata(tokens[0], user_id=user_id, journal=journal) != journal["tokenId"]:
|
|
_fail("forge_reader_token_binding_mismatch")
|
|
|
|
reader_token = _reader_secret(session)
|
|
if reader_token is None:
|
|
_fail("forge_reader_token_required", status="blocked")
|
|
admin_config = connector._config()
|
|
admin_token = getattr(admin_config, "token", None) if admin_config is not None else None
|
|
if isinstance(admin_token, str) and hmac.compare_digest(reader_token, admin_token):
|
|
_fail("forge_reader_administrative_token_reuse")
|
|
if (
|
|
not isinstance(oidc_secret, str)
|
|
or not oidc_secret.isascii()
|
|
or not oidc_secret.isprintable()
|
|
or not reader_token.isascii()
|
|
):
|
|
_fail("forge_reader_delivery_input_invalid")
|
|
|
|
_verify_public_reader(
|
|
connector,
|
|
reader_token,
|
|
oidc_secret,
|
|
context,
|
|
role_id,
|
|
user_id,
|
|
)
|
|
return _VerifiedForgeDeliveryInputs(
|
|
context=dict(context),
|
|
oidc_client_secret=oidc_secret.encode("ascii"),
|
|
authentik_read_token=reader_token.encode("ascii"),
|
|
)
|
|
|
|
|
|
def _preflight_collisions(
|
|
connector: Any,
|
|
journal: dict[str, Any],
|
|
role_id: str | None,
|
|
existing_token: str | None,
|
|
) -> None:
|
|
users = _user_matches_by_username(connector)
|
|
if len(users) > 1:
|
|
_fail("forge_reader_user_ambiguous")
|
|
if users:
|
|
if role_id is None:
|
|
role_id = journal.get("roleId")
|
|
if role_id is None:
|
|
_fail("forge_reader_existing_user_conflict")
|
|
_validate_existing_user(
|
|
users[0],
|
|
journal=journal,
|
|
role_id=role_id,
|
|
)
|
|
token_rows = _token_metadata(connector)
|
|
token_rows = [row for row in token_rows if row.get("identifier") == FORGE_READER_TOKEN_ID]
|
|
if len(token_rows) > 1:
|
|
_fail("forge_reader_token_ambiguous")
|
|
if token_rows and (
|
|
journal["phase"] not in {"token", "ready"}
|
|
or (journal["intent"] != "token" and journal["phase"] != "ready")
|
|
):
|
|
_fail("forge_reader_unbound_token_conflict")
|
|
if token_rows:
|
|
if journal["userId"] is None:
|
|
_fail("forge_reader_unbound_token_conflict")
|
|
if len(token_rows) != 1:
|
|
_fail("forge_reader_token_ambiguous")
|
|
token_id = _validate_token_metadata(token_rows[0], user_id=journal["userId"], journal=journal)
|
|
if journal["tokenId"] is not None and journal["tokenId"] != token_id:
|
|
_fail("forge_reader_token_binding_mismatch")
|
|
elif existing_token is not None or journal["tokenId"] is not None:
|
|
_fail("forge_reader_secret_without_remote_token")
|
|
|
|
|
|
def _provision_locked(session: Session, connector: Any) -> dict[str, Any]:
|
|
context, oidc_secret = _oidc_resource_context(session, connector)
|
|
manifest = _manifest(context)
|
|
journal = _load_or_create_journal(session, context, manifest)
|
|
existing_token = _reader_secret(session)
|
|
role_id = journal["roleId"]
|
|
_preflight_collisions(connector, journal, role_id, existing_token)
|
|
role_id = _resolve_role(session, connector, journal)
|
|
_ensure_role_permissions(session, connector, journal, manifest)
|
|
user_id = _ensure_user(session, connector, journal, role_id)
|
|
_ensure_membership(session, connector, journal, user_id, role_id)
|
|
admin_token = str(connector._config().token)
|
|
reader_token = _ensure_token(session, connector, journal, user_id, admin_token)
|
|
_verify_public_reader(connector, reader_token, oidc_secret, context, role_id, user_id)
|
|
_journal_store(session, journal, phase="ready", intent=None, permission_index=None, token_id=journal["tokenId"])
|
|
return _result("ready", "forge_reader_ready", ready=True)
|
|
|
|
|
|
def _result(status: str, message: str, *, ready: bool = False) -> dict[str, Any]:
|
|
return {
|
|
"success": ready,
|
|
"status": status,
|
|
"message": message,
|
|
"ready": ready,
|
|
"secretRef": FORGE_READER_TOKEN_REF,
|
|
}
|
|
|
|
|
|
def _audit_result(session: Session, identity: str, outcome: dict[str, Any]) -> bool:
|
|
try:
|
|
log_event(
|
|
session,
|
|
source="manual",
|
|
identity=identity,
|
|
action_id=FORGE_OIDC_ACTION,
|
|
target=FORGE_OIDC_TARGET,
|
|
target_type="authentik_service_account",
|
|
risk="critical",
|
|
policy_decision="allowed" if outcome["status"] != "blocked" else "blocked",
|
|
reason=outcome["message"],
|
|
result=outcome["status"],
|
|
redacted_metadata={"reader": FORGE_READER_USERNAME, "secretRef": FORGE_READER_TOKEN_REF},
|
|
)
|
|
return True
|
|
except Exception: # noqa: BLE001 - audit failures cannot expose credentials or remote bodies.
|
|
session.rollback()
|
|
return False
|
|
|
|
|
|
def provision_forge_controller_reader(
|
|
session: Session,
|
|
*,
|
|
actor_identity: str,
|
|
authentik: Any | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Provision and verify the one fixed, read-only Forge Authentik identity."""
|
|
identity = actor_identity.strip() if isinstance(actor_identity, str) else ""
|
|
if not identity:
|
|
outcome = _result("blocked", "forge_reader_operator_identity_required")
|
|
else:
|
|
try:
|
|
with _forge_oidc_process_lock():
|
|
if not _exact_policy_allowed(session):
|
|
outcome = _result("blocked", "forge_reader_exact_target_policy_required")
|
|
elif secret_storage_status().get("productionReady") is not True:
|
|
outcome = _result("blocked", "forge_reader_secret_store_not_production_ready")
|
|
else:
|
|
if authentik is None:
|
|
from .setup_service import apply_stored_connector_settings
|
|
|
|
apply_stored_connector_settings(session, {"authentik"})
|
|
outcome = _provision_locked(session, authentik or AuthentikConnector())
|
|
except _ReaderFailure as exc:
|
|
outcome = _result(exc.status, exc.code)
|
|
except Exception: # noqa: BLE001 - fail closed without remote details or credentials.
|
|
outcome = _result("failed", "forge_reader_provisioning_failed")
|
|
if not _audit_result(session, identity or "system", outcome) and outcome["ready"] is True:
|
|
return _result("partial_failure", "forge_reader_audit_persist_failed")
|
|
return outcome
|