2. Open Policy Agent
Date: 2025-11-03
Status
Accepted
Context
Use of oslo.policy is not easily possible from Rust. In addition to that during the OpenStack Summit 2025 it was shown how Open Policy Agent can be used to further improve the policy control in OpenStack. As such the Keystone implement the policy enforcement using the OPA with the following rules:
-
Listoperation MUST receive the all query parameters of the operation in the target. -
For
Showoperation the policy MUST receive the current record as the target (fetch the record and pass it into the policy engine). -
Updateoperation MUST receive current and new state of the resource (first the current resource is fetched and passed together with the new state [current, target] to the policy engine). -
Createoperation works similarly as current oslo.policy with the desired state passed to the policy engine. -
Deleteoperation MUST pass the current resource state of the resource into the policy engine.
Decision
The only policy enforcement engine supported in the Keystone is Open Policy Engine.
Consequences
-
Policy evaluation requires external service (OPA) to be running.
-
When covering existing functionality of the python Keystone policies SHOULD be converted as is and do not introduce a changed flow.
Standardized Policy Input Structure
The PolicyEnforcer interface is standardized with the following signature:
#![allow(unused)]
fn main() {
async fn enforce(
&self,
policy_name: &'static str,
credentials: &ValidatedSecurityContext,
target: Value,
existing: Option<Value>,
) -> Result<PolicyEvaluationResult, PolicyError>;
}
The OPA input document structure is:
{
"credentials": { "user_id": "...", "roles": [...], "domain_id": "...", ... },
"target": {
"<resource>": <object or null>
},
"existing": {
"<resource>": <object or null>
}
}
The <resource> key matches the REST resource type: user, group, role,
project, instance, idp, mapping, restriction, assignment, etc.
This prevents field name collisions between policies and ensures each resource’s
data is properly isolated.
Field Semantics Per Operation
The <resource> key matches the REST resource type:
user,group,role,project,instance,idp,mapping,restriction,assignment, etc.- This isolates data and prevents field name collisions between different resource schemas.
Policies read fields as input.target.user.domain_id,
input.existing.restriction.user_id, input.target.instance.name, etc.
Examples:
- Create user:
{"target": {"user": request_payload}} - Update restriction:
{"target": {"restriction": patch}, "existing": {"restriction": stored}} - Show group:
{"target": {"group": stored_object}} - List instances:
{"target": {"instance": query_params}}
Implementation Details
The handler-side contract for enforce():
- Create: Pass
serde_json::to_value(request_object)?as target,Noneas existing - Update: Pass
serde_json::to_value(patch)?as target,Option::from(stored_object).map(serde_json::to_value)as existing - Show: Pass
serde_json::to_value(stored_object)?as target,Noneas existing - Delete: Pass
serde_json::to_value(stored_object)?as target,Noneas existing - List: Pass
serde_json::to_value(query_params)?as target,Noneas existing
Policy Evaluation Guidelines
Ownership predicates that need to work across create/show/delete/update should
resolve the domain_id from either target or existing:
# Resolve domain_id from target or existing depending on operation
resource_domain_id := input.target.domain_id if {
input.target.domain_id
}
resource_domain_id := input.existing.domain_id if {
input.existing.domain_id
}
own_resource if {
resource_domain_id != null
resource_domain_id == input.credentials.domain_id
}
Validation rules (checking user-provided data for referential integrity, e.g.,
that domain/project/role IDs exist) should read from input.target for both
create and update operations, as target carries the user’s request in both
cases.
Notes
-
The
input.existingfield isValue::Nullwhen passed asNonefrom the handler, not an absent key. Policies can checkinput.existing == null. -
The
input.targetfield is nevernullexcept deliberately (e.g., when no target object is relevant). For operations where the object is the existing stored resource,targetcarries that object. -
Policy tests (
*_test.rego) should use the same input structure as handlers:- Create tests:
"target": { "binding": { ... } } - Update tests:
"target": { "binding": { ... } }, "existing": { "binding": { ... } } - Show/Delete tests:
"target": { "binding": { ... } }
- Create tests: