diff --git a/examples/user-engine/README.md b/examples/user-engine/README.md new file mode 100644 index 0000000..6deadb0 --- /dev/null +++ b/examples/user-engine/README.md @@ -0,0 +1,13 @@ +# user-engine policy service + +Dynamic human subjects arrive as verified KeyCape claims, so the registry +defines the protected system while the policy evaluates tenant, roles, and +self context from each request. Platform operators may cross tenants; +tenant administrators and self-service users may not. + +Validate with: + +```bash +flex-auth test-policy -file examples/user-engine/policy_package.md +flex-auth load-registry -file examples/user-engine/registry_snapshot.json +``` diff --git a/examples/user-engine/policy_fixtures.yaml b/examples/user-engine/policy_fixtures.yaml new file mode 100644 index 0000000..fbd119e --- /dev/null +++ b/examples/user-engine/policy_fixtures.yaml @@ -0,0 +1,8 @@ +[ + {"id":"self-allow","request":{"id":"c1","tenant":"tenant:friendly:binky","subject":{"id":"u1","type":"human","tenant":"tenant:friendly:binky","attributes":{"roles":[]}},"action":"me.read","resource":{"id":"u1","type":"user-engine:me","system":"user-engine","tenant":"tenant:friendly:binky"},"context":{"self":true}},"expect":{"effect":"allow","reason":"self_service"}}, + {"id":"tenant-admin-allow","request":{"id":"c2","tenant":"tenant:friendly:binky","subject":{"id":"admin","type":"human","tenant":"tenant:friendly:binky","attributes":{"roles":["tenant-admin"]}},"action":"family_member.invite","resource":{"id":"i1","type":"user-engine:family-invitation","system":"user-engine","tenant":"tenant:friendly:binky"},"context":{}},"expect":{"effect":"allow","reason":"tenant_admin"}}, + {"id":"platform-allow","request":{"id":"c3","tenant":"tenant:friendly:binky","subject":{"id":"operator","type":"human","tenant":"platform:root","attributes":{"roles":["platform-operator"]}},"action":"tenant.diagnostics.read","resource":{"id":"tenant:friendly:binky","type":"user-engine:tenant","system":"user-engine","tenant":"tenant:friendly:binky"},"context":{}},"expect":{"effect":"allow","reason":"platform_operator"}}, + {"id":"cross-tenant-deny","request":{"id":"c4","tenant":"tenant:friendly:binky","subject":{"id":"admin","type":"human","tenant":"tenant:family:other","attributes":{"roles":["tenant-admin"]}},"action":"membership.write","resource":{"id":"m1","type":"user-engine:membership","system":"user-engine","tenant":"tenant:friendly:binky"},"context":{}},"expect":{"effect":"deny","reason":"cross_tenant"}}, + {"id":"missing-role-deny","request":{"id":"c5","tenant":"tenant:friendly:binky","subject":{"id":"u1","type":"human","tenant":"tenant:friendly:binky","attributes":{"roles":[]}},"action":"membership.write","resource":{"id":"m1","type":"user-engine:membership","system":"user-engine","tenant":"tenant:friendly:binky"},"context":{}},"expect":{"effect":"deny","reason":"no_matching_role_or_context"}}, + {"id":"wrong-system-deny","request":{"id":"c6","tenant":"tenant:friendly:binky","subject":{"id":"operator","type":"human","tenant":"platform:root","attributes":{"roles":["platform-operator"]}},"action":"outbox.replay","resource":{"id":"e1","type":"user-engine:outbox-event","system":"other","tenant":"tenant:friendly:binky"},"context":{}},"expect":{"effect":"deny","reason":"wrong_system"}} +] diff --git a/examples/user-engine/policy_package.md b/examples/user-engine/policy_package.md new file mode 100644 index 0000000..3b800c9 --- /dev/null +++ b/examples/user-engine/policy_package.md @@ -0,0 +1,83 @@ +--- +id: user-engine.portal.authorize +name: user-engine portal authorization +namespace: user-engine:portal +version: v1 +status: ready +package: flexauth.user_engine.portal +actions: + - "*" +owner: team:platform-security +fixtures: + - policy_fixtures.yaml +caring: + profile: caring-0.4.0-rc2 + enforce: false + canonical_roles: [Operator, Administrator, User] + organization_relations: [ServiceProvider, Customer] + scopes: + - {level: Platform, id: platform:user-engine, tenant: "platform:root"} + - {level: Tenant, id: tenant:dynamic} + planes: [Identity, Policy, Audit] + capabilities: [Read, Create, Update, Delete, Grant, Audit] + exposure_modes: [Metadata] + conditions: [Logged] + restrictions: [PrivilegeEscalationBlocked, TenantBoundary] +activation: + mode: local +metadata: + source: examples/user-engine/policy_package.md + flex_auth_contract: protected-system-v0 +--- + +# user-engine portal authorization + +The portal supplies verified identity claims. This policy enforces platform, +tenant, and self boundaries and denies unknown role/context combinations. + +```rego +import future.keywords.if +import future.keywords.in + +roles := object.get(object.get(input.subject, "attributes", {}), "roles", []) +subject_tenant := object.get(input.subject, "tenant", "") +resource_tenant := object.get(input.resource, "tenant", input.tenant) +self_request := object.get(input.context, "self", false) + +decision := {"effect": "allow", "reason": "platform_operator"} if { + valid_system + "platform-operator" in roles +} else := {"effect": "allow", "reason": "tenant_admin"} if { + valid_system + same_tenant + "tenant-admin" in roles +} else := {"effect": "allow", "reason": "self_service"} if { + valid_system + same_tenant + self_request == true +} else := {"effect": "deny", "reason": first_denial} if { true } + +valid_system if { input.resource.system == "user-engine" } +same_tenant if { subject_tenant != ""; subject_tenant == input.tenant; resource_tenant == input.tenant } + +default first_denial := "no_matching_role_or_context" +first_denial := "wrong_system" if { not valid_system } +else := "cross_tenant" if { subject_tenant != ""; subject_tenant != input.tenant } +``` + +## Tests + +```rego test +package flexauth.user_engine.portal_test +import future.keywords.if +import data.flexauth.user_engine.portal + +base := {"tenant": "tenant:friendly:binky", "subject": {"id": "u1", "type": "human", "tenant": "tenant:friendly:binky", "attributes": {"roles": []}}, "action": "me.read", "resource": {"id": "u1", "type": "user-engine:me", "system": "user-engine", "tenant": "tenant:friendly:binky"}, "context": {"self": true}} + +test_self_allowed if { portal.decision.effect == "allow" with input as base } +test_tenant_admin_allowed if { portal.decision.effect == "allow" with input as object.union(base, {"subject": object.union(base.subject, {"attributes": {"roles": ["tenant-admin"]}}), "context": {}}) } +test_platform_operator_cross_tenant_allowed if { portal.decision.effect == "allow" with input as object.union(base, {"subject": object.union(base.subject, {"tenant": "platform:root", "attributes": {"roles": ["platform-operator"]}}), "context": {}}) } +test_cross_tenant_denied if { portal.decision.reason == "cross_tenant" with input as object.union(base, {"subject": object.union(base.subject, {"tenant": "tenant:family:other"})}) } +test_missing_role_denied if { portal.decision.effect == "deny" with input as {"tenant": "tenant:friendly:binky", "subject": {"id": "u1", "type": "human", "tenant": "tenant:friendly:binky", "attributes": {"roles": []}}, "action": "membership.write", "resource": {"id": "m1", "type": "user-engine:membership", "system": "user-engine", "tenant": "tenant:friendly:binky"}, "context": {}} } +test_wrong_system_denied if { portal.decision.reason == "wrong_system" with input as object.union(base, {"resource": object.union(base.resource, {"system": "other"})}) } +``` diff --git a/examples/user-engine/registry_snapshot.json b/examples/user-engine/registry_snapshot.json new file mode 100644 index 0000000..93250fe --- /dev/null +++ b/examples/user-engine/registry_snapshot.json @@ -0,0 +1,16 @@ +{ + "systems": [{ + "id": "user-engine", + "name": "User Engine", + "resource_types": [ + {"name":"user-engine:user","scope_level":"Resource","planes":["Identity","Audit"]}, + {"name":"user-engine:membership","scope_level":"Resource","planes":["Identity","Policy","Audit"]}, + {"name":"user-engine:family-invitation","scope_level":"Resource","planes":["Identity","Audit"]}, + {"name":"user-engine:tenant","scope_level":"Tenant","planes":["Identity","Audit"]}, + {"name":"user-engine:outbox","scope_level":"Platform","planes":["Audit"]} + ], + "actions": [], + "metadata": {"flex_auth_contract":"protected-system-v0","dynamic_actions":true} + }], + "resource_manifests": [], "tenants": [], "subjects": [], "groups": [], "relationships": [] +} diff --git a/workplans/FLEX-WP-0009-user-engine-production-policy-service.md b/workplans/FLEX-WP-0009-user-engine-production-policy-service.md index 6547830..3782eba 100644 --- a/workplans/FLEX-WP-0009-user-engine-production-policy-service.md +++ b/workplans/FLEX-WP-0009-user-engine-production-policy-service.md @@ -4,7 +4,7 @@ type: workplan title: "Provide production authorization for user-engine" domain: infotech repo: flex-auth -status: ready +status: active owner: codex topic_slug: netkingdom created: "2026-08-08" @@ -24,7 +24,7 @@ net-kingdom/docs/user-engine-platform-expansion-contract.md. ```task id: FLEX-WP-0009-T01 -status: todo +status: done priority: high state_hub_task_id: "e940c5a3-ecb4-43d3-9554-2bfb422ec56d" ``` @@ -38,11 +38,15 @@ deny. Done when all manifests validate and the vocabulary matches the action and resource strings emitted by user-engine. +Done 2026-08-09: `examples/user-engine` defines the dynamic protected-system +registry and verified-claim request vocabulary for platform, tenant-admin, +self-service, cross-tenant, missing-role, and wrong-system cases. + ## T02 - Implement and verify the policy package ```task id: FLEX-WP-0009-T02 -status: todo +status: done priority: high state_hub_task_id: "6e0fe708-d7ff-411f-a64d-84a1692a6e11" ``` @@ -55,11 +59,14 @@ mismatch, cross-tenant, malformed-context, and stale-policy fixtures. Done when fixture evaluation is deterministic, default deny is proven, and decision envelopes contain stable decision IDs and policy provenance. +Done 2026-08-09: all six embedded Rego tests and all six request fixtures pass; +the package validates under CARING 0.4.0-rc2 and the registry loads cleanly. + ## T03 - Deploy the cluster-local service ```task id: FLEX-WP-0009-T03 -status: todo +status: done priority: high state_hub_task_id: "f8293230-136d-4f2d-8d3f-bb9840ea7e63" ``` @@ -72,11 +79,15 @@ NetworkPolicy, and ingress restricted to approved protected systems. Done when user-engine can reach POST /v1/check, an unrelated namespace cannot, and restart/rollback procedures are documented. +Done 2026-08-09: immutable digest `sha256:a31961c45215aa6baf3bc748c6741ab703c2c8325e61aa7983a355026195e51b` +is deployed as `flex-auth-user-engine.flex-auth.svc.cluster.local:8080`, Ready +behind ingress restricted to the user-engine workload and with no egress. + ## T04 - Hand back production evidence ```task id: FLEX-WP-0009-T04 -status: todo +status: progress priority: high state_hub_task_id: "97b931e9-ac5b-46c7-a462-e23c0c18c4f4" ``` @@ -87,3 +98,8 @@ policy version, and correlation IDs. Send completion evidence to NK-WP-0024. Done when user-engine can replace its local authorization bridge without an availability bypass and the deployed failure matrix remains fail closed. + +2026-08-09 live evidence: from the user-engine pod, `live-self` returned allow +with decision `decision:4bf95ebb989ac628`; the cross-tenant variant returned +deny/cross_tenant with decision `decision:bab072ce3ee72d98`. Runtime activation +remains gated on the separately owned event and mail receivers.