enterprise/flex-auth integration layer

This commit is contained in:
2026-05-04 17:54:52 +02:00
parent e33f296bdb
commit 6cb3b7b172
17 changed files with 1240 additions and 23 deletions

View File

@@ -52,7 +52,12 @@ from markitect_tool.generation import (
from markitect_tool.literate import tangle_markdown, weave_markdown, write_tangle_files
from markitect_tool.ops import IncludeError, compose_files, resolve_includes, transform_markdown
from markitect_tool.processor import ProcessorContext, run_fenced_processors
from markitect_tool.policy import LocalLabelPolicyGateway
from markitect_tool.policy import (
EnterprisePolicyError,
FlexAuthResourceManifest,
LocalLabelPolicyGateway,
load_enterprise_policy_subject,
)
from markitect_tool.query import (
InvalidQueryError,
extract_document,
@@ -791,6 +796,68 @@ def policy_check(
raise click.exceptions.Exit(0 if decision.get("allowed") else 1)
@policy.command("subject")
@click.argument("claims_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option(
"--policy-map",
"policy_map_file",
type=click.Path(exists=True, dir_okay=False, path_type=Path),
required=True,
help="Enterprise policy map file.",
)
@click.option("--group", "groups", multiple=True, help="Additional resolved group. May be repeated.")
@click.option(
"--environment",
type=click.Choice(["development", "test", "production"], case_sensitive=False),
help="Validation environment for issuer safety checks.",
)
@click.option(
"--format",
"output_format",
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
default="text",
show_default=True,
)
def policy_subject(
claims_file: Path,
policy_map_file: Path,
groups: tuple[str, ...],
environment: str | None,
output_format: str,
) -> None:
"""Map enterprise identity claims into a Markitect policy subject."""
try:
subject = load_enterprise_policy_subject(
claims_file,
policy_map_file,
extra_groups=list(groups),
environment=environment,
)
except EnterprisePolicyError as exc:
raise click.ClickException(str(exc)) from exc
_emit_subject_result({"subject": subject.to_dict()}, output_format)
@policy.command("resource-manifest")
@click.argument("manifest_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option(
"--format",
"output_format",
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
default="text",
show_default=True,
)
def policy_resource_manifest(manifest_file: Path, output_format: str) -> None:
"""Inspect a Markitect flex-auth resource registration manifest."""
try:
manifest = FlexAuthResourceManifest.from_file(manifest_file)
except EnterprisePolicyError as exc:
raise click.ClickException(str(exc)) from exc
_emit_resource_manifest_result({"manifest": manifest.to_dict()}, output_format)
@main.group("class")
def class_group() -> None:
"""Resolve deterministic content classes."""
@@ -1736,6 +1803,34 @@ def _emit_policy_result(data: dict, output_format: str) -> None:
click.echo(f"reason: {decision.get('reason')}")
def _emit_subject_result(data: dict, output_format: str) -> None:
if output_format == "json":
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
elif output_format == "yaml":
click.echo(yaml.safe_dump(data, sort_keys=False))
else:
subject = data["subject"]
click.echo(f"subject: {subject.get('id')}")
click.echo(f"roles: {', '.join(subject.get('roles', [])) or '<none>'}")
click.echo(f"labels: {', '.join(subject.get('allowed_labels', [])) or '<none>'}")
click.echo(f"trust_zones: {', '.join(subject.get('trust_zones', [])) or '<none>'}")
click.echo(f"actions: {', '.join(subject.get('allowed_actions', [])) or '<none>'}")
def _emit_resource_manifest_result(data: dict, output_format: str) -> None:
if output_format == "json":
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
elif output_format == "yaml":
click.echo(yaml.safe_dump(data, sort_keys=False))
else:
manifest = data["manifest"]
click.echo(f"manifest: {manifest.get('id')}")
click.echo(f"system: {manifest.get('system')}")
click.echo(f"resources: {len(manifest.get('resources', []))}")
actions = ", ".join(manifest.get("actions", [])) or "<none>"
click.echo(f"actions: {actions}")
def _emit_metrics(data: dict, output_format: str) -> None:
if output_format == "json":
click.echo(json.dumps(data, indent=2, ensure_ascii=False))

View File

@@ -200,6 +200,9 @@ def _local_label_policy_descriptor() -> ExtensionDescriptor:
capabilities=[
ProcessingCapability(id="policy", kind="authorize"),
ProcessingCapability(id="policy_filter", kind="filter"),
ProcessingCapability(id="identity_claims", kind="normalize"),
ProcessingCapability(id="resource_manifest", kind="register"),
ProcessingCapability(id="decision_log", kind="emit"),
ProcessingCapability(id="diagnostics", kind="emit"),
ProcessingCapability(id="provenance", kind="emit"),
],
@@ -211,6 +214,8 @@ def _local_label_policy_descriptor() -> ExtensionDescriptor:
cli={
"commands": [
"mkt policy check",
"mkt policy subject",
"mkt policy resource-manifest",
"mkt cache query --policy",
"mkt search --policy",
]

View File

@@ -14,6 +14,18 @@ from markitect_tool.policy.adapters import (
RulePolicyAdapter,
RulePolicyRequest,
)
from markitect_tool.policy.enterprise import (
EnterprisePolicyError,
EnterprisePolicyMap,
FlexAuthResource,
FlexAuthResourceManifest,
LocalDecisionLogStore,
LocalEnterprisePolicyMapper,
NetKingdomIdentityClaimsAdapter,
StaticDirectoryGroupResolver,
load_enterprise_identity_file,
load_enterprise_policy_subject,
)
from markitect_tool.policy.local import (
LocalLabelPolicy,
LocalLabelPolicyGateway,
@@ -36,16 +48,26 @@ __all__ = [
"DirectoryGroupResolutionRequest",
"DirectoryGroupResolver",
"EnterpriseIdentity",
"EnterprisePolicyError",
"EnterprisePolicyMap",
"EnterprisePolicyMapRequest",
"EnterprisePolicyMapper",
"FlexAuthResource",
"FlexAuthResourceManifest",
"IdentityClaimsAdapter",
"LocalDecisionLogStore",
"LocalEnterprisePolicyMapper",
"PolicyDecision",
"PolicyFilterResult",
"PolicyObject",
"PolicySubject",
"NetKingdomIdentityClaimsAdapter",
"RelationshipPolicyAdapter",
"RelationshipPolicyRequest",
"RulePolicyAdapter",
"RulePolicyRequest",
"StaticDirectoryGroupResolver",
"load_enterprise_identity_file",
"load_enterprise_policy_subject",
"policy_metadata_from_document",
]

View File

@@ -0,0 +1,579 @@
"""Enterprise policy integration helpers for Markitect-side adapters."""
from __future__ import annotations
import base64
import hashlib
import json
import time
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
import yaml
from markitect_tool.policy.adapters import (
DirectoryGroupResolution,
DirectoryGroupResolutionRequest,
EnterpriseIdentity,
EnterprisePolicyMapRequest,
)
from markitect_tool.policy.models import PolicyDecision, PolicySubject
NETKINGDOM_REQUIRED_CLAIMS = {
"iss",
"sub",
"aud",
"exp",
"iat",
"preferred_username",
}
class EnterprisePolicyError(ValueError):
"""Raised when enterprise identity or policy mapping is invalid."""
@dataclass(frozen=True)
class NetKingdomIdentityClaimsAdapter:
"""Validate NetKingdom/key-cape-compatible claims and normalize identity.
This adapter validates already trusted claims or explicitly allowed
unverified JWT fixtures. Live OIDC discovery, JWKS retrieval, and signature
verification belong in provider adapters, normally in flex-auth or
key-cape-facing integration code.
"""
issuer: str | None = None
audiences: list[str] = field(default_factory=list)
clock_skew_seconds: int = 60
reject_local_issuers_in_production: bool = True
def verify(
self,
token_or_assertion: str | dict[str, Any],
*,
context: dict[str, Any] | None = None,
) -> EnterpriseIdentity:
context = dict(context or {})
claims, provenance = self._claims(token_or_assertion, context)
self._validate_claims(claims, context)
roles = _roles_from_claims(claims)
scopes = _scopes_from_claims(claims)
groups = _string_list(claims.get("groups"))
issuer = str(claims["iss"])
subject = str(claims["sub"])
principal_type = _principal_type(claims, roles)
return EnterpriseIdentity(
issuer=issuer,
subject=subject,
principal_type=principal_type,
audience=_string_list(claims.get("aud")),
authorized_party=claims.get("azp") or claims.get("client_id"),
preferred_username=claims.get("preferred_username"),
roles=roles,
scopes=scopes,
groups=groups,
assurance=_assurance_from_claims(claims),
directory={
"groups_claim_present": "groups" in claims,
"group_overage": _has_group_overage(claims),
},
claims={key: claims[key] for key in sorted(claims) if key not in {"groups"}},
provenance=provenance,
)
def _claims(
self,
token_or_assertion: str | dict[str, Any],
context: dict[str, Any],
) -> tuple[dict[str, Any], dict[str, Any]]:
if isinstance(token_or_assertion, dict):
return dict(token_or_assertion), {"source": "claims", "verified_signature": context.get("verified_signature")}
if not context.get("allow_unverified_jwt_fixture"):
raise EnterprisePolicyError(
"JWT strings require a provider adapter with signature verification "
"or context.allow_unverified_jwt_fixture=true for test fixtures."
)
claims = _decode_unverified_jwt_payload(token_or_assertion)
return claims, {"source": "jwt-fixture", "verified_signature": False}
def _validate_claims(self, claims: dict[str, Any], context: dict[str, Any]) -> None:
missing = sorted(NETKINGDOM_REQUIRED_CLAIMS - set(claims))
if missing:
raise EnterprisePolicyError(f"Missing required NetKingdom claims: {missing}")
if not (_roles_from_claims(claims)):
raise EnterprisePolicyError("Missing required role claim: roles or realm_access.roles")
if not (_scopes_from_claims(claims)):
raise EnterprisePolicyError("Missing required scope claim: scope or scp")
issuer = str(claims["iss"])
if self.issuer and issuer != self.issuer:
raise EnterprisePolicyError(f"Unexpected issuer `{issuer}`; expected `{self.issuer}`")
if self.reject_local_issuers_in_production and context.get("environment") == "production":
if _is_local_development_issuer(issuer):
raise EnterprisePolicyError("Local development issuer is not accepted in production")
expected_audiences = set(self.audiences or _string_list(context.get("audiences")))
if expected_audiences:
actual_audiences = set(_string_list(claims.get("aud")))
if not actual_audiences.intersection(expected_audiences):
raise EnterprisePolicyError(
f"Token audience {sorted(actual_audiences)} does not include one of {sorted(expected_audiences)}"
)
now = int(context.get("now", time.time()))
exp = _int_claim(claims, "exp")
iat = _int_claim(claims, "iat")
if exp + self.clock_skew_seconds < now:
raise EnterprisePolicyError("Token is expired")
if iat - self.clock_skew_seconds > now:
raise EnterprisePolicyError("Token issued-at is in the future")
@dataclass(frozen=True)
class StaticDirectoryGroupResolver:
"""Deterministic group resolver for fixtures and local development."""
groups_by_subject: dict[str, list[str]] = field(default_factory=dict)
source: str = "static"
refreshed_at: str | None = None
def resolve(
self,
request: DirectoryGroupResolutionRequest,
) -> DirectoryGroupResolution:
groups = _unique(
list(request.groups)
+ _string_list(request.claims.get("groups"))
+ self.groups_by_subject.get(request.subject_id, [])
)
return DirectoryGroupResolution(
groups=groups,
source=self.source,
refreshed_at=self.refreshed_at,
overage=_has_group_overage(request.claims),
metadata={
"subject_id": request.subject_id,
"issuer": request.issuer,
"claim_group_count": len(_string_list(request.claims.get("groups"))),
},
)
@dataclass(frozen=True)
class EnterprisePolicyMap:
"""Versioned Markitect-side mapping from IAM claims to policy subjects."""
id: str = "enterprise-policy-map"
issuer: str | None = None
audiences: list[str] = field(default_factory=list)
defaults: dict[str, Any] = field(default_factory=dict)
groups: dict[str, dict[str, Any]] = field(default_factory=dict)
roles: dict[str, dict[str, Any]] = field(default_factory=dict)
scopes: dict[str, dict[str, Any]] = field(default_factory=dict)
trust_zones: dict[str, dict[str, Any]] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
@classmethod
def from_mapping(cls, raw: dict[str, Any]) -> "EnterprisePolicyMap":
data = raw.get("policy_map") if isinstance(raw.get("policy_map"), dict) else raw
return cls(
id=str(data.get("id", "enterprise-policy-map")),
issuer=data.get("issuer"),
audiences=_string_list(data.get("audiences") or data.get("audience")),
defaults=dict(data.get("defaults") or {}),
groups=_rule_mapping(data.get("groups")),
roles=_rule_mapping(data.get("roles")),
scopes=_rule_mapping(data.get("scopes")),
trust_zones=_rule_mapping(data.get("trust_zones")),
metadata=dict(data.get("metadata") or {}),
)
@classmethod
def from_file(cls, path: str | Path) -> "EnterprisePolicyMap":
data = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
raise EnterprisePolicyError("Enterprise policy map must contain a mapping.")
return cls.from_mapping(data)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(asdict(self))
@dataclass(frozen=True)
class LocalEnterprisePolicyMapper:
"""Map verified enterprise identity into Markitect's PolicySubject shape."""
policy_map: EnterprisePolicyMap
def map_subject(self, request: EnterprisePolicyMapRequest) -> PolicySubject:
identity = _identity_from_value(request.identity)
if self.policy_map.issuer and identity.issuer != self.policy_map.issuer:
raise EnterprisePolicyError(
f"Identity issuer `{identity.issuer}` does not match policy map issuer `{self.policy_map.issuer}`"
)
if self.policy_map.audiences:
if not set(identity.audience).intersection(self.policy_map.audiences):
raise EnterprisePolicyError(
f"Identity audience {identity.audience} does not match policy map audiences {self.policy_map.audiences}"
)
group_values = _unique(identity.groups + list(request.groups))
allowed_labels = _string_list(self.policy_map.defaults.get("allowed_labels"))
trust_zones = _string_list(self.policy_map.defaults.get("trust_zones"))
allowed_actions = _string_list(self.policy_map.defaults.get("actions") or self.policy_map.defaults.get("allowed_actions"))
attributes: dict[str, Any] = {
"policy_map_id": self.policy_map.id,
"matched_policy_rules": [],
"tenant": request.context.get("tenant"),
}
for rule_type, values, rules in (
("group", group_values, self.policy_map.groups),
("role", identity.roles, self.policy_map.roles),
("scope", identity.scopes, self.policy_map.scopes),
):
for value in values:
rule = rules.get(value)
if not rule:
continue
allowed_labels = _unique(allowed_labels + _string_list(rule.get("allowed_labels") or rule.get("labels")))
trust_zones = _unique(trust_zones + _string_list(rule.get("trust_zones") or rule.get("zones")))
allowed_actions = _unique(allowed_actions + _string_list(rule.get("actions") or rule.get("allowed_actions")))
attributes["matched_policy_rules"].append(f"{rule_type}:{value}")
attributes.update(dict(rule.get("attributes") or {}))
trust_zones = _unique(trust_zones + self._allowed_trust_zones(group_values, identity))
attributes["groups"] = group_values
attributes["scopes"] = identity.scopes
attributes["assurance"] = identity.assurance
return identity.to_policy_subject(
allowed_labels=allowed_labels,
trust_zones=trust_zones,
allowed_actions=allowed_actions,
attributes={key: value for key, value in attributes.items() if value not in (None, [], {})},
)
def _allowed_trust_zones(
self,
groups: list[str],
identity: EnterpriseIdentity,
) -> list[str]:
allowed: list[str] = []
group_set = set(groups)
role_set = set(identity.roles)
scope_set = set(identity.scopes)
for zone, rule in self.policy_map.trust_zones.items():
required_groups = set(_string_list(rule.get("required_groups")))
required_roles = set(_string_list(rule.get("required_roles")))
required_scopes = set(_string_list(rule.get("required_scopes")))
if required_groups and not required_groups.issubset(group_set):
continue
if required_roles and not required_roles.issubset(role_set):
continue
if required_scopes and not required_scopes.issubset(scope_set):
continue
if required_groups or required_roles or required_scopes:
allowed.append(zone)
return allowed
@dataclass(frozen=True)
class FlexAuthResource:
"""Resource record Markitect can register with flex-auth."""
id: str
type: str
path: str | None = None
parent: str | None = None
labels: list[str] = field(default_factory=list)
trust_zone: str | None = None
owner: str | None = None
attributes: dict[str, Any] = field(default_factory=dict)
@classmethod
def from_mapping(cls, raw: dict[str, Any]) -> "FlexAuthResource":
return cls(
id=str(raw["id"]),
type=str(raw.get("type", "document")),
path=raw.get("path"),
parent=raw.get("parent"),
labels=_string_list(raw.get("labels") or raw.get("label")),
trust_zone=raw.get("trust_zone"),
owner=raw.get("owner"),
attributes=dict(raw.get("attributes") or {}),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(asdict(self))
@dataclass(frozen=True)
class FlexAuthResourceManifest:
"""Manifest of Markitect resources intended for flex-auth registration."""
id: str
system: str = "markitect-tool"
resources: list[FlexAuthResource] = field(default_factory=list)
actions: list[str] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
@classmethod
def from_mapping(cls, raw: dict[str, Any]) -> "FlexAuthResourceManifest":
data = raw.get("manifest") if isinstance(raw.get("manifest"), dict) else raw
resources = [
FlexAuthResource.from_mapping(item)
for item in data.get("resources", [])
if isinstance(item, dict)
]
return cls(
id=str(data.get("id", "markitect-resources")),
system=str(data.get("system", "markitect-tool")),
resources=resources,
actions=_string_list(data.get("actions")),
metadata=dict(data.get("metadata") or {}),
)
@classmethod
def from_file(cls, path: str | Path) -> "FlexAuthResourceManifest":
data = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
raise EnterprisePolicyError("Flex-auth resource manifest must contain a mapping.")
return cls.from_mapping(data)
def to_dict(self) -> dict[str, Any]:
data = asdict(self)
data["resources"] = [resource.to_dict() for resource in self.resources]
return _drop_empty(data)
@dataclass
class LocalDecisionLogStore:
"""Local JSONL decision sink for development and tests."""
path: Path | None = None
_entries: dict[str, dict[str, Any]] = field(default_factory=dict, init=False)
def record(
self,
decision: PolicyDecision,
*,
context: dict[str, Any] | None = None,
) -> str:
payload = {
"decision": decision.to_dict(),
"context": _redact_sensitive_context(context or {}),
"recorded_by": "markitect.local-decision-log",
}
audit_id = "audit:" + hashlib.sha256(
json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str).encode("utf-8")
).hexdigest()
payload["audit_id"] = audit_id
self._entries[decision.decision_id] = payload
if self.path:
self.path.parent.mkdir(parents=True, exist_ok=True)
with self.path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str) + "\n")
return audit_id
def get(self, decision_id: str) -> dict[str, Any] | None:
if decision_id in self._entries:
return self._entries[decision_id]
if not self.path or not self.path.exists():
return None
with self.path.open(encoding="utf-8") as handle:
for line in handle:
entry = json.loads(line)
if entry.get("decision", {}).get("decision_id") == decision_id:
self._entries[decision_id] = entry
return entry
return None
def load_enterprise_identity_file(
path: str | Path,
*,
issuer: str | None = None,
audiences: list[str] | None = None,
environment: str | None = None,
) -> EnterpriseIdentity:
"""Load and validate deterministic NetKingdom/key-cape-style claims."""
claims = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {}
if not isinstance(claims, dict):
raise EnterprisePolicyError("Enterprise identity claims file must contain a mapping.")
adapter = NetKingdomIdentityClaimsAdapter(issuer=issuer, audiences=list(audiences or []))
return adapter.verify(claims, context={"environment": environment} if environment else {})
def load_enterprise_policy_subject(
claims_file: str | Path,
policy_map_file: str | Path,
*,
extra_groups: list[str] | None = None,
environment: str | None = None,
) -> PolicySubject:
"""Load claims and policy map files and return a gateway-ready subject."""
policy_map = EnterprisePolicyMap.from_file(policy_map_file)
identity = load_enterprise_identity_file(
claims_file,
issuer=policy_map.issuer,
audiences=policy_map.audiences,
environment=environment,
)
group_resolver = StaticDirectoryGroupResolver()
group_result = group_resolver.resolve(
DirectoryGroupResolutionRequest(
subject_id=identity.canonical_id,
issuer=identity.issuer,
groups=list(extra_groups or []),
claims=identity.claims | {"groups": identity.groups},
)
)
mapper = LocalEnterprisePolicyMapper(policy_map)
return mapper.map_subject(
EnterprisePolicyMapRequest(
identity=identity,
policy_map=policy_map.to_dict(),
groups=group_result.groups,
context={"environment": environment},
)
)
def _roles_from_claims(claims: dict[str, Any]) -> list[str]:
roles = _string_list(claims.get("roles"))
realm_access = claims.get("realm_access")
if isinstance(realm_access, dict):
roles += _string_list(realm_access.get("roles"))
resource_access = claims.get("resource_access")
if isinstance(resource_access, dict):
for client in resource_access.values():
if isinstance(client, dict):
roles += _string_list(client.get("roles"))
return _unique(roles)
def _scopes_from_claims(claims: dict[str, Any]) -> list[str]:
return _unique(_string_list(claims.get("scope")) + _string_list(claims.get("scp")))
def _assurance_from_claims(claims: dict[str, Any]) -> dict[str, Any]:
amr = _string_list(claims.get("amr"))
return {
"acr": claims.get("acr"),
"amr": amr,
"mfa": bool(claims.get("mfa") or {"otp", "mfa", "hwk"}.intersection(amr)),
}
def _principal_type(claims: dict[str, Any], roles: list[str]) -> str:
if claims.get("client_id") and "service" in roles:
return "service"
if claims.get("azp", "").startswith("svc-") or "service" in roles:
return "service"
return "human"
def _has_group_overage(claims: dict[str, Any]) -> bool:
return bool(claims.get("hasgroups") or claims.get("_claim_names", {}).get("groups"))
def _decode_unverified_jwt_payload(token: str) -> dict[str, Any]:
parts = token.split(".")
if len(parts) != 3:
raise EnterprisePolicyError("JWT fixture must have three segments.")
payload = parts[1] + "=" * (-len(parts[1]) % 4)
try:
decoded = base64.urlsafe_b64decode(payload.encode("ascii"))
claims = json.loads(decoded.decode("utf-8"))
except (ValueError, json.JSONDecodeError) as exc:
raise EnterprisePolicyError("JWT fixture payload is not valid JSON.") from exc
if not isinstance(claims, dict):
raise EnterprisePolicyError("JWT fixture payload must contain claims mapping.")
return claims
def _identity_from_value(value: EnterpriseIdentity | dict[str, Any]) -> EnterpriseIdentity:
if isinstance(value, EnterpriseIdentity):
return value
return EnterpriseIdentity(
issuer=str(value["issuer"]),
subject=str(value["subject"]),
identity_scheme=str(value.get("identity_scheme", "oidc")),
principal_type=str(value.get("principal_type", "human")),
audience=_string_list(value.get("audience")),
authorized_party=value.get("authorized_party"),
preferred_username=value.get("preferred_username"),
roles=_string_list(value.get("roles")),
scopes=_string_list(value.get("scopes")),
groups=_string_list(value.get("groups")),
assurance=dict(value.get("assurance") or {}),
directory=dict(value.get("directory") or {}),
claims=dict(value.get("claims") or {}),
provenance=dict(value.get("provenance") or {}),
)
def _int_claim(claims: dict[str, Any], key: str) -> int:
try:
return int(claims[key])
except (TypeError, ValueError) as exc:
raise EnterprisePolicyError(f"Claim `{key}` must be an integer timestamp.") from exc
def _rule_mapping(value: Any) -> dict[str, dict[str, Any]]:
if not isinstance(value, dict):
return {}
return {
str(key): dict(raw or {})
for key, raw in value.items()
if isinstance(raw, dict)
}
def _is_local_development_issuer(issuer: str) -> bool:
return any(marker in issuer for marker in ("localhost", "127.0.0.1", ".local", "dev.local"))
def _redact_sensitive_context(context: dict[str, Any]) -> dict[str, Any]:
redacted = dict(context)
for key in list(redacted):
if key.lower() in {"token", "access_token", "refresh_token", "assertion"}:
redacted[key] = "<redacted>"
return redacted
def _string_list(value: Any) -> list[str]:
if value is None:
return []
if isinstance(value, str):
return [item for item in value.split() if item]
if isinstance(value, list | tuple | set):
return [str(item) for item in value if item not in (None, "")]
return [str(value)]
def _unique(values: list[str]) -> list[str]:
seen: set[str] = set()
result: list[str] = []
for value in values:
normalized = str(value).strip()
key = normalized.lower()
if normalized and key not in seen:
result.append(normalized)
seen.add(key)
return result
def _drop_empty(data: dict[str, Any]) -> dict[str, Any]:
return {
key: value
for key, value in data.items()
if value not in (None, [], {}, "")
}