Local + HTTP POST /v1/check PEP with fail-closed transport errors; shell:view enforced on /app/. Vocabulary docs for T07. Helm chart lives in railiance-apps; Dockerfile already present for T08.
162 lines
4.6 KiB
Python
162 lines
4.6 KiB
Python
"""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",
|
|
)
|