chore: sync GovOPlaN module split state

This commit is contained in:
2026-07-10 12:51:21 +02:00
parent 18e6c3eb9b
commit 13fd7fc3bd
50 changed files with 12678 additions and 1109 deletions
@@ -0,0 +1,310 @@
from __future__ import annotations
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from fnmatch import fnmatchcase
from typing import Any
from govoplan_core.core.policy import PolicyDecision, PolicySourceStep, normalize_policy_scope_type
_ALLOW_KEYS = ("allow", "allowlist", "whitelist")
_DENY_KEYS = ("deny", "denylist", "blacklist")
_FIELD_ALIASES = {
"connectors": ("connectors", "connector_ids", "connector_id"),
"credentials": ("credentials", "credential_ids", "credential_id"),
"providers": ("providers", "provider"),
"external_ids": ("external_ids", "external_id"),
"external_paths": ("external_paths", "paths", "path_prefixes", "external_path"),
"external_urls": ("external_urls", "urls", "external_url"),
}
CONNECTOR_POLICY_FIELDS = tuple(_FIELD_ALIASES)
class ConnectorPolicyDenied(RuntimeError):
def __init__(self, decision: PolicyDecision) -> None:
super().__init__(decision.reason or "Connector policy denied")
self.decision = decision
@dataclass(frozen=True, slots=True)
class ConnectorAccessRequest:
connector_id: str | None = None
credential_id: str | None = None
provider: str | None = None
external_id: str | None = None
external_path: str | None = None
external_url: str | None = None
operation: str = "access"
@classmethod
def from_provenance(cls, value: Mapping[str, Any] | None, *, operation: str = "access") -> "ConnectorAccessRequest":
payload = value or {}
return cls(
connector_id=_clean(payload.get("connector_id")),
credential_id=_clean(payload.get("credential_id") or payload.get("connector_credential_id")),
provider=_clean(payload.get("provider") or payload.get("source_type")),
external_id=_clean(payload.get("external_id")),
external_path=_clean(payload.get("external_path")),
external_url=_clean(payload.get("external_url")),
operation=_clean(operation) or "access",
)
def to_dict(self) -> dict[str, str | None]:
return {
"connector_id": self.connector_id,
"credential_id": self.credential_id,
"provider": self.provider,
"external_id": self.external_id,
"external_path": self.external_path,
"external_url": self.external_url,
"operation": self.operation,
}
@dataclass(frozen=True, slots=True)
class ConnectorPolicySource:
scope_type: str
label: str
scope_id: str | None = None
policy: Mapping[str, Any] | None = None
def source_step(self, applied_fields: Iterable[str]) -> PolicySourceStep:
return PolicySourceStep(
scope_type=normalize_policy_scope_type(self.scope_type),
scope_id=self.scope_id,
label=self.label,
applied_fields=tuple(dict.fromkeys(applied_fields)),
policy=dict(self.policy or {}),
)
def connector_policy_sources_from_payload(value: object) -> list[ConnectorPolicySource]:
if value is None or value == "":
return []
if isinstance(value, Mapping):
raw_sources = value.get("sources")
if raw_sources is None:
raw_sources = [value]
elif isinstance(value, list):
raw_sources = value
else:
raise ValueError("connector_policy must be a JSON object or list")
if not isinstance(raw_sources, list):
raise ValueError("connector_policy.sources must be a list")
return [_source_from_mapping(item) for item in raw_sources if isinstance(item, Mapping)]
def connector_policy_applied_fields(policy: Mapping[str, Any] | None) -> tuple[str, ...]:
return _applied_fields(_policy_mapping(policy))
def filtered_connector_policy_source(source: ConnectorPolicySource, fields: Iterable[str]) -> ConnectorPolicySource:
allowed_fields = {field for field in fields if field in _FIELD_ALIASES}
if not allowed_fields:
return ConnectorPolicySource(scope_type=source.scope_type, scope_id=source.scope_id, label=source.label, policy={})
policy = _policy_mapping(source.policy)
filtered: dict[str, Any] = {}
for prefix, keys in (("allow", _ALLOW_KEYS), ("deny", _DENY_KEYS)):
rules = _merged_rule(policy, keys)
selected = {field: _string_list(rules.get(field)) for field in allowed_fields}
selected = {field: values for field, values in selected.items() if values}
if selected:
filtered[prefix] = selected
return ConnectorPolicySource(scope_type=source.scope_type, scope_id=source.scope_id, label=source.label, policy=filtered)
def connector_policy_sources_for_fields(
sources: Iterable[ConnectorPolicySource],
fields: Iterable[str],
) -> list[ConnectorPolicySource]:
return [filtered_connector_policy_source(source, fields) for source in sources]
def connector_policy_decision(
request: ConnectorAccessRequest,
sources: Iterable[ConnectorPolicySource],
) -> PolicyDecision:
source_list = list(sources)
source_steps: list[PolicySourceStep] = []
deny_matches: list[dict[str, Any]] = []
allow_misses: list[dict[str, Any]] = []
for source in source_list:
policy = _policy_mapping(source.policy)
applied_fields = _applied_fields(policy)
if applied_fields:
source_steps.append(source.source_step(applied_fields))
deny_policy = _merged_rule(policy, _DENY_KEYS)
for field, patterns in _rules_by_field(deny_policy).items():
if _matches_field(request, field, patterns):
deny_matches.append({
"scope": source.source_step((f"deny.{field}",)).to_dict(),
"field": field,
"patterns": patterns,
})
allow_policy = _merged_rule(policy, _ALLOW_KEYS)
for field, patterns in _rules_by_field(allow_policy).items():
if not _matches_field(request, field, patterns):
allow_misses.append({
"scope": source.source_step((f"allow.{field}",)).to_dict(),
"field": field,
"patterns": patterns,
})
details = {
"request": request.to_dict(),
"deny_matches": deny_matches,
"allow_misses": allow_misses,
}
if deny_matches:
return PolicyDecision(
allowed=False,
reason="Connector access is denied by a blacklist rule.",
source_path=tuple(source_steps),
requirements=("connector_policy_denylist",),
details=details,
)
if allow_misses:
return PolicyDecision(
allowed=False,
reason="Connector access is not allowed by the configured whitelist.",
source_path=tuple(source_steps),
requirements=("connector_policy_allowlist",),
details=details,
)
return PolicyDecision(
allowed=True,
reason=None,
source_path=tuple(source_steps),
requirements=(),
details=details,
)
def ensure_connector_policy_allows(
request: ConnectorAccessRequest,
sources: Iterable[ConnectorPolicySource],
) -> PolicyDecision:
decision = connector_policy_decision(request, sources)
if not decision.allowed:
raise ConnectorPolicyDenied(decision)
return decision
def _source_from_mapping(value: Mapping[str, Any]) -> ConnectorPolicySource:
scope_type = normalize_policy_scope_type(str(value.get("scope_type") or "system"))
scope_id = _clean(value.get("scope_id"))
if scope_type == "system":
scope_id = None
elif not scope_id:
raise ValueError(f"{scope_type} connector policy sources require scope_id")
label = _clean(value.get("label")) or scope_type.capitalize()
policy = value.get("policy")
if policy is None:
policy = {key: value[key] for key in (*_ALLOW_KEYS, *_DENY_KEYS) if key in value}
if policy is not None and not isinstance(policy, Mapping):
raise ValueError("connector policy source policy must be a JSON object")
return ConnectorPolicySource(scope_type=scope_type, scope_id=scope_id, label=label, policy=policy or {})
def _policy_mapping(value: Mapping[str, Any] | None) -> Mapping[str, Any]:
return value if isinstance(value, Mapping) else {}
def _merged_rule(policy: Mapping[str, Any], keys: Iterable[str]) -> dict[str, Any]:
merged: dict[str, Any] = {}
for key in keys:
raw = policy.get(key)
if isinstance(raw, Mapping):
merged.update(raw)
return merged
def _rules_by_field(value: Mapping[str, Any]) -> dict[str, list[str]]:
result: dict[str, list[str]] = {}
for field, aliases in _FIELD_ALIASES.items():
items: list[str] = []
for alias in aliases:
items.extend(_string_list(value.get(alias)))
if items:
result[field] = list(dict.fromkeys(items))
return result
def _applied_fields(policy: Mapping[str, Any]) -> tuple[str, ...]:
fields: list[str] = []
for prefix, keys in (("allow", _ALLOW_KEYS), ("deny", _DENY_KEYS)):
rules = _merged_rule(policy, keys)
fields.extend(f"{prefix}.{field}" for field in _rules_by_field(rules))
return tuple(dict.fromkeys(fields))
def _matches_field(request: ConnectorAccessRequest, field: str, patterns: list[str]) -> bool:
if field == "connectors":
return _matches_exact(request.connector_id, patterns)
if field == "credentials":
return _matches_exact(request.credential_id, patterns)
if field == "providers":
return _matches_exact(request.provider, patterns)
if field == "external_ids":
return _matches_glob(request.external_id, patterns)
if field == "external_paths":
return _matches_path(request.external_path, patterns)
if field == "external_urls":
return _matches_glob(request.external_url, patterns)
return False
def _matches_exact(value: str | None, patterns: list[str]) -> bool:
if value is None:
return False
clean = value.casefold()
return any(pattern == "*" or clean == pattern.casefold() for pattern in patterns)
def _matches_glob(value: str | None, patterns: list[str]) -> bool:
if value is None:
return False
clean = value.casefold()
return any(fnmatchcase(clean, pattern.casefold()) for pattern in patterns)
def _matches_path(value: str | None, patterns: list[str]) -> bool:
if value is None:
return False
clean = value.replace("\\", "/").strip()
for pattern in patterns:
normalized = pattern.replace("\\", "/").strip()
if normalized == "*":
return True
if any(token in normalized for token in "*?[]"):
if fnmatchcase(clean.casefold(), normalized.casefold()):
return True
continue
if clean.casefold() == normalized.casefold() or clean.casefold().startswith(normalized.rstrip("/").casefold() + "/"):
return True
return False
def _string_list(value: object) -> list[str]:
if value is None:
return []
if isinstance(value, str):
values = [value]
elif isinstance(value, Iterable) and not isinstance(value, (bytes, bytearray, Mapping)):
values = [str(item) for item in value]
else:
values = [str(value)]
return [item.strip() for item in values if item.strip()]
def _clean(value: object) -> str | None:
if value is None:
return None
text = str(value).strip()
return text or None