Complete flex-auth PEP and document railiance packaging path
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.
This commit is contained in:
parent
0e973a91aa
commit
44439f8d8d
8 changed files with 273 additions and 20 deletions
|
|
@ -1,8 +1,25 @@
|
|||
"""flex-auth PEP port — fail-closed for sensitive actions when PDP missing."""
|
||||
"""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
|
||||
|
||||
|
|
@ -12,22 +29,134 @@ class AuthzDecision:
|
|||
allow: bool
|
||||
reason: str
|
||||
decision_id: str = ""
|
||||
effect: str = ""
|
||||
|
||||
|
||||
def check(action: str, *, resource: str = "shell", subject: str = "") -> AuthzDecision:
|
||||
"""Decide whether `action` is allowed.
|
||||
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()
|
||||
|
||||
- shell:view is allowed for any authenticated principal (shell smoke).
|
||||
- other actions require FLEX_AUTH_BASE_URL; until wired, deny.
|
||||
"""
|
||||
if action == "shell:view":
|
||||
return AuthzDecision(allow=True, reason="shell-view-authenticated")
|
||||
|
||||
if not settings.FLEX_AUTH_BASE_URL:
|
||||
return AuthzDecision(
|
||||
allow=False,
|
||||
reason="flex-auth-not-configured-fail-closed",
|
||||
if not base:
|
||||
return _local_check(
|
||||
action,
|
||||
resource=resource,
|
||||
resource_id=resource_id,
|
||||
subject=subject,
|
||||
)
|
||||
|
||||
# TODO(CSOC-WP-0002-T07): HTTP call to flex-auth PDP
|
||||
return AuthzDecision(allow=False, reason="flex-auth-http-not-implemented")
|
||||
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",
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue