use chrono::Utc;
use tracing::{debug, info, warn};
use uuid::Uuid;
use vti_common::error::AppError;
use vti_common::store::KeyspaceHandle;
use super::engine::compile;
use super::model::PolicyPurpose;
use super::storage::{get_active_policy_id, new_policy, set_active_policy_id, store_policy};
pub const DEFAULTS_AUTHOR: &str = "did:example:vtc-defaults";
const DEFAULT_SOURCES: &[(PolicyPurpose, &str)] = &[
(
PolicyPurpose::Join,
include_str!("../../policies/default/join.rego"),
),
(
PolicyPurpose::Removal,
include_str!("../../policies/default/removal.rego"),
),
(
PolicyPurpose::Personhood,
include_str!("../../policies/default/personhood.rego"),
),
(
PolicyPurpose::Registry,
include_str!("../../policies/default/registry.rego"),
),
(
PolicyPurpose::Directory,
include_str!("../../policies/default/directory.rego"),
),
(
PolicyPurpose::RoleDefinitions,
include_str!("../../policies/default/role_definitions.rego"),
),
(
PolicyPurpose::CrossCommunityRoles,
include_str!("../../policies/default/cross_community_roles.rego"),
),
(
PolicyPurpose::CrossCommunityRelationships,
include_str!("../../policies/default/cross_community_relationships.rego"),
),
(
PolicyPurpose::Relationships,
include_str!("../../policies/default/relationships.rego"),
),
];
pub const DEFAULT_COUNT: usize = 9;
pub fn default_source(purpose: PolicyPurpose) -> &'static str {
DEFAULT_SOURCES
.iter()
.find(|(p, _)| *p == purpose)
.map(|(_, src)| *src)
.expect("every PolicyPurpose has a shipped default — see DEFAULT_SOURCES")
}
pub async fn install_defaults(
policies_ks: &KeyspaceHandle,
active_policies_ks: &KeyspaceHandle,
) -> Result<usize, AppError> {
let mut installed = 0_usize;
for (purpose, source) in DEFAULT_SOURCES {
if get_active_policy_id(active_policies_ks, *purpose)
.await?
.is_some()
{
debug!(
purpose = purpose.as_str(),
"default-policy install skipped — purpose already has an active policy"
);
continue;
}
let id = Uuid::new_v4();
let compiled = compile(source, id).map_err(|e| {
AppError::Internal(format!(
"default policy for {} failed to compile: {e}",
purpose.as_str()
))
})?;
let sha = *compiled.source_sha256();
let mut policy = new_policy(
*purpose,
(*source).to_string(),
sha,
DEFAULTS_AUTHOR.to_string(),
1,
);
policy.id = id;
policy.activated_at = Some(Utc::now());
store_policy(policies_ks, &policy).await?;
set_active_policy_id(active_policies_ks, *purpose, id).await?;
installed += 1;
info!(
purpose = purpose.as_str(),
policy_id = %id,
sha256 = %hex::encode(sha),
"default policy installed"
);
}
if installed == 0 {
debug!("no default policies installed — every purpose already has an active row");
} else if installed < DEFAULT_COUNT {
debug!(
installed,
total = DEFAULT_COUNT,
"partial default-policy install — some purposes already had operator policies"
);
}
if let Err(e) = sanity_check_active_set(active_policies_ks).await {
warn!(error = %e, "default-policy install did not yield a full active set");
}
Ok(installed)
}
async fn sanity_check_active_set(active_policies_ks: &KeyspaceHandle) -> Result<(), AppError> {
let mut missing = Vec::new();
for purpose in PolicyPurpose::ALL {
if get_active_policy_id(active_policies_ks, purpose)
.await?
.is_none()
{
missing.push(purpose.as_str());
}
}
if missing.is_empty() {
Ok(())
} else {
Err(AppError::Internal(format!(
"purposes left without an active policy after install_defaults: {}",
missing.join(", ")
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::engine::{compile as compile_policy, evaluate};
use serde_json::json;
use vti_common::config::StoreConfig;
use vti_common::store::Store;
async fn temp_keyspaces() -> (KeyspaceHandle, KeyspaceHandle, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("tempdir");
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.expect("store");
let policies_ks = store.keyspace("policies").expect("policies ks");
let active_ks = store.keyspace("active_policies").expect("active ks");
(policies_ks, active_ks, dir)
}
#[test]
fn defaults_cover_every_purpose() {
assert_eq!(DEFAULT_SOURCES.len(), DEFAULT_COUNT);
for purpose in PolicyPurpose::ALL {
assert!(
DEFAULT_SOURCES.iter().any(|(p, _)| *p == purpose),
"no default policy shipped for {purpose:?}"
);
}
}
#[test]
fn every_default_compiles() {
for (purpose, source) in DEFAULT_SOURCES {
let id = Uuid::new_v4();
compile_policy(source, id).unwrap_or_else(|e| {
panic!(
"default policy for {} failed to compile: {e}",
purpose.as_str()
)
});
}
}
#[tokio::test]
async fn install_defaults_is_idempotent() {
let (policies_ks, active_ks, _dir) = temp_keyspaces().await;
let installed = install_defaults(&policies_ks, &active_ks).await.unwrap();
assert_eq!(installed, DEFAULT_COUNT);
for purpose in PolicyPurpose::ALL {
assert!(
get_active_policy_id(&active_ks, purpose)
.await
.unwrap()
.is_some(),
"purpose {purpose:?} should have an active row"
);
}
let again = install_defaults(&policies_ks, &active_ks).await.unwrap();
assert_eq!(again, 0, "second install must be a no-op");
}
#[tokio::test]
async fn install_defaults_preserves_operator_policies() {
let (policies_ks, active_ks, _dir) = temp_keyspaces().await;
let operator_source = "package vtc.join\nimport rego.v1\n\ndefault allow := false\n";
let operator_id = Uuid::new_v4();
let compiled = compile_policy(operator_source, operator_id).unwrap();
let sha = *compiled.source_sha256();
let mut operator_policy = new_policy(
PolicyPurpose::Join,
operator_source.to_string(),
sha,
"did:key:zRealOperator".into(),
1,
);
operator_policy.id = operator_id;
operator_policy.activated_at = Some(Utc::now());
store_policy(&policies_ks, &operator_policy).await.unwrap();
set_active_policy_id(&active_ks, PolicyPurpose::Join, operator_id)
.await
.unwrap();
let installed = install_defaults(&policies_ks, &active_ks).await.unwrap();
assert_eq!(installed, DEFAULT_COUNT - 1);
assert_eq!(
get_active_policy_id(&active_ks, PolicyPurpose::Join)
.await
.unwrap(),
Some(operator_id)
);
}
fn pluck_bool(result: &serde_json::Value) -> bool {
result
.pointer("/result/0/expressions/0/value")
.and_then(|v| v.as_bool())
.unwrap_or_else(|| panic!("expected boolean expression value, got {result}"))
}
fn compile_default(purpose: PolicyPurpose) -> crate::policy::CompiledPolicy {
compile_policy(default_source(purpose), Uuid::new_v4()).expect("compile default")
}
#[test]
fn join_default_allows_well_formed_request() {
let c = compile_default(PolicyPurpose::Join);
let input = json!({
"applicant_did": "did:key:zApplicant",
"vp_claims": {},
"action": "join",
"now": "2026-05-12T00:00:00Z"
});
let r = evaluate(&c, "data.vtc.join.allow", input).unwrap();
assert!(
pluck_bool(&r),
"default join policy must allow valid submissions"
);
}
#[test]
fn join_default_denies_wrong_action() {
let c = compile_default(PolicyPurpose::Join);
let r = evaluate(&c, "data.vtc.join.allow", json!({ "action": "withdraw" })).unwrap();
assert!(!pluck_bool(&r));
}
#[test]
fn removal_default_allows_admin_removing_member() {
let c = compile_default(PolicyPurpose::Removal);
let input = json!({
"actor_did": "did:key:zAdmin",
"target_did": "did:key:zMember",
"target_role": "member",
"reason": "",
"action": "remove",
"now": "2026-05-12T00:00:00Z"
});
let r = evaluate(&c, "data.vtc.removal.allow", input).unwrap();
assert!(pluck_bool(&r));
}
#[test]
fn removal_default_min_disposition_is_tombstone() {
let c = compile_default(PolicyPurpose::Removal);
let r = evaluate(&c, "data.vtc.removal.min_disposition", json!({})).unwrap();
assert_eq!(
r.pointer("/result/0/expressions/0/value"),
Some(&json!("tombstone")),
"removal default min_disposition mirrors Phase 1's hardcoded Tombstone"
);
}
#[test]
fn removal_default_denies_admin_removing_admin() {
let c = compile_default(PolicyPurpose::Removal);
let r = evaluate(
&c,
"data.vtc.removal.allow",
json!({ "action": "remove", "target_role": "admin" }),
)
.unwrap();
assert!(!pluck_bool(&r));
}
#[test]
fn personhood_default_denies_empty_input() {
let c = compile_default(PolicyPurpose::Personhood);
let r = evaluate(
&c,
"data.vtc.personhood.allow",
json!({ "applicant_did": "did:key:zX", "vp_claims": {} }),
)
.unwrap();
assert!(
!pluck_bool(&r),
"empty input must deny — no WitnessCredential present"
);
}
#[test]
fn personhood_default_denies_vp_without_witness_credential() {
let c = compile_default(PolicyPurpose::Personhood);
let r = evaluate(
&c,
"data.vtc.personhood.allow",
json!({
"applicant_did": "did:key:zX",
"vp_claims": {
"holder": "did:key:zX",
"credentials": [
{ "type": ["VerifiableCredential"], "issuer": "did:key:zIss" }
]
}
}),
)
.unwrap();
assert!(
!pluck_bool(&r),
"VC without WitnessCredential type must deny"
);
}
#[test]
fn personhood_default_denies_witness_credential_with_empty_issuer() {
let c = compile_default(PolicyPurpose::Personhood);
let r = evaluate(
&c,
"data.vtc.personhood.allow",
json!({
"applicant_did": "did:key:zX",
"vp_claims": {
"holder": "did:key:zX",
"credentials": [
{ "type": ["VerifiableCredential", "WitnessCredential"], "issuer": "" }
]
}
}),
)
.unwrap();
assert!(
!pluck_bool(&r),
"WitnessCredential with empty issuer must deny"
);
}
#[test]
fn personhood_default_allows_witness_credential_with_issuer() {
let c = compile_default(PolicyPurpose::Personhood);
let r = evaluate(
&c,
"data.vtc.personhood.allow",
json!({
"applicant_did": "did:key:zX",
"vp_claims": {
"holder": "did:key:zX",
"credentials": [
{
"type": ["VerifiableCredential", "WitnessCredential"],
"issuer": "did:key:zWitness"
}
]
}
}),
)
.unwrap();
assert!(
pluck_bool(&r),
"WitnessCredential with non-empty issuer must allow"
);
}
#[test]
fn personhood_default_preserves_current_true_on_renewal() {
let c = compile_default(PolicyPurpose::Personhood);
let r = evaluate(
&c,
"data.vtc.personhood.allow",
json!({
"applicant_did": "did:key:zX",
"current_personhood": true,
"asserted_at_seconds_ago": 3600,
"vp_claims": { "holder": "did:key:zX", "credentials": [] }
}),
)
.unwrap();
assert!(
pluck_bool(&r),
"renewal must preserve current_personhood=true under default policy"
);
}
#[test]
fn personhood_default_renewal_denies_when_current_false_no_evidence() {
let c = compile_default(PolicyPurpose::Personhood);
let r = evaluate(
&c,
"data.vtc.personhood.allow",
json!({
"applicant_did": "did:key:zX",
"current_personhood": false,
"vp_claims": { "holder": "did:key:zX", "credentials": [] }
}),
)
.unwrap();
assert!(
!pluck_bool(&r),
"renewal with no evidence + current=false must deny"
);
}
#[test]
fn registry_default_publishes_on_join_with_tombstone_default() {
let c = compile_default(PolicyPurpose::Registry);
let publish = evaluate(&c, "data.vtc.registry.publish_on_join", json!({})).unwrap();
assert!(pluck_bool(&publish));
let default = evaluate(&c, "data.vtc.registry.default_departure", json!({})).unwrap();
assert_eq!(
default.pointer("/result/0/expressions/0/value"),
Some(&json!("tombstone")),
);
}
#[test]
fn directory_default_allows_did_and_role_only() {
let c = compile_default(PolicyPurpose::Directory);
let ok = evaluate(
&c,
"data.vtc.directory.allow",
json!({
"viewer_did": "did:key:zViewer",
"viewer_role": "member",
"target_member": { "did": "did:key:zTarget" },
"fields_requested": ["did", "role"],
"action": "show"
}),
)
.unwrap();
assert!(pluck_bool(&ok));
let denied = evaluate(
&c,
"data.vtc.directory.allow",
json!({
"viewer_did": "did:key:zViewer",
"viewer_role": "member",
"target_member": { "did": "did:key:zTarget" },
"fields_requested": ["did", "role", "email"],
"action": "show"
}),
)
.unwrap();
assert!(
!pluck_bool(&denied),
"directory default must deny when extra fields are requested"
);
}
#[test]
fn role_definitions_default_matches_spec_matrix() {
let c = compile_default(PolicyPurpose::RoleDefinitions);
let cases: &[(&str, &str, bool)] = &[
("admin", "edit_community_profile", true),
("admin", "author_policies", true),
("admin", "promote_to_admin", true),
("moderator", "approve_join", true),
("moderator", "remove_member", true),
("moderator", "edit_community_profile", false),
("moderator", "promote_to_admin", false),
("issuer", "issue_community_credential", true),
("issuer", "remove_member", false),
("member", "self_remove", true),
("member", "renew_vmc", true),
("member", "remove_member", false),
("member", "approve_join", false),
("custom:editor", "renew_vmc", false),
];
for (role, action, expected) in cases {
let r = evaluate(
&c,
"data.vtc.role_definitions.allow",
json!({ "role": role, "action": action }),
)
.unwrap();
assert_eq!(
pluck_bool(&r),
*expected,
"role={role} action={action}: expected allow={expected}"
);
}
}
#[test]
fn cross_community_roles_default_denies_everything() {
let c = compile_default(PolicyPurpose::CrossCommunityRoles);
let r = evaluate(
&c,
"data.vtc.cross_community_roles.allow",
json!({
"foreign_vec": { "issuer": "did:webvh:peer.example", "role": "admin" },
"target_role": "admin",
"vtc_state": {}
}),
)
.unwrap();
assert!(!pluck_bool(&r));
}
#[test]
fn cross_community_relationships_default_denies_everything() {
let c = compile_default(PolicyPurpose::CrossCommunityRelationships);
let r = evaluate(
&c,
"data.vtc.cross_community_relationships.allow",
json!({
"vrc": { "issuer": "did:webvh:peer.example" },
"viewer_member": { "did": "did:key:zViewer" },
"vtc_state": {}
}),
)
.unwrap();
assert!(!pluck_bool(&r));
}
#[test]
fn relationships_default_requires_both_parties_current() {
let c = compile_default(PolicyPurpose::Relationships);
let both = evaluate(
&c,
"data.vtc.relationships.allow",
json!({
"vrc": {},
"issuer_member": { "did": "did:key:zIssuer", "is_current": true },
"subject_member": { "did": "did:key:zSubject", "is_current": true },
"action": "publish"
}),
)
.unwrap();
assert!(pluck_bool(&both));
let only_one = evaluate(
&c,
"data.vtc.relationships.allow",
json!({
"vrc": {},
"issuer_member": { "did": "did:key:zIssuer", "is_current": true },
"subject_member": { "did": "did:key:zSubject", "is_current": false },
"action": "publish"
}),
)
.unwrap();
assert!(
!pluck_bool(&only_one),
"relationships default must deny when one party isn't a member"
);
}
}