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

@@ -94,7 +94,12 @@ def test_builtin_policy_descriptor_exposes_cli_and_adapter_boundary():
assert {capability.id for capability in descriptor.capabilities} >= {
"policy",
"policy_filter",
"identity_claims",
"resource_manifest",
"decision_log",
}
assert "mkt policy check" in descriptor.cli["commands"]
assert "mkt policy subject" in descriptor.cli["commands"]
assert "mkt policy resource-manifest" in descriptor.cli["commands"]
assert "IdentityClaimsAdapter" in descriptor.metadata["external_adapters"]
assert "RelationshipPolicyAdapter" in descriptor.metadata["external_adapters"]

View File

@@ -7,11 +7,19 @@ from markitect_tool.cli import main
from markitect_tool.policy import (
DirectoryGroupResolution,
DirectoryGroupResolutionRequest,
EnterprisePolicyError,
EnterprisePolicyMap,
EnterpriseIdentity,
EnterprisePolicyMapRequest,
FlexAuthResourceManifest,
LocalDecisionLogStore,
LocalEnterprisePolicyMapper,
LocalLabelPolicy,
LocalLabelPolicyGateway,
NetKingdomIdentityClaimsAdapter,
StaticDirectoryGroupResolver,
)
from markitect_tool.policy.models import PolicyDecision
POLICY_TEXT = """id: example-policy
@@ -247,6 +255,203 @@ def test_enterprise_policy_adapter_requests_serialize_cleanly():
assert map_request.to_dict()["identity"]["canonical_id"].endswith("#user-123")
def test_netkingdom_claims_adapter_validates_required_claims_and_audience():
adapter = NetKingdomIdentityClaimsAdapter(
issuer="https://sso.example.test/realms/netkingdom",
audiences=["markitect-tool"],
)
identity = adapter.verify(_claims())
assert identity.canonical_id == "oidc:https://sso.example.test/realms/netkingdom#user-123"
assert identity.roles == ["viewer"]
assert identity.scopes == ["openid", "profile", "markitect:read"]
assert identity.groups == ["/markitect/readers"]
assert identity.assurance["mfa"] is True
def test_netkingdom_claims_adapter_rejects_local_issuer_in_production():
adapter = NetKingdomIdentityClaimsAdapter(audiences=["markitect-tool"])
claims = _claims() | {"iss": "http://localhost:8080/realms/dev"}
try:
adapter.verify(claims, context={"environment": "production"})
except EnterprisePolicyError as exc:
assert "Local development issuer" in str(exc)
else:
raise AssertionError("expected local production issuer rejection")
def test_static_group_resolver_reports_group_overage_and_freshness():
request = DirectoryGroupResolutionRequest(
subject_id="oidc:https://sso.example.test/realms/netkingdom#user-123",
issuer="https://sso.example.test/realms/netkingdom",
claims={"hasgroups": True},
)
resolver = StaticDirectoryGroupResolver(
groups_by_subject={request.subject_id: ["/markitect/readers"]},
refreshed_at="2026-05-04T10:00:00Z",
)
result = resolver.resolve(request)
assert result.groups == ["/markitect/readers"]
assert result.overage is True
assert result.refreshed_at == "2026-05-04T10:00:00Z"
def test_local_enterprise_policy_mapper_maps_groups_roles_and_scopes():
identity = NetKingdomIdentityClaimsAdapter(
issuer="https://sso.example.test/realms/netkingdom",
audiences=["markitect-tool"],
).verify(_claims())
policy_map = EnterprisePolicyMap.from_mapping(_enterprise_policy_map())
subject = LocalEnterprisePolicyMapper(policy_map).map_subject(
EnterprisePolicyMapRequest(identity=identity)
)
assert subject.allowed_labels == ["public", "internal"]
assert subject.trust_zones == ["public", "internal"]
assert subject.allowed_actions == ["read", "query", "search"]
assert subject.attributes["matched_policy_rules"] == [
"group:/markitect/readers",
"role:viewer",
"scope:markitect:read",
]
def test_flex_auth_resource_manifest_serializes_resources():
manifest = FlexAuthResourceManifest.from_mapping(
{
"id": "markitect-example",
"system": "markitect-tool",
"actions": ["read", "query"],
"resources": [
{
"id": "document:public-note",
"type": "document",
"path": "examples/policy/public-note.md",
"labels": ["public"],
"trust_zone": "public",
}
],
}
)
data = manifest.to_dict()
assert data["id"] == "markitect-example"
assert data["resources"][0]["id"] == "document:public-note"
def test_local_decision_log_store_redacts_token_context(tmp_path: Path):
store = LocalDecisionLogStore(tmp_path / "decisions.jsonl")
decision = PolicyDecision(
subject="subject-1",
action="query",
object_id="document:internal",
effect="deny",
reason="missing label",
)
audit_id = store.record(decision, context={"token": "secret", "policy_version": "v1"})
entry = store.get(decision.decision_id)
assert audit_id.startswith("audit:")
assert entry is not None
assert entry["context"]["token"] == "<redacted>"
assert entry["context"]["policy_version"] == "v1"
def test_mkt_policy_subject_maps_claims_file_to_subject(tmp_path: Path):
claims_file = tmp_path / "claims.yaml"
map_file = tmp_path / "policy-map.yaml"
claims_file.write_text(yaml_dump(_claims()), encoding="utf-8")
map_file.write_text(yaml_dump(_enterprise_policy_map()), encoding="utf-8")
result = CliRunner().invoke(
main,
[
"policy",
"subject",
str(claims_file),
"--policy-map",
str(map_file),
"--format",
"json",
],
)
data = json.loads(result.output)
assert result.exit_code == 0
assert data["subject"]["allowed_labels"] == ["public", "internal"]
assert data["subject"]["allowed_actions"] == ["read", "query", "search"]
def test_mkt_policy_resource_manifest_inspects_manifest(tmp_path: Path):
manifest_file = tmp_path / "resources.yaml"
manifest_file.write_text(
yaml_dump(
{
"id": "manifest-1",
"system": "markitect-tool",
"actions": ["read"],
"resources": [{"id": "document:one", "type": "document"}],
}
),
encoding="utf-8",
)
result = CliRunner().invoke(
main,
["policy", "resource-manifest", str(manifest_file), "--format", "json"],
)
data = json.loads(result.output)
assert result.exit_code == 0
assert data["manifest"]["resources"][0]["id"] == "document:one"
def yaml_dump(value: dict) -> str:
import yaml
return yaml.safe_dump(value, sort_keys=False)
def _claims() -> dict:
return {
"iss": "https://sso.example.test/realms/netkingdom",
"sub": "user-123",
"aud": ["markitect-tool"],
"exp": 4102444800,
"iat": 1767225600,
"preferred_username": "ada",
"scope": "openid profile markitect:read",
"realm_access": {"roles": ["viewer"]},
"groups": ["/markitect/readers"],
"amr": ["pwd", "otp"],
}
def _enterprise_policy_map() -> dict:
return {
"id": "markitect-enterprise-policy-map",
"issuer": "https://sso.example.test/realms/netkingdom",
"audiences": ["markitect-tool"],
"defaults": {"allowed_labels": ["public"], "trust_zones": ["public"]},
"groups": {
"/markitect/readers": {
"allowed_labels": ["internal"],
"trust_zones": ["internal"],
"actions": ["read", "query", "search"],
}
},
"roles": {"viewer": {"actions": ["read", "query", "search"]}},
"scopes": {"markitect:read": {"actions": ["read", "query", "search"]}},
"trust_zones": {"internal": {"required_groups": ["/markitect/readers"]}},
}
def _policy_mapping() -> dict:
return {
"id": "example-policy",

View File

@@ -83,6 +83,39 @@ def test_load_workflow_file_preserves_standard_sections(tmp_path: Path):
assert plan.steps[0]["id"] == "render"
def test_load_workflow_file_preserves_policy_identity_permissions(tmp_path: Path):
workflow = tmp_path / "policy.workflow.md"
workflow.write_text(
"""# Policy Workflow
```yaml workflow
metadata:
id: policy-aware
inputs:
static:
value: ok
permissions:
policy:
subject_from_token: examples/policy/netkingdom-claims.yaml
policy_map: examples/policy/enterprise-policy-map.yaml
required_assurance:
mfa: true
emergency_justification: INC-123
decision_log: .markitect/policy-decisions.jsonl
flex_auth:
resource_manifest: examples/policy/flex-auth-resource-manifest.yaml
```
""",
encoding="utf-8",
)
plan = load_workflow_file(workflow)
assert plan.permissions["policy"]["subject_from_token"] == "examples/policy/netkingdom-claims.yaml"
assert plan.permissions["policy"]["required_assurance"]["mfa"] is True
assert plan.permissions["flex_auth"]["resource_manifest"].endswith("flex-auth-resource-manifest.yaml")
def test_workflow_runner_collects_sources_and_renders_output(tmp_path: Path):
workflow = _write_workflow_fixture(tmp_path)
plan = load_workflow_file(workflow)