diff --git a/examples/qonto-assistant/README.md b/examples/qonto-assistant/README.md new file mode 100644 index 0000000..3292184 --- /dev/null +++ b/examples/qonto-assistant/README.md @@ -0,0 +1,65 @@ +# qonto-assistant Consumer Integration Fixtures + +`QONTO-WP-0004-T04`. Registers `qonto-assistant` as a flex-auth +protected-system consumer, gating its `finance.qonto.read` capability +(the live-authorization half of `docs/SecurityPractice.md` — the other half +is a `tenant-engine` capability-role/plan liveness check, deliberately kept +separate; see `policy_package.md`'s scope note). + +## Files + +| File | Purpose | +| --- | --- | +| `protected_system_manifest.yaml` | Resource type (`finance-snapshot`) and action (`finance.qonto.read`) | +| `subject_manifest.yaml` | Registered callers: an agent-harness session identity and the founder's human identity, both in `group:qonto-assistant-readers` | +| `policy_package.md` | Rego rules + embedded tests gating the read action | +| `policy_fixtures.yaml` | Allow/deny request/decision pairs, referenced by `policy_package.md`'s frontmatter | +| `registry_snapshot.json` | Merged `systems`/`subjects`/`groups` snapshot assembled from the two manifests above, loadable by `flex-auth serve`/`check`/`load-registry` | +| `check_request_allow_read.json`, `check_request_deny_wrong_tenant.json` | Standalone example requests for `flex-auth check` | + +**No `resource_manifest.yaml`** — like tenant-engine's tenants, `qonto-assistant` +doesn't have a fixed enumerable resource inventory to register; the single +`finance-snapshot` resource type is declared on the protected-system +manifest and that is sufficient for this policy's coarse per-actor/per-tenant +gate. + +## Verified + +```bash +go build -o bin/flex-auth ./cmd/flex-auth + +# Rego rules + embedded tests + fixtures, all pass: +bin/flex-auth test-policy -file examples/qonto-assistant/policy_package.md + +# Registry loads cleanly: +bin/flex-auth load-registry -file examples/qonto-assistant/registry_snapshot.json + +# Individual requests via the CLI: +bin/flex-auth check \ + -registry examples/qonto-assistant/registry_snapshot.json \ + -policy examples/qonto-assistant/policy_package.md \ + -request examples/qonto-assistant/check_request_allow_read.json + +# End-to-end over real HTTP: a live `flex-auth serve` loaded with this exact +# registry+policy, hit by qonto-assistant's actual FlexAuthCheckClient (not +# a mock) -- see qonto-assistant/tests/test_flex_auth_client.py and +# tests/test_live_authorization_gate.py, and the manual curl walkthrough +# below: +bin/flex-auth serve -addr 127.0.0.1:9099 \ + -registry examples/qonto-assistant/registry_snapshot.json \ + -policy examples/qonto-assistant/policy_package.md +# from another shell: +curl -s -X POST http://127.0.0.1:9099/v1/check -H "Content-Type: application/json" \ + -d @examples/qonto-assistant/check_request_allow_read.json +# -> effect: allow, reason: finance_read_policy_matched +curl -s -X POST http://127.0.0.1:9099/v1/check -H "Content-Type: application/json" \ + -d @examples/qonto-assistant/check_request_deny_wrong_tenant.json +# -> effect: deny, reason: wrong_tenant +``` + +## Related + +- `qonto-assistant/docs/SecurityPractice.md` +- `qonto-assistant/workplans/QONTO-WP-0004-security-hardening-and-scale-to-zero.md` +- `examples/tenant-engine/` — the pattern this was modeled on +- `net-kingdom/docs/adr/ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md` diff --git a/examples/qonto-assistant/check_request_allow_read.json b/examples/qonto-assistant/check_request_allow_read.json new file mode 100644 index 0000000..a47ab07 --- /dev/null +++ b/examples/qonto-assistant/check_request_allow_read.json @@ -0,0 +1,15 @@ +{ + "id": "check:qonto-assistant-read-t1", + "tenant": "tenant:friendly:binky", + "subject": { + "id": "agent-harness-binky", + "type": "agent" + }, + "action": "finance.qonto.read", + "resource": { + "id": "finance-snapshot", + "type": "finance-snapshot", + "system": "qonto-assistant" + }, + "context": {} +} diff --git a/examples/qonto-assistant/check_request_deny_wrong_tenant.json b/examples/qonto-assistant/check_request_deny_wrong_tenant.json new file mode 100644 index 0000000..1cc6b0a --- /dev/null +++ b/examples/qonto-assistant/check_request_deny_wrong_tenant.json @@ -0,0 +1,15 @@ +{ + "id": "check:qonto-assistant-read-t6", + "tenant": "tenant:friendly:some-other-company", + "subject": { + "id": "agent-harness-binky", + "type": "agent" + }, + "action": "finance.qonto.read", + "resource": { + "id": "finance-snapshot", + "type": "finance-snapshot", + "system": "qonto-assistant" + }, + "context": {} +} diff --git a/examples/qonto-assistant/policy_fixtures.yaml b/examples/qonto-assistant/policy_fixtures.yaml new file mode 100644 index 0000000..5f23df4 --- /dev/null +++ b/examples/qonto-assistant/policy_fixtures.yaml @@ -0,0 +1,74 @@ +[ + { + "id": "fixture:qonto-assistant-agent-read-allow", + "request": { + "id": "check:qonto-assistant-read-t1", + "tenant": "tenant:friendly:binky", + "subject": {"id": "agent-harness-binky", "type": "agent"}, + "action": "finance.qonto.read", + "resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"}, + "context": {} + }, + "expect": {"effect": "allow", "reason": "finance_read_policy_matched"} + }, + { + "id": "fixture:qonto-assistant-human-read-allow", + "request": { + "id": "check:qonto-assistant-read-t2", + "tenant": "tenant:friendly:binky", + "subject": {"id": "bernd.worsch", "type": "human"}, + "action": "finance.qonto.read", + "resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"}, + "context": {} + }, + "expect": {"effect": "allow", "reason": "finance_read_policy_matched"} + }, + { + "id": "fixture:qonto-assistant-wrong-system-deny", + "request": { + "id": "check:qonto-assistant-read-t3", + "tenant": "tenant:friendly:binky", + "subject": {"id": "agent-harness-binky", "type": "agent"}, + "action": "finance.qonto.read", + "resource": {"id": "x", "type": "finance-snapshot", "system": "some-other-system"}, + "context": {} + }, + "expect": {"effect": "deny", "reason": "wrong_system"} + }, + { + "id": "fixture:qonto-assistant-unknown-action-deny", + "request": { + "id": "check:qonto-assistant-read-t4", + "tenant": "tenant:friendly:binky", + "subject": {"id": "agent-harness-binky", "type": "agent"}, + "action": "finance.qonto.transfer", + "resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"}, + "context": {} + }, + "expect": {"effect": "deny", "reason": "unknown_action"} + }, + { + "id": "fixture:qonto-assistant-wrong-subject-type-deny", + "request": { + "id": "check:qonto-assistant-read-t5", + "tenant": "tenant:friendly:binky", + "subject": {"id": "unknown-device", "type": "device"}, + "action": "finance.qonto.read", + "resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"}, + "context": {} + }, + "expect": {"effect": "deny", "reason": "wrong_subject_type"} + }, + { + "id": "fixture:qonto-assistant-wrong-tenant-deny", + "request": { + "id": "check:qonto-assistant-read-t6", + "tenant": "tenant:friendly:some-other-company", + "subject": {"id": "agent-harness-binky", "type": "agent"}, + "action": "finance.qonto.read", + "resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"}, + "context": {} + }, + "expect": {"effect": "deny", "reason": "wrong_tenant"} + } +] diff --git a/examples/qonto-assistant/policy_package.md b/examples/qonto-assistant/policy_package.md new file mode 100644 index 0000000..e7f02a3 --- /dev/null +++ b/examples/qonto-assistant/policy_package.md @@ -0,0 +1,167 @@ +--- +id: qonto-assistant.finance-read +name: qonto-assistant finance.qonto.read authorization +namespace: qonto-assistant:finance +version: v1 +status: ready +package: flexauth.qonto_assistant.finance_read +actions: + - finance.qonto.read +owner: team:platform-security +fixtures: + - policy_fixtures.yaml +caring: + profile: caring-0.4.0-rc2 + enforce: false + canonical_roles: + - Operator + organization_relations: + - ServiceProvider + - Customer + scopes: + - level: Tenant + id: tenant:friendly:binky + tenant: tenant:friendly:binky + planes: + - Data + - Audit + capabilities: + - View + - Audit + exposure_modes: + - Masked + conditions: + - Logged + restrictions: + - PrivilegeEscalationBlocked +activation: + mode: local +metadata: + source: examples/qonto-assistant/policy_package.md + flex_auth_contract: protected-system-v0 +--- + +# qonto-assistant finance.qonto.read authorization + +This package authorizes `qonto-assistant`'s read surface +(`QONTO-WP-0004-T04`'s live authorization gate). `qonto-assistant` keeps +custody of the bank credential and its own default-deny policy kernel +(spend/transfer/card/write tools are hard-denied there and never reach this +policy); flex-auth decides whether a specific *actor* may use the +`finance.qonto.read` capability at all. + +**Scope note:** this policy governs *who may call `finance.qonto.read`* (an +actor/tenant question) — it does not evaluate a *tenant's* capability roles +or plan status (`PLTF`/`IAM`/`VEN`/`CUS`, ADR-0014). Those are tenant state +`qonto-assistant` checks separately via `tenant-engine`'s live-lookup +endpoint (`GET /tenants/{id}/roles/live`); conflating the two here would +authorize the wrong thing, exactly as tenant-engine's own +`policy_package.md` notes for its analogous case. + +Single-tenant dogfood today (`tenant:friendly:binky` only); generalizing to +other tenants is a policy update here, not a `qonto-assistant` code change. + +## Rules + +```rego +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +valid_actions := {"finance.qonto.read"} + +valid_subject_types := {"agent", "human", "service"} + +known_tenant := "tenant:friendly:binky" + +decision := {"effect": "allow", "reason": "finance_read_policy_matched"} if { + allowed +} else := {"effect": "deny", "reason": first_denial} if { + true +} + +allowed if { + input.resource.system == "qonto-assistant" + input.action in valid_actions + input.subject.type in valid_subject_types + input.tenant == known_tenant +} + +default first_denial := "no_matching_rule" + +first_denial := "wrong_system" if { + input.resource.system != "qonto-assistant" +} else := "unknown_action" if { + not input.action in valid_actions +} else := "wrong_subject_type" if { + not input.subject.type in valid_subject_types +} else := "wrong_tenant" if { + input.tenant != known_tenant +} +``` + +## Tests + +```rego test +package flexauth.qonto_assistant.finance_read_test + +import future.keywords.if +import data.flexauth.qonto_assistant.finance_read + +base_request := { + "id": "check:qonto-assistant-read", + "tenant": "tenant:friendly:binky", + "subject": {"id": "agent-harness-binky", "type": "agent"}, + "action": "finance.qonto.read", + "resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"} +} + +test_agent_read_allowed if { + finance_read.decision.effect == "allow" with input as base_request +} + +test_human_read_allowed if { + finance_read.decision.effect == "allow" with input as { + "tenant": "tenant:friendly:binky", + "subject": {"id": "bernd.worsch", "type": "human"}, + "action": "finance.qonto.read", + "resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"} + } +} + +test_wrong_system_denied if { + finance_read.decision.reason == "wrong_system" with input as { + "tenant": "tenant:friendly:binky", + "subject": {"id": "agent-harness-binky", "type": "agent"}, + "action": "finance.qonto.read", + "resource": {"id": "x", "type": "finance-snapshot", "system": "some-other-system"} + } +} + +test_unknown_action_denied if { + finance_read.decision.reason == "unknown_action" with input as { + "tenant": "tenant:friendly:binky", + "subject": {"id": "agent-harness-binky", "type": "agent"}, + "action": "finance.qonto.transfer", + "resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"} + } +} + +test_wrong_subject_type_denied if { + finance_read.decision.reason == "wrong_subject_type" with input as { + "tenant": "tenant:friendly:binky", + "subject": {"id": "unknown-device", "type": "device"}, + "action": "finance.qonto.read", + "resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"} + } +} + +test_wrong_tenant_denied if { + finance_read.decision.reason == "wrong_tenant" with input as { + "tenant": "tenant:friendly:some-other-company", + "subject": {"id": "agent-harness-binky", "type": "agent"}, + "action": "finance.qonto.read", + "resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"} + } +} +``` diff --git a/examples/qonto-assistant/protected_system_manifest.yaml b/examples/qonto-assistant/protected_system_manifest.yaml new file mode 100644 index 0000000..b45d0e3 --- /dev/null +++ b/examples/qonto-assistant/protected_system_manifest.yaml @@ -0,0 +1,37 @@ +id: qonto-assistant +name: Qonto Governed Assistant +resource_types: + - name: finance-snapshot + scope_level: Resource + planes: + - Data + - Audit + metadata: + description: >- + Read-only Qonto finance capability surface (org summary, + transactions, CostRunRate hints). No spend/transfer/card/write + capability is ever registered here -- those are hard-denied inside + qonto-assistant's own policy kernel and never reach flex-auth. +actions: + - name: finance.qonto.read + capabilities: + - View + - Audit + planes: + - Data + - Audit + exposure_modes: + - Masked + metadata: + required_context: [] + description: >- + Coarse "may this actor use qonto-assistant's read surface at all" + gate. Tenant capability-role/plan liveness (VEN/CUS, ADR-0014) is a + separate check against tenant-engine's live-lookup endpoint, not + encoded in this policy -- conflating the two would authorize the + wrong thing (see tenant-engine's own policy_package.md note). +caring_profiles: + - caring-0.4.0-rc2 +metadata: + flex_auth_contract: protected-system-v0 + boundary_contract: qonto-assistant/docs/SecurityPractice.md diff --git a/examples/qonto-assistant/registry_snapshot.json b/examples/qonto-assistant/registry_snapshot.json new file mode 100644 index 0000000..2e075b4 --- /dev/null +++ b/examples/qonto-assistant/registry_snapshot.json @@ -0,0 +1,101 @@ +{ + "systems": [ + { + "id": "qonto-assistant", + "name": "Qonto Governed Assistant", + "resource_types": [ + { + "name": "finance-snapshot", + "scope_level": "Resource", + "planes": [ + "Data", + "Audit" + ], + "metadata": { + "description": "Read-only Qonto finance capability surface (org summary, transactions, CostRunRate hints). No spend/transfer/card/write capability is ever registered here -- those are hard-denied inside qonto-assistant's own policy kernel and never reach flex-auth." + } + } + ], + "actions": [ + { + "name": "finance.qonto.read", + "capabilities": [ + "View", + "Audit" + ], + "planes": [ + "Data", + "Audit" + ], + "exposure_modes": [ + "Masked" + ], + "metadata": { + "required_context": [], + "description": "Coarse \"may this actor use qonto-assistant's read surface at all\" gate. Tenant capability-role/plan liveness (VEN/CUS, ADR-0014) is a separate check against tenant-engine's live-lookup endpoint, not encoded in this policy." + } + } + ], + "caring_profiles": [ + "caring-0.4.0-rc2" + ], + "metadata": { + "flex_auth_contract": "protected-system-v0", + "boundary_contract": "qonto-assistant/docs/SecurityPractice.md" + } + } + ], + "resource_manifests": [], + "tenants": [ + { + "id": "tenant:friendly:binky", + "name": "Binky Hedgehog GmbH" + } + ], + "subjects": [ + { + "id": "agent-harness-binky", + "type": "Agent", + "display_name": "agent-harness session (binky tenant)", + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ], + "groups": [ + "group:qonto-assistant-readers" + ], + "tenant": "tenant:friendly:binky", + "metadata": { + "description": "Harness-run agent sessions calling qonto-assistant's finance.qonto.read capability over REST or MCP." + } + }, + { + "id": "bernd.worsch", + "type": "Human", + "display_name": "Bernd Worsch (founder)", + "organization_relation": "Customer", + "roles": [ + "Operator" + ], + "groups": [ + "group:qonto-assistant-readers" + ], + "tenant": "tenant:friendly:binky", + "metadata": { + "description": "Founder operator, human REST/MCP caller." + } + } + ], + "groups": [ + { + "id": "group:qonto-assistant-readers", + "display_name": "qonto-assistant finance.qonto.read callers", + "members": [ + "agent-harness-binky", + "bernd.worsch" + ], + "tenant": "tenant:friendly:binky" + } + ], + "relationships": [] +} diff --git a/examples/qonto-assistant/subject_manifest.yaml b/examples/qonto-assistant/subject_manifest.yaml new file mode 100644 index 0000000..b13da37 --- /dev/null +++ b/examples/qonto-assistant/subject_manifest.yaml @@ -0,0 +1,36 @@ +id: subjects:qonto-assistant-readers +tenants: + - id: tenant:friendly:binky + name: Binky Hedgehog GmbH +subjects: + - id: agent-harness-binky + type: Agent + display_name: agent-harness session (binky tenant) + organization_relation: ServiceProvider + roles: + - Operator + groups: + - group:qonto-assistant-readers + tenant: tenant:friendly:binky + metadata: + description: >- + Harness-run agent sessions calling qonto-assistant's + finance.qonto.read capability over REST or MCP. + - id: bernd.worsch + type: Human + display_name: Bernd Worsch (founder) + organization_relation: Customer + roles: + - Operator + groups: + - group:qonto-assistant-readers + tenant: tenant:friendly:binky + metadata: + description: Founder operator, human REST/MCP caller. +groups: + - id: group:qonto-assistant-readers + display_name: qonto-assistant finance.qonto.read callers + members: + - agent-harness-binky + - bernd.worsch + tenant: tenant:friendly:binky