use super::policies::{EvalContext, ResourceRef};
use super::registry::{ConfigRegistry, ConfigRegistryEntry, EvidenceRequirement};
use super::store::AuthStore;
use super::UserId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ManagedConfigDecision {
PassThrough { key: String },
Allow {
entry_id: String,
entry_version: u64,
matched_action: String,
matched_resource: String,
evidence: EvidenceRequirement,
},
Deny {
entry_id: String,
entry_version: u64,
matched_action: String,
matched_resource: String,
reason: DenyReason,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DenyReason {
NotStructurallyEligible {
is_system_owned: bool,
is_platform_scoped: bool,
},
PolicyDenied,
}
impl std::fmt::Display for DenyReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotStructurallyEligible {
is_system_owned,
is_platform_scoped,
} => write!(
f,
"caller is not structurally eligible for managed config \
(system_owned={is_system_owned}, platform_scoped={is_platform_scoped})"
),
Self::PolicyDenied => write!(f, "managed config required policy permission was denied"),
}
}
}
impl ManagedConfigDecision {
pub fn permitted(&self) -> bool {
matches!(self, Self::PassThrough { .. } | Self::Allow { .. })
}
}
pub const RESOURCE_TYPE_CONFIG_KEY: &str = "config_key";
pub const RESOURCE_TYPE_CONFIG_NAMESPACE: &str = "config_namespace";
pub struct ManagedConfigGate<'a> {
registry: &'a ConfigRegistry,
}
impl<'a> ManagedConfigGate<'a> {
pub fn new(registry: &'a ConfigRegistry) -> Self {
Self { registry }
}
pub fn check_write(
&self,
auth: &AuthStore,
actor: &UserId,
ctx: &EvalContext,
key: &str,
) -> ManagedConfigDecision {
let Some(entry) = self.lookup_governing_entry(key) else {
return ManagedConfigDecision::PassThrough {
key: key.to_string(),
};
};
if !entry.managed {
return ManagedConfigDecision::PassThrough {
key: key.to_string(),
};
}
let (kind, name) = split_required_resource(&entry.required_resource);
let matched_resource = format!("{kind}:{name}");
if !(ctx.principal_is_system_owned && ctx.principal_is_platform_scoped) {
return ManagedConfigDecision::Deny {
entry_id: entry.id.clone(),
entry_version: entry.version,
matched_action: entry.required_action.clone(),
matched_resource,
reason: DenyReason::NotStructurallyEligible {
is_system_owned: ctx.principal_is_system_owned,
is_platform_scoped: ctx.principal_is_platform_scoped,
},
};
}
let resource = ResourceRef::new(kind, name);
if !auth.check_policy_authz(actor, &entry.required_action, &resource, ctx) {
return ManagedConfigDecision::Deny {
entry_id: entry.id.clone(),
entry_version: entry.version,
matched_action: entry.required_action.clone(),
matched_resource,
reason: DenyReason::PolicyDenied,
};
}
ManagedConfigDecision::Allow {
entry_id: entry.id,
entry_version: entry.version,
matched_action: entry.required_action.clone(),
matched_resource,
evidence: entry.evidence_requirement,
}
}
fn lookup_governing_entry(&self, key: &str) -> Option<ConfigRegistryEntry> {
if let Some(e) = self.registry.get_active(key) {
if e.resource_type == RESOURCE_TYPE_CONFIG_KEY {
return Some(e);
}
}
let mut cursor = key;
while let Some(idx) = cursor.rfind('.') {
cursor = &cursor[..idx];
if let Some(e) = self.registry.get_active(cursor) {
if e.resource_type == RESOURCE_TYPE_CONFIG_NAMESPACE {
return Some(e);
}
}
}
None
}
}
fn split_required_resource(s: &str) -> (&str, &str) {
match s.split_once(':') {
Some((k, n)) if !k.is_empty() => (k, n),
_ => ("config", s),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::policies::Policy;
use crate::auth::registry::{ConfigRegistryDraft, Mutability, Sensitivity};
use crate::auth::store::PrincipalRef;
use crate::auth::{AuthConfig, Role};
use std::sync::Arc;
fn store() -> Arc<AuthStore> {
Arc::new(AuthStore::new(AuthConfig::default()))
}
fn registry_admin_ctx() -> EvalContext {
EvalContext {
principal_tenant: None,
current_tenant: None,
peer_ip: None,
mfa_present: false,
now_ms: 1_700_000_000_000,
principal_is_admin_role: true,
principal_is_system_owned: false,
principal_is_platform_scoped: true,
}
}
fn allow_all_registry(id: &str) -> Policy {
Policy::from_json_str(&format!(
r#"{{
"id": "{id}",
"version": 1,
"statements": [{{
"effect": "allow",
"actions": ["red.registry:*"],
"resources": ["registry:*"]
}}]
}}"#
))
.unwrap()
}
fn allow_config_write(id: &str, resource_glob: &str) -> Policy {
Policy::from_json_str(&format!(
r#"{{
"id": "{id}",
"version": 1,
"statements": [{{
"effect": "allow",
"actions": ["config:write"],
"resources": ["{resource_glob}"]
}}]
}}"#
))
.unwrap()
}
fn deny_config_write(id: &str, resource_glob: &str) -> Policy {
Policy::from_json_str(&format!(
r#"{{
"id": "{id}",
"version": 1,
"statements": [{{
"effect": "deny",
"actions": ["config:write"],
"resources": ["{resource_glob}"]
}}]
}}"#
))
.unwrap()
}
fn allow_all_config(id: &str) -> Policy {
Policy::from_json_str(&format!(
r#"{{
"id": "{id}",
"version": 1,
"statements": [{{
"effect": "allow",
"actions": ["config:*"],
"resources": ["*"]
}}]
}}"#
))
.unwrap()
}
fn seed_registry_admin(store: &Arc<AuthStore>) -> UserId {
store.create_user("seeder", "p", Role::Admin).unwrap();
let uid = UserId::platform("seeder");
store.put_policy(allow_all_registry("p-reg-allow")).unwrap();
store
.attach_policy(PrincipalRef::User(uid.clone()), "p-reg-allow")
.unwrap();
uid
}
fn managed_key_draft(id: &str) -> ConfigRegistryDraft {
ConfigRegistryDraft {
id: id.to_string(),
resource_type: RESOURCE_TYPE_CONFIG_KEY.into(),
schema: "string".into(),
mutability: Mutability::MutableViaGovernance,
sensitivity: Sensitivity::Internal,
managed: true,
required_action: "config:write".into(),
required_resource: format!("config:{id}"),
evidence_requirement: EvidenceRequirement::Metadata,
}
}
fn managed_namespace_draft(id: &str) -> ConfigRegistryDraft {
ConfigRegistryDraft {
id: id.to_string(),
resource_type: RESOURCE_TYPE_CONFIG_NAMESPACE.into(),
schema: "namespace".into(),
mutability: Mutability::MutableViaGovernance,
sensitivity: Sensitivity::Internal,
managed: true,
required_action: "config:write".into(),
required_resource: format!("config:{id}.*"),
evidence_requirement: EvidenceRequirement::Metadata,
}
}
fn unmanaged_key_draft(id: &str) -> ConfigRegistryDraft {
let mut d = managed_key_draft(id);
d.managed = false;
d
}
#[test]
fn registry_entry_can_mark_a_config_key_as_managed() {
let store = store();
let seeder = seed_registry_admin(&store);
let reg = ConfigRegistry::new();
let entry = reg
.register(
&store,
&seeder,
®istry_admin_ctx(),
managed_key_draft("red.config.audit.enabled"),
1_000,
)
.unwrap();
assert!(entry.managed, "draft.managed must propagate to entry");
let unmanaged = reg
.register(
&store,
&seeder,
®istry_admin_ctx(),
unmanaged_key_draft("app.feature_flag"),
1_000,
)
.unwrap();
assert!(!unmanaged.managed);
}
#[test]
fn ordinary_allow_all_user_is_denied_on_managed_key() {
let store = store();
let seeder = seed_registry_admin(&store);
store.create_user("alice", "p", Role::Admin).unwrap();
let alice = UserId::platform("alice");
store.put_policy(allow_all_config("p-allow-cfg")).unwrap();
store
.attach_policy(PrincipalRef::User(alice.clone()), "p-allow-cfg")
.unwrap();
let reg = ConfigRegistry::new();
reg.register(
&store,
&seeder,
®istry_admin_ctx(),
managed_key_draft("red.config.audit.enabled"),
1_000,
)
.unwrap();
let gate = ManagedConfigGate::new(®);
let ctx = EvalContext {
principal_tenant: None,
current_tenant: None,
peer_ip: None,
mfa_present: false,
now_ms: 1_700_000_000_001,
principal_is_admin_role: true,
principal_is_system_owned: false, principal_is_platform_scoped: true,
};
let decision = gate.check_write(&store, &alice, &ctx, "red.config.audit.enabled");
match decision {
ManagedConfigDecision::Deny {
entry_id,
matched_action,
matched_resource,
reason,
..
} => {
assert_eq!(entry_id, "red.config.audit.enabled");
assert_eq!(matched_action, "config:write");
assert_eq!(matched_resource, "config:red.config.audit.enabled");
assert!(
matches!(
reason,
DenyReason::NotStructurallyEligible {
is_system_owned: false,
is_platform_scoped: true
}
),
"got {reason:?}"
);
}
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn structurally_eligible_caller_with_matching_policy_is_allowed() {
let store = store();
let seeder = seed_registry_admin(&store);
store
.create_system_user("ops", "p", Role::Admin, None)
.expect("create system-owned user");
let ops = UserId::platform("ops");
store
.put_policy(allow_config_write(
"p-cfg-write",
"config:red.config.audit.*",
))
.unwrap();
store
.attach_policy(PrincipalRef::User(ops.clone()), "p-cfg-write")
.unwrap();
let reg = ConfigRegistry::new();
reg.register(
&store,
&seeder,
®istry_admin_ctx(),
managed_key_draft("red.config.audit.enabled"),
1_000,
)
.unwrap();
let gate = ManagedConfigGate::new(®);
let ctx = EvalContext {
principal_tenant: None,
current_tenant: None,
peer_ip: None,
mfa_present: false,
now_ms: 1_700_000_000_002,
principal_is_admin_role: true,
principal_is_system_owned: true,
principal_is_platform_scoped: true,
};
let decision = gate.check_write(&store, &ops, &ctx, "red.config.audit.enabled");
assert!(
matches!(decision, ManagedConfigDecision::Allow { .. }),
"got {decision:?}"
);
assert!(decision.permitted());
}
#[test]
fn structurally_eligible_caller_without_matching_policy_is_policy_denied() {
let store = store();
let seeder = seed_registry_admin(&store);
store
.create_system_user("ops", "p", Role::Write, None)
.unwrap();
let ops = UserId::platform("ops");
store
.put_policy(
Policy::from_json_str(
r#"{"id":"p-unrelated","version":1,"statements":[{"effect":"allow","actions":["select"],"resources":["table:public.x"]}]}"#,
)
.unwrap(),
)
.unwrap();
store
.attach_policy(PrincipalRef::User(ops.clone()), "p-unrelated")
.unwrap();
let reg = ConfigRegistry::new();
reg.register(
&store,
&seeder,
®istry_admin_ctx(),
managed_key_draft("red.config.audit.enabled"),
1_000,
)
.unwrap();
let gate = ManagedConfigGate::new(®);
let ctx = EvalContext {
principal_tenant: None,
current_tenant: None,
peer_ip: None,
mfa_present: false,
now_ms: 1_700_000_000_003,
principal_is_admin_role: false,
principal_is_system_owned: true,
principal_is_platform_scoped: true,
};
let decision = gate.check_write(&store, &ops, &ctx, "red.config.audit.enabled");
match decision {
ManagedConfigDecision::Deny { reason, .. } => {
assert!(matches!(reason, DenyReason::PolicyDenied), "got {reason:?}");
}
other => panic!("expected Deny(PolicyDenied), got {other:?}"),
}
}
#[test]
fn explicit_deny_overrides_structural_allow() {
let store = store();
let seeder = seed_registry_admin(&store);
store
.create_system_user("ops", "p", Role::Admin, None)
.unwrap();
let ops = UserId::platform("ops");
store.put_policy(allow_all_config("p-allow")).unwrap();
store
.put_policy(deny_config_write("p-deny", "config:red.config.audit.*"))
.unwrap();
store
.attach_policy(PrincipalRef::User(ops.clone()), "p-allow")
.unwrap();
store
.attach_policy(PrincipalRef::User(ops.clone()), "p-deny")
.unwrap();
let reg = ConfigRegistry::new();
reg.register(
&store,
&seeder,
®istry_admin_ctx(),
managed_key_draft("red.config.audit.enabled"),
1_000,
)
.unwrap();
let gate = ManagedConfigGate::new(®);
let ctx = EvalContext {
principal_tenant: None,
current_tenant: None,
peer_ip: None,
mfa_present: false,
now_ms: 1_700_000_000_004,
principal_is_admin_role: true,
principal_is_system_owned: true,
principal_is_platform_scoped: true,
};
let decision = gate.check_write(&store, &ops, &ctx, "red.config.audit.enabled");
match decision {
ManagedConfigDecision::Deny { reason, .. } => {
assert!(matches!(reason, DenyReason::PolicyDenied), "got {reason:?}");
}
other => panic!("expected Deny(PolicyDenied), got {other:?}"),
}
}
#[test]
fn deny_carries_resource_and_reason_for_control_event() {
let store = store();
let seeder = seed_registry_admin(&store);
store.create_user("alice", "p", Role::Admin).unwrap();
let alice = UserId::platform("alice");
let reg = ConfigRegistry::new();
let mut draft = managed_key_draft("red.config.backup.retention_days");
draft.evidence_requirement = EvidenceRequirement::Full;
reg.register(&store, &seeder, ®istry_admin_ctx(), draft, 1_000)
.unwrap();
let gate = ManagedConfigGate::new(®);
let ctx = EvalContext {
principal_tenant: None,
current_tenant: None,
peer_ip: None,
mfa_present: false,
now_ms: 1_700_000_000_005,
principal_is_admin_role: true,
principal_is_system_owned: false,
principal_is_platform_scoped: true,
};
let decision = gate.check_write(&store, &alice, &ctx, "red.config.backup.retention_days");
match decision {
ManagedConfigDecision::Deny {
entry_id,
entry_version,
matched_action,
matched_resource,
reason,
} => {
assert_eq!(entry_id, "red.config.backup.retention_days");
assert_eq!(entry_version, 1);
assert_eq!(matched_action, "config:write");
assert_eq!(matched_resource, "config:red.config.backup.retention_days");
let rendered = reason.to_string();
assert!(
rendered.contains("structurally eligible"),
"reason should be Control-Event-renderable: {rendered}"
);
}
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn non_managed_application_config_passes_through() {
let store = store();
let _ = seed_registry_admin(&store);
let reg = ConfigRegistry::new();
let gate = ManagedConfigGate::new(®);
let ctx = EvalContext::default();
let alice = UserId::platform("alice");
let d = gate.check_write(&store, &alice, &ctx, "app.feature_flag");
assert!(matches!(d, ManagedConfigDecision::PassThrough { .. }));
assert!(d.permitted());
}
#[test]
fn unmanaged_registry_entry_also_passes_through() {
let store = store();
let seeder = seed_registry_admin(&store);
let reg = ConfigRegistry::new();
reg.register(
&store,
&seeder,
®istry_admin_ctx(),
unmanaged_key_draft("app.feature_flag"),
1_000,
)
.unwrap();
let gate = ManagedConfigGate::new(®);
let ctx = EvalContext::default();
let alice = UserId::platform("alice");
let d = gate.check_write(&store, &alice, &ctx, "app.feature_flag");
assert!(matches!(d, ManagedConfigDecision::PassThrough { .. }));
}
#[test]
fn namespace_entry_governs_descendant_keys() {
let store = store();
let seeder = seed_registry_admin(&store);
let reg = ConfigRegistry::new();
reg.register(
&store,
&seeder,
®istry_admin_ctx(),
managed_namespace_draft("red.config.audit"),
1_000,
)
.unwrap();
let gate = ManagedConfigGate::new(®);
let alice = UserId::platform("alice");
let ctx = EvalContext {
principal_is_system_owned: false,
principal_is_platform_scoped: true,
..EvalContext::default()
};
for key in [
"red.config.audit.enabled",
"red.config.audit.sink.kafka.brokers",
] {
let d = gate.check_write(&store, &alice, &ctx, key);
match d {
ManagedConfigDecision::Deny {
entry_id,
matched_resource,
..
} => {
assert_eq!(entry_id, "red.config.audit");
assert_eq!(matched_resource, "config:red.config.audit.*");
}
other => panic!("expected Deny for {key}, got {other:?}"),
}
}
let d = gate.check_write(&store, &alice, &ctx, "red.config.storage.tier");
assert!(
matches!(d, ManagedConfigDecision::PassThrough { .. }),
"got {d:?}"
);
}
#[test]
fn exact_key_entry_wins_over_namespace_entry() {
let store = store();
let seeder = seed_registry_admin(&store);
let reg = ConfigRegistry::new();
let mut ns = managed_namespace_draft("red.config.audit");
ns.managed = false;
reg.register(&store, &seeder, ®istry_admin_ctx(), ns, 1_000)
.unwrap();
reg.register(
&store,
&seeder,
®istry_admin_ctx(),
managed_key_draft("red.config.audit.enabled"),
1_000,
)
.unwrap();
let gate = ManagedConfigGate::new(®);
let alice = UserId::platform("alice");
let ctx = EvalContext {
principal_is_system_owned: false,
principal_is_platform_scoped: true,
..EvalContext::default()
};
let d = gate.check_write(&store, &alice, &ctx, "red.config.audit.enabled");
assert!(matches!(d, ManagedConfigDecision::Deny { .. }), "got {d:?}");
let d = gate.check_write(&store, &alice, &ctx, "red.config.audit.sink");
assert!(
matches!(d, ManagedConfigDecision::PassThrough { .. }),
"got {d:?}"
);
}
#[test]
fn entry_with_unrelated_resource_type_does_not_gate_config_writes() {
let store = store();
let seeder = seed_registry_admin(&store);
let reg = ConfigRegistry::new();
let mut d = managed_key_draft("red.config.audit.enabled");
d.resource_type = "vault_path".into();
reg.register(&store, &seeder, ®istry_admin_ctx(), d, 1_000)
.unwrap();
let gate = ManagedConfigGate::new(®);
let alice = UserId::platform("alice");
let ctx = EvalContext::default();
let dec = gate.check_write(&store, &alice, &ctx, "red.config.audit.enabled");
assert!(
matches!(dec, ManagedConfigDecision::PassThrough { .. }),
"got {dec:?}"
);
}
#[test]
fn split_required_resource_handles_bare_string() {
assert_eq!(
split_required_resource("config:red.config.audit.enabled"),
("config", "red.config.audit.enabled")
);
assert_eq!(
split_required_resource("red.config.audit.enabled"),
("config", "red.config.audit.enabled")
);
assert_eq!(
split_required_resource(":only-name"),
("config", ":only-name")
);
}
}