policies: add Rego skeletons for transformations, sink expense_db_app, and ds expense_db_app (20/20 opa test pass, rego.v1)
This commit is contained in:
parent
71259de4c8
commit
e721073767
7 changed files with 646 additions and 0 deletions
86
policies/README.md
Normal file
86
policies/README.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# ProofedMesh Policies (OPA / Rego)
|
||||
|
||||
Rego-Policies für die drei in [`docs/architecture/v1.1.md`](../docs/architecture/v1.1.md) Abschnitt 6 definierten Policy-Klassen. Alle Policies nutzen **Rego v1** (`import rego.v1`).
|
||||
|
||||
## Policy-Klassen
|
||||
|
||||
| Klasse | Package-Präfix | Fragt der |
|
||||
| ----------------- | ------------------------------------ | ------------------------------ |
|
||||
| Transformationen | `proofedmesh.transformations.<domain>` | Signing Service |
|
||||
| Senken | `proofedmesh.sinks.<sink_id_slug>` | Verification Service / Adapter |
|
||||
| Datenquellen | `proofedmesh.data_sources.<ds_id_slug>` | Session Scope Token Service |
|
||||
|
||||
Default in jedem Package ist `allow := false` — explizites Zulassen ist erforderlich (deny-by-default).
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
```
|
||||
policies/
|
||||
transformations/
|
||||
expense.rego
|
||||
expense_test.rego
|
||||
sinks/
|
||||
expense_db_app.rego
|
||||
expense_db_app_test.rego
|
||||
data_sources/
|
||||
expense_db_app.rego
|
||||
expense_db_app_test.rego
|
||||
```
|
||||
|
||||
## Input-Verträge
|
||||
|
||||
### Transformationen
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "agent-sink-mapping",
|
||||
"operation": "expense.map_to_sink",
|
||||
"input_schema": { "name": "expense.canonical", "version": "1.0.0" },
|
||||
"output_schema": { "name": "sink.db.expense", "version": "1.0.0" },
|
||||
"sink_target": "sink:expense-db-app",
|
||||
"approval": { "allowed_transformations": [...], "allowed_sinks": [...] },
|
||||
"input_payload": { ... canonical fields ... },
|
||||
"output_payload": { ... sink fields ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Senken
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "agent-sink-posting",
|
||||
"sink_id": "sink:expense-db-app",
|
||||
"action": "expense.post",
|
||||
"payload": { ... sink.db.expense ... },
|
||||
"approval": { "allowed_sinks": [...] }
|
||||
}
|
||||
```
|
||||
|
||||
### Datenquellen (für Session Scope Token Service)
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "agent-travel",
|
||||
"user_sub": "user-12345",
|
||||
"session_id": "SES-...",
|
||||
"data_source": "ds:expense-db-app",
|
||||
"resource": "project_assignments",
|
||||
"filters": { "employee_id": "EMP-12345" },
|
||||
"operation": "read"
|
||||
}
|
||||
```
|
||||
|
||||
Zusätzliche Daten (nicht `input`, sondern über `data.*` bereitgestellt): `data.employee_mapping.by_sub` (user_sub → employee_id), `data.agent_registry.by_id` (Agent-Metadaten), `data.sinks.by_id`, `data.data_sources.by_id`. Trust-API lädt diese bei jedem Policy-Call aus Postgres und übergibt sie via OPA Partial-Evaluation oder bei Bedarf als `input.data`.
|
||||
|
||||
## Lokal testen
|
||||
|
||||
```bash
|
||||
opa test policies/ -v
|
||||
opa fmt --list --diff policies/ # formatting check
|
||||
```
|
||||
|
||||
Beides läuft auch in CI (siehe `.forgejo/workflows/validate.yml`).
|
||||
|
||||
## Policy-Versionierung
|
||||
|
||||
Jede Policy-Datei bekommt bei Breaking Changes eine neue Hauptversion. Historische Versionen bleiben parallel, solange Attestierungen mit `policy_version` darauf verweisen (Retention 1 Jahr). Rego-Bundles für Deployment werden aus dem jeweiligen Branch gebaut; der SHA-256 des Bundles landet in jeder Decision als `policy_version` (siehe ADR-0002, Abschnitt „Token-Format").
|
||||
99
policies/data_sources/expense_db_app.rego
Normal file
99
policies/data_sources/expense_db_app.rego
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# METADATA
|
||||
# title: Data source policy for ds:expense-db-app
|
||||
# description: |
|
||||
# Governs which agents may read which resources in whose name on the
|
||||
# own DB-app data source. Requires an active user session for all
|
||||
# current reads (requires_user_context=TRUE for every registered
|
||||
# agent_data_source_scope).
|
||||
# authors:
|
||||
# - CTO Agent <cto@basicstack.de>
|
||||
# related_resources:
|
||||
# - ref: docs/architecture/v1.1.md#user-content-3b-data-source-registry
|
||||
# - ref: docs/architecture/v1.1.md#user-content-3c-session-scope-tokens--delegation-agent-im-auftrag-von-benutzer
|
||||
package proofedmesh.data_sources.expense_db_app
|
||||
|
||||
import rego.v1
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if {
|
||||
input.data_source == "ds:expense-db-app"
|
||||
input.operation == "read"
|
||||
input.user_sub != ""
|
||||
input.session_id != ""
|
||||
some rule in agent_scope_rules
|
||||
rule.agent_id == input.agent_id
|
||||
rule.resource == input.resource
|
||||
filters_within_scope(rule)
|
||||
user_bound_filters_ok(rule)
|
||||
}
|
||||
|
||||
# Rule table: who may read what, with which filter dimensions and user-binding mode.
|
||||
agent_scope_rules := [
|
||||
{
|
||||
"agent_id": "agent-travel",
|
||||
"resource": "project_assignments",
|
||||
"allowed_filters": {"employee_id", "active_on"},
|
||||
"required_filters": {"employee_id"},
|
||||
"require_own_employee": true,
|
||||
},
|
||||
{
|
||||
"agent_id": "agent-sink-mapping",
|
||||
"resource": "account_assignments",
|
||||
"allowed_filters": {"project_code", "cost_center"},
|
||||
"required_filters": {"project_code"},
|
||||
"require_own_employee": false,
|
||||
},
|
||||
{
|
||||
"agent_id": "agent-sink-mapping",
|
||||
"resource": "cost_centers",
|
||||
"allowed_filters": {"active"},
|
||||
"required_filters": set(),
|
||||
"require_own_employee": false,
|
||||
},
|
||||
]
|
||||
|
||||
# Only the declared filter keys are allowed; required filters must be present.
|
||||
filters_within_scope(rule) if {
|
||||
provided := {k | some k; _ := input.filters[k]}
|
||||
count(provided - rule.allowed_filters) == 0
|
||||
count(rule.required_filters - provided) == 0
|
||||
}
|
||||
|
||||
# When require_own_employee=true, the employee_id filter must map to input.user_sub.
|
||||
# The mapping is loaded as external data: data.employee_mapping.by_sub[user_sub] -> "EMP-...".
|
||||
user_bound_filters_ok(rule) if {
|
||||
not rule.require_own_employee
|
||||
}
|
||||
|
||||
user_bound_filters_ok(rule) if {
|
||||
rule.require_own_employee
|
||||
expected := data.employee_mapping.by_sub[input.user_sub]
|
||||
input.filters.employee_id == expected
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
input.data_source != "ds:expense-db-app"
|
||||
msg := sprintf("wrong data_source: %s", [input.data_source])
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
input.operation != "read"
|
||||
msg := sprintf("operation %s not supported", [input.operation])
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
input.user_sub == ""
|
||||
msg := "missing user_sub (requires_user_context=true for all registered scopes)"
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
not any_scope_rule_matches
|
||||
msg := sprintf("no scope rule for agent=%s resource=%s", [input.agent_id, input.resource])
|
||||
}
|
||||
|
||||
any_scope_rule_matches if {
|
||||
some rule in agent_scope_rules
|
||||
rule.agent_id == input.agent_id
|
||||
rule.resource == input.resource
|
||||
}
|
||||
110
policies/data_sources/expense_db_app_test.rego
Normal file
110
policies/data_sources/expense_db_app_test.rego
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package proofedmesh.data_sources.expense_db_app_test
|
||||
|
||||
import data.proofedmesh.data_sources.expense_db_app as ds
|
||||
import rego.v1
|
||||
|
||||
employee_mapping := {"by_sub": {"user-12345": "EMP-12345"}}
|
||||
|
||||
test_allow_travel_own_assignments if {
|
||||
ds.allow with input as {
|
||||
"agent_id": "agent-travel",
|
||||
"user_sub": "user-12345",
|
||||
"session_id": "SES-abc",
|
||||
"data_source": "ds:expense-db-app",
|
||||
"resource": "project_assignments",
|
||||
"filters": {"employee_id": "EMP-12345"},
|
||||
"operation": "read",
|
||||
}
|
||||
with data.employee_mapping as employee_mapping
|
||||
}
|
||||
|
||||
test_deny_travel_other_employee if {
|
||||
not ds.allow with input as {
|
||||
"agent_id": "agent-travel",
|
||||
"user_sub": "user-12345",
|
||||
"session_id": "SES-abc",
|
||||
"data_source": "ds:expense-db-app",
|
||||
"resource": "project_assignments",
|
||||
"filters": {"employee_id": "EMP-99999"},
|
||||
"operation": "read",
|
||||
}
|
||||
with data.employee_mapping as employee_mapping
|
||||
}
|
||||
|
||||
test_deny_missing_required_filter if {
|
||||
not ds.allow with input as {
|
||||
"agent_id": "agent-travel",
|
||||
"user_sub": "user-12345",
|
||||
"session_id": "SES-abc",
|
||||
"data_source": "ds:expense-db-app",
|
||||
"resource": "project_assignments",
|
||||
"filters": {"active_on": "2026-10-25"},
|
||||
"operation": "read",
|
||||
}
|
||||
with data.employee_mapping as employee_mapping
|
||||
}
|
||||
|
||||
test_deny_disallowed_filter_key if {
|
||||
not ds.allow with input as {
|
||||
"agent_id": "agent-travel",
|
||||
"user_sub": "user-12345",
|
||||
"session_id": "SES-abc",
|
||||
"data_source": "ds:expense-db-app",
|
||||
"resource": "project_assignments",
|
||||
"filters": {"employee_id": "EMP-12345", "department": "ENG"},
|
||||
"operation": "read",
|
||||
}
|
||||
with data.employee_mapping as employee_mapping
|
||||
}
|
||||
|
||||
test_allow_mapping_account_assignments if {
|
||||
ds.allow with input as {
|
||||
"agent_id": "agent-sink-mapping",
|
||||
"user_sub": "user-12345",
|
||||
"session_id": "SES-abc",
|
||||
"data_source": "ds:expense-db-app",
|
||||
"resource": "account_assignments",
|
||||
"filters": {"project_code": "P-42"},
|
||||
"operation": "read",
|
||||
}
|
||||
with data.employee_mapping as employee_mapping
|
||||
}
|
||||
|
||||
test_deny_missing_user_sub if {
|
||||
not ds.allow with input as {
|
||||
"agent_id": "agent-travel",
|
||||
"user_sub": "",
|
||||
"session_id": "SES-abc",
|
||||
"data_source": "ds:expense-db-app",
|
||||
"resource": "project_assignments",
|
||||
"filters": {"employee_id": "EMP-12345"},
|
||||
"operation": "read",
|
||||
}
|
||||
with data.employee_mapping as employee_mapping
|
||||
}
|
||||
|
||||
test_deny_wrong_data_source if {
|
||||
not ds.allow with input as {
|
||||
"agent_id": "agent-travel",
|
||||
"user_sub": "user-12345",
|
||||
"session_id": "SES-abc",
|
||||
"data_source": "ds:other",
|
||||
"resource": "project_assignments",
|
||||
"filters": {"employee_id": "EMP-12345"},
|
||||
"operation": "read",
|
||||
}
|
||||
with data.employee_mapping as employee_mapping
|
||||
}
|
||||
|
||||
test_deny_write_op if {
|
||||
not ds.allow with input as {
|
||||
"agent_id": "agent-travel",
|
||||
"user_sub": "user-12345",
|
||||
"session_id": "SES-abc",
|
||||
"data_source": "ds:expense-db-app",
|
||||
"resource": "project_assignments",
|
||||
"filters": {"employee_id": "EMP-12345"},
|
||||
"operation": "write",
|
||||
}
|
||||
with data.employee_mapping as employee_mapping
|
||||
}
|
||||
63
policies/sinks/expense_db_app.rego
Normal file
63
policies/sinks/expense_db_app.rego
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# METADATA
|
||||
# title: Sink policy for sink:expense-db-app
|
||||
# description: |
|
||||
# Only the posting agent may call the own DB-app sink, only with the
|
||||
# expense.post action, and only with the sink.db.expense payload schema
|
||||
# that was explicitly listed in the approval's allowed_sinks.
|
||||
# authors:
|
||||
# - CTO Agent <cto@basicstack.de>
|
||||
# related_resources:
|
||||
# - ref: docs/architecture/v1.1.md#user-content-70-vertrag-jedes-senken-adapters
|
||||
# - ref: schemas/sink/db/expense/v1.0.0.json
|
||||
package proofedmesh.sinks.expense_db_app
|
||||
|
||||
import rego.v1
|
||||
|
||||
default allow := false
|
||||
|
||||
allowed_agent := "agent-sink-posting"
|
||||
|
||||
allowed_actions := {"expense.post"}
|
||||
|
||||
allow if {
|
||||
input.sink_id == "sink:expense-db-app"
|
||||
input.agent_id == allowed_agent
|
||||
input.action in allowed_actions
|
||||
payload_schema_ok
|
||||
approval_lists_sink
|
||||
}
|
||||
|
||||
payload_schema_ok if {
|
||||
input.payload.schema.name == "sink.db.expense"
|
||||
startswith(input.payload.schema.version, "1.")
|
||||
}
|
||||
|
||||
approval_lists_sink if {
|
||||
some s in input.approval.allowed_sinks
|
||||
s == "sink:expense-db-app"
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
input.sink_id != "sink:expense-db-app"
|
||||
msg := sprintf("wrong sink_id: %s", [input.sink_id])
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
input.agent_id != allowed_agent
|
||||
msg := sprintf("agent %s not allowed to post to sink:expense-db-app", [input.agent_id])
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
not input.action in allowed_actions
|
||||
msg := sprintf("action %s not allowed on sink:expense-db-app", [input.action])
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
not payload_schema_ok
|
||||
msg := "payload schema mismatch (expected sink.db.expense v1.x)"
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
not approval_lists_sink
|
||||
msg := "sink:expense-db-app not in approval.allowed_sinks"
|
||||
}
|
||||
62
policies/sinks/expense_db_app_test.rego
Normal file
62
policies/sinks/expense_db_app_test.rego
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package proofedmesh.sinks.expense_db_app_test
|
||||
|
||||
import data.proofedmesh.sinks.expense_db_app as sink
|
||||
import rego.v1
|
||||
|
||||
good_payload := {
|
||||
"schema": {"name": "sink.db.expense", "version": "1.0.0"},
|
||||
"workflow_id": "WF-2026-0001",
|
||||
}
|
||||
|
||||
good_approval := {"allowed_sinks": ["sink:expense-db-app"]}
|
||||
|
||||
test_allow_posting if {
|
||||
sink.allow with input as {
|
||||
"sink_id": "sink:expense-db-app",
|
||||
"agent_id": "agent-sink-posting",
|
||||
"action": "expense.post",
|
||||
"payload": good_payload,
|
||||
"approval": good_approval,
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_wrong_agent if {
|
||||
not sink.allow with input as {
|
||||
"sink_id": "sink:expense-db-app",
|
||||
"agent_id": "agent-travel",
|
||||
"action": "expense.post",
|
||||
"payload": good_payload,
|
||||
"approval": good_approval,
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_wrong_action if {
|
||||
not sink.allow with input as {
|
||||
"sink_id": "sink:expense-db-app",
|
||||
"agent_id": "agent-sink-posting",
|
||||
"action": "expense.update",
|
||||
"payload": good_payload,
|
||||
"approval": good_approval,
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_wrong_schema if {
|
||||
bad := {"schema": {"name": "sink.sap.expense", "version": "1.0.0"}}
|
||||
not sink.allow with input as {
|
||||
"sink_id": "sink:expense-db-app",
|
||||
"agent_id": "agent-sink-posting",
|
||||
"action": "expense.post",
|
||||
"payload": bad,
|
||||
"approval": good_approval,
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_sink_not_in_approval if {
|
||||
not sink.allow with input as {
|
||||
"sink_id": "sink:expense-db-app",
|
||||
"agent_id": "agent-sink-posting",
|
||||
"action": "expense.post",
|
||||
"payload": good_payload,
|
||||
"approval": {"allowed_sinks": ["sink:sap-s4hana-prod"]},
|
||||
}
|
||||
}
|
||||
118
policies/transformations/expense.rego
Normal file
118
policies/transformations/expense.rego
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# METADATA
|
||||
# title: Transformation policies for expense workflows
|
||||
# description: |
|
||||
# Governs which agents may perform which transformations on expense
|
||||
# artifacts, and enforces the invariants that must survive mapping.
|
||||
# authors:
|
||||
# - CTO Agent <cto@basicstack.de>
|
||||
# related_resources:
|
||||
# - ref: docs/architecture/v1.1.md#user-content-6-berechtigungssteuerung-mit-open-policy-agent
|
||||
# - ref: schemas/expense/canonical/v1.0.0.json
|
||||
# - ref: schemas/sink/db/expense/v1.0.0.json
|
||||
package proofedmesh.transformations.expense
|
||||
|
||||
import rego.v1
|
||||
|
||||
default allow := false
|
||||
|
||||
# Allow only known agent + operation combinations.
|
||||
allow if {
|
||||
some rule in agent_allowed
|
||||
rule.agent_id == input.agent_id
|
||||
rule.operation == input.operation
|
||||
schemas_match(rule)
|
||||
invariants_hold
|
||||
}
|
||||
|
||||
# Rule table — the authoritative map of what each agent may do.
|
||||
agent_allowed := [
|
||||
{
|
||||
"agent_id": "agent-travel",
|
||||
"operation": "expense.extract_receipts",
|
||||
"input_schema": {"name": "receipt.raw", "version": "1.0.0"},
|
||||
"output_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
},
|
||||
{
|
||||
"agent_id": "agent-travel",
|
||||
"operation": "expense.normalize",
|
||||
"input_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
"output_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
},
|
||||
{
|
||||
"agent_id": "agent-sink-mapping",
|
||||
"operation": "expense.map_to_sink",
|
||||
"input_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
"output_schema": {"name": "sink.db.expense", "version": "1.0.0"},
|
||||
},
|
||||
]
|
||||
|
||||
schemas_match(rule) if {
|
||||
input.input_schema.name == rule.input_schema.name
|
||||
input.input_schema.version == rule.input_schema.version
|
||||
input.output_schema.name == rule.output_schema.name
|
||||
input.output_schema.version == rule.output_schema.version
|
||||
}
|
||||
|
||||
# Business invariants that no transformation may violate without a new approval.
|
||||
invariants_hold if {
|
||||
total_amount_preserved
|
||||
currency_preserved
|
||||
employee_id_preserved
|
||||
}
|
||||
|
||||
# total_amount from the canonical input must appear unchanged in the sink output.
|
||||
# Only enforced when both sides carry the fields.
|
||||
total_amount_preserved if {
|
||||
not input.input_payload.total_amount
|
||||
}
|
||||
|
||||
total_amount_preserved if {
|
||||
input.input_payload.total_amount == input.output_payload.expense.total_amount
|
||||
}
|
||||
|
||||
currency_preserved if {
|
||||
not input.input_payload.currency
|
||||
}
|
||||
|
||||
currency_preserved if {
|
||||
input.input_payload.currency == input.output_payload.expense.currency
|
||||
}
|
||||
|
||||
employee_id_preserved if {
|
||||
not input.input_payload.employee_id
|
||||
}
|
||||
|
||||
employee_id_preserved if {
|
||||
input.input_payload.employee_id == input.output_payload.expense.employee_id
|
||||
}
|
||||
|
||||
# Human-readable denial reasons for debugging and audit.
|
||||
deny_reason contains msg if {
|
||||
not any_rule_matches
|
||||
msg := sprintf("no allow rule for agent=%s operation=%s", [input.agent_id, input.operation])
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
any_rule_matches
|
||||
not total_amount_preserved
|
||||
msg := "total_amount changed by transformation"
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
any_rule_matches
|
||||
not currency_preserved
|
||||
msg := "currency changed by transformation"
|
||||
}
|
||||
|
||||
deny_reason contains msg if {
|
||||
any_rule_matches
|
||||
not employee_id_preserved
|
||||
msg := "employee_id changed by transformation"
|
||||
}
|
||||
|
||||
any_rule_matches if {
|
||||
some rule in agent_allowed
|
||||
rule.agent_id == input.agent_id
|
||||
rule.operation == input.operation
|
||||
schemas_match(rule)
|
||||
}
|
||||
108
policies/transformations/expense_test.rego
Normal file
108
policies/transformations/expense_test.rego
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package proofedmesh.transformations.expense_test
|
||||
|
||||
import data.proofedmesh.transformations.expense
|
||||
import rego.v1
|
||||
|
||||
canonical_payload := {
|
||||
"employee_id": "EMP-12345",
|
||||
"currency": "EUR",
|
||||
"total_amount": "428.50",
|
||||
}
|
||||
|
||||
sink_payload := {"expense": {
|
||||
"employee_id": "EMP-12345",
|
||||
"currency": "EUR",
|
||||
"total_amount": "428.50",
|
||||
}}
|
||||
|
||||
test_allow_map_to_sink if {
|
||||
expense.allow with input as {
|
||||
"agent_id": "agent-sink-mapping",
|
||||
"operation": "expense.map_to_sink",
|
||||
"input_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
"output_schema": {"name": "sink.db.expense", "version": "1.0.0"},
|
||||
"input_payload": canonical_payload,
|
||||
"output_payload": sink_payload,
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_unknown_agent if {
|
||||
not expense.allow with input as {
|
||||
"agent_id": "agent-rogue",
|
||||
"operation": "expense.map_to_sink",
|
||||
"input_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
"output_schema": {"name": "sink.db.expense", "version": "1.0.0"},
|
||||
"input_payload": canonical_payload,
|
||||
"output_payload": sink_payload,
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_schema_mismatch if {
|
||||
not expense.allow with input as {
|
||||
"agent_id": "agent-sink-mapping",
|
||||
"operation": "expense.map_to_sink",
|
||||
"input_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
"output_schema": {"name": "sink.db.expense", "version": "2.0.0"},
|
||||
"input_payload": canonical_payload,
|
||||
"output_payload": sink_payload,
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_total_amount_change if {
|
||||
tampered := {"expense": {
|
||||
"employee_id": "EMP-12345",
|
||||
"currency": "EUR",
|
||||
"total_amount": "999.99",
|
||||
}}
|
||||
not expense.allow with input as {
|
||||
"agent_id": "agent-sink-mapping",
|
||||
"operation": "expense.map_to_sink",
|
||||
"input_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
"output_schema": {"name": "sink.db.expense", "version": "1.0.0"},
|
||||
"input_payload": canonical_payload,
|
||||
"output_payload": tampered,
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_currency_change if {
|
||||
tampered := {"expense": {
|
||||
"employee_id": "EMP-12345",
|
||||
"currency": "USD",
|
||||
"total_amount": "428.50",
|
||||
}}
|
||||
not expense.allow with input as {
|
||||
"agent_id": "agent-sink-mapping",
|
||||
"operation": "expense.map_to_sink",
|
||||
"input_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
"output_schema": {"name": "sink.db.expense", "version": "1.0.0"},
|
||||
"input_payload": canonical_payload,
|
||||
"output_payload": tampered,
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_employee_change if {
|
||||
tampered := {"expense": {
|
||||
"employee_id": "EMP-99999",
|
||||
"currency": "EUR",
|
||||
"total_amount": "428.50",
|
||||
}}
|
||||
not expense.allow with input as {
|
||||
"agent_id": "agent-sink-mapping",
|
||||
"operation": "expense.map_to_sink",
|
||||
"input_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
"output_schema": {"name": "sink.db.expense", "version": "1.0.0"},
|
||||
"input_payload": canonical_payload,
|
||||
"output_payload": tampered,
|
||||
}
|
||||
}
|
||||
|
||||
test_deny_posting_agent_cannot_map if {
|
||||
not expense.allow with input as {
|
||||
"agent_id": "agent-sink-posting",
|
||||
"operation": "expense.map_to_sink",
|
||||
"input_schema": {"name": "expense.canonical", "version": "1.0.0"},
|
||||
"output_schema": {"name": "sink.db.expense", "version": "1.0.0"},
|
||||
"input_payload": canonical_payload,
|
||||
"output_payload": sink_payload,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue