Policies
The Policies screen lets you manage the policy rules of the tenant, evaluate a Policy Contract request against them, publish the OPA policies as a signed bundle and check the revision each policy engine really runs. Portal path: /security/policies, menu Security and governance > Policies. It is used by security officers and tenant administrators who write the rules, by auditors who read them, and by developers who test a request.
What the screen shows
The screen (Policies in portal/src/screens/security.tsx) has four sections, from top to bottom.
- Policies table, read from
GET /v1/policies(no paging): columns Name, Version, Engine, Priority, Status, Updated. A click on a row opens a detail panel loaded fromGET /v1/policies/{policy_id}(the JSON of the policy). Empty table: "No policy", "Built in policies apply until you add your own." - Evaluate a Policy Contract request: a form with one JSON field Request, prefilled with an example (an agent invoking a model). The result shows a large Allow or Deny badge, the decision identifier, the list of the reasons, one badge per obligation and the full JSON answer.
- Signed OPA bundles table, read from
GET /v1/policies/bundles: columns Revision, SHA-256, Signed, Created by, Created. A click on a row shows the row. - Policy engines: the JSON answer of
GET /v1/policies/engines, "Revision each OPA bundle really runs and the state of the relationship engine" (OPA: configured, delivery, expected revision, reachable, in sync; OpenFGA: configured, reachable).
The create and edit form of a policy has these fields:
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
| Name | text | yes | Lowercase letters, digits, _, . and -, up to 100 characters (deny-restricted-external). | |
| Description | text | no | Free text. | |
| Engine | builtin or opa | yes | opa needs a Rego (OPA) text. | |
| Priority | number | 100 | no | Lower values are evaluated first. |
| Status | active, draft, disabled | no | The API stores active when the field is empty. | |
| Rules | JSON | one deny rule on model.invoke for restricted and secret data | no | List of rules: effect, actions, resource_types, conditions, reason. |
| Rego (OPA) | text | for opa | Module such as package eai.model_invocation. |
Actions
| Button or action | What it does | API call | Roles |
|---|---|---|---|
| New policy (then Create) | Creates a policy from the form; a revision is recorded and the change is audited (policy.create). | POST /v1/policies | tenant_admin, security_officer |
| Row click or View details | Opens the detail panel of the policy. | GET /v1/policies/{policy_id} | Every user of the tenant |
| Edit (row menu, then Edit) | Replaces the policy with the form values, prefilled from the row; a new revision is recorded (policy.update). | PUT /v1/policies/{policy_id} | tenant_admin, security_officer |
| Delete (row menu) | Opens the destructive confirmation (impact, dependents, risks); you type the name of the policy, then Confirm. | GET /v1/lineage/graph, then DELETE /v1/policies/{policy_id} | tenant_admin, security_officer |
| Run (Evaluate a Policy Contract request) | Evaluates the request and shows the decision. | POST /v1/policies/evaluate | Every user of the tenant |
| Publish a bundle (then Create) | Builds and publishes a signed bundle of the OPA policies, with an optional Revision and a Data JSON. | POST /v1/policies/bundles | tenant_admin, security_officer (button hidden for others) |
| Row click (Signed OPA bundles) | Shows the bundle row. | None | Readers of the bundles |
| Refresh icon of a table | Reads the list again. | GET /v1/policies or GET /v1/policies/bundles | Readers of the list |
The edit form does not carry the Version of the policy: the API stores its default version 1.0.0 on each save from this screen. Use the API to set an explicit version.
An example of a request for Evaluate a Policy Contract request (the subject is optional: without it, the request is evaluated for you):
{
"action": "model.invoke",
"resource": {"type": "model", "id": "claude-sonnet-eu"},
"context": {"data_classification": "confidential", "region": "eu-central-1"}
}
Roles
The portal hides only the Publish a bundle button, shown to ROLES.delegationAdmins (tenant_admin, security_officer in portal/src/lib/kit.ts). Every other control is shown and the Control Plane decides (services/control-plane/src/eai/domains/policies/__init__.py):
- list, detail and evaluation: any user of the tenant;
- create, edit, delete, publish a bundle:
tenant_admin,security_officer; - list of the bundles and state of the engines:
tenant_admin,security_officer,auditor; platform_adminpasses every role check.
A tenant_admin or a security_officer may evaluate a request on behalf of another subject of the tenant (what-if): the evaluation is then audited as policy.simulate. For any other user, the subject is always the caller.
Typical tasks
Add a deny rule for restricted data
- Open the form. A policy of the tenant adds to the built in ones. Click New policy. Expected result: a side panel with the fields prefilled (rule
denyonmodel.invoke). If it fails: no table means the list did not load; click the refresh icon. - Fill the policy. Enter a Name such as
deny-restricted-external, keep Enginebuiltin, set Priority and adapt Rules. Expected result: the form accepts the JSON. If it fails: "Invalid JSON" under Rules means the JSON does not parse. - Create. Click Create. Expected result: the message Created and a new row with status
active. If it fails:forbidden(HTTP 403) means a missing role;conflict(HTTP 409) means the name already exists;validation_error(HTTP 422) means a name outside the allowed characters, oropawithout Rego (OPA). - Test it. In Evaluate a Policy Contract request, send a request with
"data_classification": "restricted"and click Run. Expected result: a Deny badge with the reason of your rule. If it fails: an Allow badge means the rule does not match (checkactions,resource_typesandconditions).
Publish the OPA policies for the engines
- Publish. OPA polls a signed archive instead of receiving each module. Click Publish a bundle, leave Revision empty or enter one, then Create. Expected result: a new row in Signed OPA bundles with Signed checked and its SHA-256. If it fails:
conflictmeans the revision already exists; the button is missing when you are neithertenant_adminnorsecurity_officer. - Check the engines. OPA must run the revision just published. Read Policy engines. Expected result:
in_syncistrueandexpected_revisionis the new revision. If it fails:reachable: falseshows the error of OPA; wait for its next poll and reload the page.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
forbidden (HTTP 403) on create, edit or delete | Your roles are neither tenant_admin nor security_officer. | Ask for one of these roles. |
validation_error (HTTP 422) on create | Name outside ^[a-z0-9][a-z0-9_.-]{0,99}$, or "rego is required for engine opa". | Fix the name, or fill Rego (OPA). |
validation_error with contract: policy-contract on Run | The request does not follow the Policy Contract schema. | Start again from the prefilled example. |
conflict (HTTP 409) | A policy with this name, or a bundle with this revision, already exists. | Choose another name or revision. |
policy_engine_unavailable (HTTP 503) | The Control Plane could not push the Rego module to OPA, or remove it on a delete (nothing is deleted then). | Check that OPA runs, or use the bundle delivery. |
not_found on the detail panel | The policy was deleted, or its identifier is not a UUID. | Refresh the table. |
Policy engines or Signed OPA bundles show forbidden | These reads need tenant_admin, security_officer or auditor. | Ask for one of these roles. |
tenant_required (HTTP 403) | The account acts without a tenant. | Sign in with an account of the tenant. |