use serde_json::Value;
use trust_tasks_rs::{RejectReason, TrustTask};
use super::TrustTaskOutcome;
use super::helpers::reject_with;
use crate::auth::AuthClaims;
use crate::policy::{self, Disposition};
use crate::server::AppState;
const STEP_UP_TARGET_ACR: &str = "aal2";
#[allow(deprecated)]
fn op_class_for(type_uri: &str) -> Option<&'static str> {
use super::step_up::op;
use vta_sdk::trust_tasks as t;
match type_uri {
t::TASK_ACL_CREATE_1_0 => Some(op::ACL_GRANT),
t::TASK_ACL_UPDATE_1_0 => Some(op::ACL_CHANGE_ROLE),
t::TASK_ACL_DELETE_1_0 => Some(op::ACL_REVOKE),
t::TASK_CONTEXTS_DELETE_1_0 => Some(op::CONTEXT_DELETE),
t::TASK_KEYS_REVOKE_1_0 => Some(op::KEY_REVOKE),
t::TASK_VAULT_RELEASE_0_1 => Some(op::VAULT_RELEASE),
t::TASK_VAULT_PROXY_LOGIN_0_1 => Some(op::VAULT_PROXY_LOGIN),
t::TASK_VAULT_SIGN_TRUST_TASK_0_1 => Some(op::VAULT_SIGN_TRUST_TASK),
t::TASK_VTA_CREDENTIALS_ISSUE_0_1 => Some(op::CREDENTIALS_ISSUE),
t::TASK_VTA_CREDENTIALS_REVOKE_0_1 => Some(op::CREDENTIALS_REVOKE),
_ => None,
}
}
pub(super) async fn policy_gate(
state: &AppState,
auth: &AuthClaims,
type_uri: &str,
doc: &TrustTask<Value>,
) -> Option<TrustTaskOutcome> {
if let Some(op_class) = op_class_for(type_uri)
&& let Some(reject) = super::step_up::require_step_up(state, auth, op_class, doc).await
{
return Some(reject);
}
if !state.config.read().await.policy.enforcement {
return None;
}
let class = super::class_for(type_uri);
let input = policy::build_policy_input(
type_uri,
&doc.payload,
&auth.did,
&auth.acr,
&auth.amr,
class,
);
let policies = match policy::load_active_for_context(&state.policy_ks, &input.context_id).await
{
Ok(p) => p,
Err(e) => {
tracing::error!(error = %e, type_uri, "policy load failed — denying (fail-closed)");
return Some(reject_with(
doc,
RejectReason::PermissionDenied {
reason: "policy evaluation unavailable".to_string(),
},
));
}
};
let decision = policy::decide(&policies, &input);
match decision.decision {
Disposition::Allow => None,
Disposition::Deny => Some(reject_with(
doc,
RejectReason::PermissionDenied {
reason: decision
.explanation
.unwrap_or_else(|| "denied by policy".to_string()),
},
)),
Disposition::RequireStepUp => {
if auth.acr == STEP_UP_TARGET_ACR {
None
} else {
Some(super::step_up::initiate_self_step_up(state, auth, doc).await)
}
}
Disposition::RequireConsent => Some(reject_with(
doc,
RejectReason::PermissionDenied {
reason: format!(
"consent required: {}",
decision
.explanation
.as_deref()
.unwrap_or("policy requires approver consent")
),
},
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::types::PolicyModule;
fn module(id: &str, priority: i32, rego: &str) -> PolicyModule {
PolicyModule {
id: id.into(),
name: id.into(),
description: None,
module: rego.into(),
applies_to: vec![],
priority,
enabled: true,
version: 1,
created_at: "2026-01-01T00:00:00Z".into(),
updated_at: "2026-01-01T00:00:00Z".into(),
}
}
const DENY_ALL: &str = "package vta.policy\nimport rego.v1\ndecision := {\"decision\": \"deny\", \"explanation\": \"blocked\"}";
const ALLOW_ALL: &str =
"package vta.policy\nimport rego.v1\ndecision := {\"decision\": \"allow\"}";
const STEPUP_IF_NOT_AAL2: &str = "package vta.policy\nimport rego.v1\ndecision := {\"decision\": \"requireStepUp\"} if input.consumer.acr != \"aal2\"\ndecision := {\"decision\": \"allow\"} if input.consumer.acr == \"aal2\"";
fn doc(type_uri: &str) -> TrustTask<Value> {
serde_json::from_value(serde_json::json!({
"id": "urn:uuid:00000000-0000-0000-0000-000000000001",
"type": type_uri,
"issuer": "did:key:zTestAdmin",
"recipient": "did:example:vta",
"issuedAt": "2026-05-20T00:00:00Z",
"payload": { "contextId": "default" }
}))
.expect("valid trust task")
}
const UNGATED_URI: &str = "https://trusttasks.org/spec/vta/memory/list/0.1";
#[tokio::test]
async fn gate_inert_when_disabled_enforces_when_enabled() {
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let auth = crate::test_support::super_admin_claims();
let d = doc(UNGATED_URI);
assert!(policy_gate(&state, &auth, UNGATED_URI, &d).await.is_none());
state.config.write().await.policy.enforcement = true;
assert!(policy_gate(&state, &auth, UNGATED_URI, &d).await.is_some());
crate::policy::storage::store_policy(&state.policy_ks, &module("deny", 0, DENY_ALL))
.await
.unwrap();
assert!(policy_gate(&state, &auth, UNGATED_URI, &d).await.is_some());
crate::policy::storage::store_policy(&state.policy_ks, &module("allow", 10, ALLOW_ALL))
.await
.unwrap();
assert!(policy_gate(&state, &auth, UNGATED_URI, &d).await.is_none());
}
#[tokio::test]
async fn rego_requires_step_up_when_session_not_elevated() {
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let mut auth = crate::test_support::super_admin_claims();
let d = doc(UNGATED_URI);
state.config.write().await.policy.enforcement = true;
crate::policy::storage::store_policy(
&state.policy_ks,
&module("su", 0, STEPUP_IF_NOT_AAL2),
)
.await
.unwrap();
auth.acr = "aal1".into();
assert!(
policy_gate(&state, &auth, UNGATED_URI, &d).await.is_some(),
"aal1 session must be sent to step-up"
);
auth.acr = "aal2".into();
assert!(
policy_gate(&state, &auth, UNGATED_URI, &d).await.is_none(),
"aal2 session must pass the step-up gate"
);
}
}