use std::collections::HashMap;
use std::sync::Arc;
use serde_json::Value;
use tokio::sync::RwLock;
use trust_tasks_rs::specs::auth::step_up::policy::v0_2 as policy;
use vti_common::acl::list_acl_entries;
use vti_common::auth::step_up::{StepUpFloor, StepUpMode, StepUpPolicy, op_class};
use vti_common::store::KeyspaceHandle;
use crate::config::AppConfig;
#[derive(Debug, thiserror::Error)]
pub enum SetPolicyError {
#[error("unknown operation-class '{0}': not a gated operation (or '*')")]
UnknownOperation(String),
#[error("{0}")]
LockoutRefused(String),
#[error("acl read failed: {0}")]
Store(String),
#[error("policy persistence failed: {0}")]
Persistence(String),
}
pub async fn set_step_up_policy(
config: &Arc<RwLock<AppConfig>>,
acl_ks: &KeyspaceHandle,
requested: StepUpPolicy,
) -> Result<StepUpPolicy, SetPolicyError> {
for floor in &requested.floors {
if !op_class::is_recognized(&floor.operation) {
return Err(SetPolicyError::UnknownOperation(floor.operation.clone()));
}
}
let effective = canonicalize(requested);
if effective.enabled {
lockout_check(&effective, acl_ks).await?;
}
{
let mut cfg = config.write().await;
cfg.auth.step_up = effective.clone();
cfg.save()
.map_err(|e| SetPolicyError::Persistence(e.to_string()))?;
}
Ok(effective)
}
fn canonicalize(policy: StepUpPolicy) -> StepUpPolicy {
let mut order: Vec<String> = Vec::new();
let mut by_op: HashMap<String, vti_common::auth::step_up::StepUpFloor> = HashMap::new();
for floor in policy.floors {
if !by_op.contains_key(&floor.operation) {
order.push(floor.operation.clone());
}
by_op.insert(floor.operation.clone(), floor);
}
let floors = order
.into_iter()
.map(|op| by_op.remove(&op).expect("op was just inserted"))
.collect();
StepUpPolicy {
enabled: policy.enabled,
floors,
}
}
async fn lockout_check(
policy: &StepUpPolicy,
acl_ks: &KeyspaceHandle,
) -> Result<(), SetPolicyError> {
let needs_delegated = policy
.floors
.iter()
.any(|f| matches!(f.mode, StepUpMode::Delegated));
let needs_delegated_any = policy
.floors
.iter()
.any(|f| matches!(f.mode, StepUpMode::DelegatedAny));
if !needs_delegated && !needs_delegated_any {
return Ok(());
}
let entries = list_acl_entries(acl_ks)
.await
.map_err(|e| SetPolicyError::Store(e.to_string()))?;
if needs_delegated
&& !entries
.iter()
.any(|e| e.step_up_approver.as_deref().is_some_and(|a| !a.is_empty()))
{
return Err(SetPolicyError::LockoutRefused(
"enabling a delegated floor would lock out all administrators: no AclEntry \
carries a stepUp.approver. Register an approver (acl create/update \
--step-up-approver), then enable."
.to_string(),
));
}
if needs_delegated_any && !entries.iter().any(|e| e.is_admin()) {
return Err(SetPolicyError::LockoutRefused(
"enabling a delegated-any floor would lock out all administrators: no admin \
AclEntry exists to ratify. Register an admin, then enable."
.to_string(),
));
}
Ok(())
}
fn floormode_to_mode(m: policy::FloorMode) -> StepUpMode {
match m {
policy::FloorMode::None => StepUpMode::None,
policy::FloorMode::Self_ => StepUpMode::SelfApprove,
policy::FloorMode::Delegated => StepUpMode::Delegated,
policy::FloorMode::DelegatedAny => StepUpMode::DelegatedAny,
}
}
fn mode_to_floormode(m: StepUpMode) -> policy::FloorMode {
match m {
StepUpMode::None => policy::FloorMode::None,
StepUpMode::SelfApprove => policy::FloorMode::Self_,
StepUpMode::Delegated => policy::FloorMode::Delegated,
StepUpMode::DelegatedAny => policy::FloorMode::DelegatedAny,
}
}
pub fn policy_from_payload(p: &policy::Payload) -> StepUpPolicy {
StepUpPolicy {
enabled: p.enabled,
floors: p
.floors
.iter()
.map(|f| StepUpFloor {
operation: f.operation.clone(),
mode: floormode_to_mode(f.mode),
allow_aal1_if_non_escalating: f.allow_aal1_if_non_escalating.unwrap_or(false),
})
.collect(),
}
}
pub fn policy_from_value(v: &Value) -> Result<StepUpPolicy, String> {
let payload: policy::Payload =
serde_json::from_value(v.clone()).map_err(|e| format!("invalid step-up policy: {e}"))?;
Ok(policy_from_payload(&payload))
}
pub fn effective_response(p: &StepUpPolicy) -> Value {
let resp = policy::Response {
enabled: p.enabled,
ext: None,
floors: p
.floors
.iter()
.map(|f| policy::Floor {
operation: f.operation.clone(),
mode: mode_to_floormode(f.mode),
allow_aal1_if_non_escalating: Some(f.allow_aal1_if_non_escalating),
})
.collect(),
};
serde_json::to_value(resp).unwrap_or(Value::Null)
}
#[cfg(test)]
mod tests {
use super::*;
fn floor(op: &str, mode: StepUpMode) -> StepUpFloor {
StepUpFloor {
operation: op.to_string(),
mode,
allow_aal1_if_non_escalating: false,
}
}
#[test]
fn canonicalize_dedupes_last_wins_preserving_order() {
let p = StepUpPolicy {
enabled: true,
floors: vec![
floor("acl/grant", StepUpMode::SelfApprove),
floor("*", StepUpMode::SelfApprove),
floor("acl/grant", StepUpMode::Delegated), ],
};
let c = canonicalize(p);
assert_eq!(c.floors.len(), 2);
assert_eq!(c.floors[0].operation, "acl/grant");
assert_eq!(c.floors[0].mode, StepUpMode::Delegated); assert_eq!(c.floors[1].operation, "*");
}
#[test]
fn op_class_recognition() {
assert!(op_class::is_recognized("*"));
assert!(op_class::is_recognized("acl/grant"));
assert!(op_class::is_recognized("key/revoke"));
assert!(!op_class::is_recognized("acl/teleport"));
assert!(!op_class::is_recognized(""));
}
}