"""flex-auth PEP — check actions via POST /v1/check (fail-closed). Vocabulary (minimal for shell / CSOC-WP-0002-T07): | action | resource.type | When allowed (local mode) | |-------------------|---------------|--------------------------------| | shell:view | shell | any authenticated principal | | member:self:read | member | subject matches resource id | | member:admin | member | deny until policy package live | When FLEX_AUTH_BASE_URL is set, all checks go to the PDP. Transport or malformed responses → deny. """ from __future__ import annotations import json import uuid from dataclasses import dataclass from typing import Any from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen from django.conf import settings @dataclass(frozen=True) class AuthzDecision: allow: bool reason: str decision_id: str = "" effect: str = "" def check( action: str, *, resource: str = "shell", resource_id: str = "default", subject: str = "", tenant: str = "", subject_type: str = "human", attributes: dict[str, Any] | None = None, ) -> AuthzDecision: """Decide whether `action` is allowed for the subject on the resource.""" tenant = tenant or settings.DEFAULT_TENANT_ID system_id = settings.FLEX_AUTH_PROTECTED_SYSTEM_ID base = (settings.FLEX_AUTH_BASE_URL or "").strip() if not base: return _local_check( action, resource=resource, resource_id=resource_id, subject=subject, ) return _http_check( action, resource=resource, resource_id=resource_id, subject=subject, tenant=tenant, subject_type=subject_type, system_id=system_id, attributes=attributes or {}, ) def _local_check( action: str, *, resource: str, resource_id: str, subject: str, ) -> AuthzDecision: if action == "shell:view" and resource == "shell": return AuthzDecision( allow=True, reason="local-shell-view-authenticated", effect="allow", decision_id=f"local_{uuid.uuid4().hex[:12]}", ) if action == "member:self:read" and resource == "member": if subject and resource_id and subject == resource_id: return AuthzDecision( allow=True, reason="local-member-self-read", effect="allow", decision_id=f"local_{uuid.uuid4().hex[:12]}", ) return AuthzDecision( allow=False, reason="local-member-self-mismatch", effect="deny", ) return AuthzDecision( allow=False, reason="local-fail-closed-unknown-action", effect="deny", ) def _http_check( action: str, *, resource: str, resource_id: str, subject: str, tenant: str, subject_type: str, system_id: str, attributes: dict[str, Any], ) -> AuthzDecision: decision_id = f"req_{uuid.uuid4().hex}" payload = { "id": decision_id, "tenant": tenant, "subject": { "id": subject or "anonymous", "type": subject_type, "tenant": tenant, "attributes": attributes, }, "action": action, "resource": { "id": resource_id, "type": resource, "system": system_id, "tenant": tenant, "attributes": {}, }, "context": {"application_id": settings.USER_ENGINE_APPLICATION_ID}, } url = f"{settings.FLEX_AUTH_BASE_URL.rstrip('/')}/v1/check" timeout = float(getattr(settings, "FLEX_AUTH_TIMEOUT_SECONDS", 3.0)) try: with urlopen( Request( url, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json", "Accept": "application/json"}, method="POST", ), timeout=timeout, ) as resp: body = json.load(resp) effect = str(body.get("effect") or "deny").lower() allow = effect == "allow" return AuthzDecision( allow=allow, reason=str(body.get("reason") or "flex-auth"), decision_id=str(body.get("id") or decision_id), effect=effect, ) except (HTTPError, URLError, TimeoutError, ValueError, KeyError, TypeError, json.JSONDecodeError): return AuthzDecision( allow=False, reason="flex-auth-unavailable-fail-closed", decision_id=decision_id, effect="deny", )