use vti_common::acl::get_acl_entry;
use vti_common::auth::step_up::StepUpMode;
use vti_common::store::KeyspaceHandle;
use crate::config::AppConfig;
pub mod op {
pub use vti_common::auth::step_up::op_class::{
ACL_CHANGE_ROLE, ACL_GRANT, ACL_REVOKE, ACL_SWAP_KEY, CONTEXT_DELETE, KEY_REVOKE,
VAULT_PROXY_LOGIN, VAULT_RELEASE, VAULT_SIGN_TRUST_TASK,
};
}
pub enum StepUpDecision {
Allow,
Require { recipient: String },
RequireAny,
Deny,
}
pub async fn resolve_step_up(
config: &tokio::sync::RwLock<AppConfig>,
acl_ks: &KeyspaceHandle,
op_class: &str,
caller_did: &str,
is_non_escalating: bool,
) -> StepUpDecision {
let (floor_mode, allow_carveout) = {
let cfg = config.read().await;
match cfg.auth.step_up.floor_record(op_class) {
None => return StepUpDecision::Allow,
Some(f) => (f.mode, f.allow_aal1_if_non_escalating),
}
};
let entry = get_acl_entry(acl_ks, caller_did).await.ok().flatten();
let override_mode = entry
.as_ref()
.and_then(|e| e.step_up_require)
.unwrap_or(StepUpMode::None);
let mode = floor_mode.strictest(override_mode);
if !mode.requires_aal2() || (allow_carveout && is_non_escalating) {
return StepUpDecision::Allow;
}
match mode {
StepUpMode::None => StepUpDecision::Allow,
StepUpMode::SelfApprove => StepUpDecision::Require {
recipient: caller_did.to_string(),
},
StepUpMode::Delegated => match entry.and_then(|e| e.step_up_approver) {
Some(approver) => StepUpDecision::Require {
recipient: approver,
},
None => StepUpDecision::Deny,
},
StepUpMode::DelegatedAny => StepUpDecision::RequireAny,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn resolve_step_up_swap_key_honours_floor_and_carveout() {
use vti_common::auth::step_up::{StepUpFloor, StepUpPolicy};
use vti_common::config::StoreConfig;
use vti_common::store::Store;
let dir = tempfile::tempdir().unwrap();
let store = Store::open(&StoreConfig {
data_dir: dir.path().into(),
})
.unwrap();
let acl_ks = store.keyspace(crate::keyspaces::ACL).unwrap();
let caller = "did:key:zCaller";
let mk_config = |allow_carveout: bool, enabled: bool| {
let mut c: crate::config::AppConfig = toml::from_str("").unwrap();
c.auth.step_up = StepUpPolicy {
enabled,
floors: vec![StepUpFloor {
operation: op::ACL_SWAP_KEY.to_string(),
mode: StepUpMode::SelfApprove,
allow_aal1_if_non_escalating: allow_carveout,
}],
};
tokio::sync::RwLock::new(c)
};
let cfg = mk_config(false, true);
assert!(
!matches!(
resolve_step_up(&cfg, &acl_ks, op::ACL_SWAP_KEY, caller, true).await,
StepUpDecision::Allow
),
"a swap-key floor without the carve-out must gate even a non-escalating request"
);
let cfg = mk_config(true, true);
assert!(
matches!(
resolve_step_up(&cfg, &acl_ks, op::ACL_SWAP_KEY, caller, true).await,
StepUpDecision::Allow
),
"the non-escalation carve-out must admit swap-key at AAL1"
);
let cfg = mk_config(false, false);
assert!(
matches!(
resolve_step_up(&cfg, &acl_ks, op::ACL_SWAP_KEY, caller, true).await,
StepUpDecision::Allow
),
"a disabled policy gates nothing"
);
}
#[tokio::test]
async fn resolve_step_up_gates_configured_vault_op_only() {
use vti_common::auth::step_up::{StepUpFloor, StepUpPolicy};
use vti_common::config::StoreConfig;
use vti_common::store::Store;
let dir = tempfile::tempdir().unwrap();
let store = Store::open(&StoreConfig {
data_dir: dir.path().into(),
})
.unwrap();
let acl_ks = store.keyspace(crate::keyspaces::ACL).unwrap();
let caller = "did:key:zCaller";
let mut c: crate::config::AppConfig = toml::from_str("").unwrap();
c.auth.step_up = StepUpPolicy {
enabled: true,
floors: vec![StepUpFloor {
operation: op::VAULT_RELEASE.to_string(),
mode: StepUpMode::SelfApprove,
allow_aal1_if_non_escalating: false,
}],
};
let cfg = tokio::sync::RwLock::new(c);
assert!(
!matches!(
resolve_step_up(&cfg, &acl_ks, op::VAULT_RELEASE, caller, false).await,
StepUpDecision::Allow
),
"a vault/release floor must gate the op"
);
assert!(
matches!(
resolve_step_up(&cfg, &acl_ks, op::VAULT_PROXY_LOGIN, caller, false).await,
StepUpDecision::Allow
),
"an op with no configured floor must not be gated"
);
}
}