118 lines
3.2 KiB
Rego
118 lines
3.2 KiB
Rego
# 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)
|
|
}
|