use super::policy::{ALL_OPS, Config, Grant, Op, ResolvedPolicy, SubjectName};
use super::schema::{Catalog, Class};
use crate::actor::{
AuthenticatedActor, PresenterInfo, SubjectResolutionError, TransportInfo, resolve_local_actor,
resolve_unix_actor as resolve_unix_authenticated_actor,
};
use crate::peer::PeerInfo;
#[derive(Debug, Clone, Copy)]
pub struct Pdp<'a> {
catalog: &'a Catalog,
policy: &'a ResolvedPolicy,
config: &'a Config,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Allow {
via: AllowVia,
},
Deny {
reason: DenyReason,
},
}
impl Decision {
#[must_use]
pub const fn is_allow(&self) -> bool {
matches!(self, Self::Allow { .. })
}
#[must_use]
pub const fn is_deny(&self) -> bool {
matches!(self, Self::Deny { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AllowVia {
Subject(String),
PublicClass,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DenyReason {
UnknownKey,
NotWritable,
IssuerRawSign,
NotPermitted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Explanation {
pub decision: Decision,
pub matched: Option<MatchedRule>,
}
impl Explanation {
const fn deny(reason: DenyReason) -> Self {
Self {
decision: Decision::Deny { reason },
matched: None,
}
}
fn allow(subject: &str, grant: &Grant) -> Self {
Self {
decision: Decision::Allow {
via: AllowVia::Subject(subject.to_string()),
},
matched: Some(MatchedRule {
rule_id: grant.rule_id.clone(),
via: AllowVia::Subject(subject.to_string()),
subject: subject.to_string(),
action: grant.action.clone(),
target: grant.target.source(),
}),
}
}
#[must_use]
pub const fn is_allow(&self) -> bool {
self.decision.is_allow()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MatchedRule {
pub rule_id: String,
pub via: AllowVia,
pub subject: String,
pub action: String,
pub target: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EffectiveGrant {
pub key: String,
pub op: Op,
pub via: AllowVia,
pub rule_id: Option<String>,
}
impl<'a> Pdp<'a> {
#[must_use]
pub const fn new(catalog: &'a Catalog, policy: &'a ResolvedPolicy, config: &'a Config) -> Self {
Self {
catalog,
policy,
config,
}
}
pub fn resolve_local_actor(
&self,
peer: &PeerInfo,
) -> Result<AuthenticatedActor, SubjectResolutionError> {
resolve_local_actor(self.policy, self.config, peer)
}
pub fn resolve_unix_actor(
&self,
uid: u32,
) -> Result<AuthenticatedActor, SubjectResolutionError> {
let peer = PeerInfo {
uid: Some(uid),
..PeerInfo::default()
};
resolve_unix_authenticated_actor(self.policy, self.config, &peer, uid)
}
#[must_use]
pub fn resolve_subject_actor(&self, subject: &str) -> Option<AuthenticatedActor> {
self.policy
.subjects
.contains_key(subject)
.then(|| AuthenticatedActor {
subject: subject.to_string(),
authenticated_by: Vec::new(),
presenter: PresenterInfo {
pid: None,
uid: None,
gid: None,
executable_path: None,
display_label: Some("policy-inspection".to_string()),
},
transport: TransportInfo::default(),
})
}
#[must_use]
pub fn decide(&self, actor: &AuthenticatedActor, op: Op, key: &str) -> Decision {
self.evaluate(actor, op, key).decision
}
#[must_use]
pub fn decide_admin(&self, actor: &AuthenticatedActor, op: Op) -> Decision {
let Some(key) = admin_target(op) else {
return Decision::Deny {
reason: DenyReason::NotPermitted,
};
};
for rule in &self.policy.rules {
if let Some(subject) = matching_rule_subject(&rule.subjects, actor.subject.as_str())
&& rule
.grants
.iter()
.any(|g| g.op == op && g.target.matches(key))
{
return Decision::Allow {
via: AllowVia::Subject(subject.to_string()),
};
}
}
Decision::Deny {
reason: DenyReason::NotPermitted,
}
}
#[must_use]
pub fn explain(&self, actor: &AuthenticatedActor, op: Op, key: &str) -> Explanation {
self.evaluate(actor, op, key)
}
#[must_use]
pub fn explain_subject(&self, subject: &str, op: Op, key: &str) -> Explanation {
self.resolve_subject_actor(subject).map_or_else(
|| self.evaluate_without_actor(op, key),
|actor| self.evaluate(&actor, op, key),
)
}
fn evaluate(&self, actor: &AuthenticatedActor, op: Op, key: &str) -> Explanation {
let Some(entry) = self.catalog.keys.get(key) else {
return Explanation::deny(DenyReason::UnknownKey);
};
if op.is_write() && !entry.writable {
return Explanation::deny(DenyReason::NotWritable);
}
if op == Op::Sign && entry.is_credential_issuer() {
return Explanation::deny(DenyReason::IssuerRawSign);
}
for rule in &self.policy.rules {
if let Some(subject) = matching_rule_subject(&rule.subjects, actor.subject.as_str())
&& let Some(grant) = rule
.grants
.iter()
.find(|g| g.op == op && g.target.matches(key))
{
return Explanation::allow(subject, grant);
}
}
if entry.class == Class::Public && is_public_read(op) {
return Explanation {
decision: Decision::Allow {
via: AllowVia::PublicClass,
},
matched: None,
};
}
Explanation::deny(DenyReason::NotPermitted)
}
fn evaluate_without_actor(&self, op: Op, key: &str) -> Explanation {
let Some(entry) = self.catalog.keys.get(key) else {
return Explanation::deny(DenyReason::UnknownKey);
};
if op.is_write() && !entry.writable {
return Explanation::deny(DenyReason::NotWritable);
}
if op == Op::Sign && entry.is_credential_issuer() {
return Explanation::deny(DenyReason::IssuerRawSign);
}
Explanation::deny(DenyReason::NotPermitted)
}
#[must_use]
pub fn effective(&self, subject: &str) -> Vec<EffectiveGrant> {
let mut out = Vec::new();
let Some(actor) = self.resolve_subject_actor(subject) else {
return out;
};
for key in self.catalog.keys.keys() {
for op in ALL_OPS {
let ex = self.explain(&actor, op, key);
if let Decision::Allow { via } = ex.decision {
out.push(EffectiveGrant {
key: key.clone(),
op,
via,
rule_id: ex.matched.map(|m| m.rule_id),
});
}
}
}
out.sort_by(|a, b| (a.key.as_str(), a.op.token()).cmp(&(b.key.as_str(), b.op.token())));
out
}
#[must_use]
pub fn user_label(&self, uid: u32) -> String {
self.config.user_name_num(uid)
}
#[must_use]
pub fn group_label(&self, gid: u32) -> String {
self.config.group_name_num(gid)
}
}
pub const ADMIN_RELOAD_TARGET: &str = "broker.reload";
pub const ADMIN_EXPLAIN_TARGET: &str = "broker.explain";
pub const ADMIN_REVOKE_TARGET: &str = "broker.revoke";
pub const ADMIN_WATCH_TARGET: &str = "broker.watch";
const fn admin_target(op: Op) -> Option<&'static str> {
match op {
Op::Reload => Some(ADMIN_RELOAD_TARGET),
Op::Explain => Some(ADMIN_EXPLAIN_TARGET),
Op::Revoke => Some(ADMIN_REVOKE_TARGET),
Op::Watch => Some(ADMIN_WATCH_TARGET),
Op::Get
| Op::List
| Op::GetPublicKey
| Op::Verify
| Op::Sign
| Op::Encrypt
| Op::Decrypt
| Op::Mint
| Op::SignNatsJwt
| Op::ValidateNatsJwt
| Op::EncryptNatsCurve
| Op::DecryptNatsCurve
| Op::Validate
| Op::Set
| Op::Rotate
| Op::Import
| Op::NewKey
| Op::UseSoftwareCustody => None,
}
}
const fn is_public_read(op: Op) -> bool {
matches!(op, Op::Get | Op::GetPublicKey)
}
fn matching_rule_subject<'a>(
rule_subjects: &'a [SubjectName],
actor_subject: &str,
) -> Option<&'a str> {
rule_subjects
.iter()
.find(|subject| subject.as_str() == actor_subject)
.map(String::as_str)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::load;
const CATALOG: &str = r#"{
"schemaVersion": 1,
"backends": { "bao": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
"keys": {
"nats.account": {
"class": "asymmetric", "keyType": "ed25519-nkey", "backend": "bao",
"path": "nats", "writable": true, "missing": "error",
"labels": ["nats_type=A"], "description": "account signing key"
},
"web.tls.ca_cert": {
"class": "public", "backend": "bao", "engine": "kv2",
"path": "secret/data/web/tls/ca-cert", "writable": false,
"missing": "warn", "description": "public CA cert"
},
"grafana.admin_password": {
"class": "value", "backend": "bao", "engine": "kv2",
"path": "secret/data/grafana/admin", "writable": true,
"missing": "error", "description": "grafana admin value"
},
"locked.value": {
"class": "value", "backend": "bao", "engine": "kv2",
"path": "secret/data/locked", "writable": false,
"missing": "warn", "description": "externally managed value"
},
"enroll.sealing": {
"class": "sealing", "keyType": "x25519", "backend": "bao", "engine": "kv2",
"path": "secret/data/enroll/x25519",
"publicPath": "secret/data/enroll/x25519-public", "writable": true,
"missing": "error", "description": "x25519 enrollment sealing key"
}
}
}"#;
const ROLES: &str = r#"
"reader": ["get", "list", "get_public_key"],
"signer": ["sign", "verify", "get_public_key"],
"minter": ["mint", "get_public_key"],
"sealer": ["decrypt", "encrypt", "get_public_key"],
"operator": ["set", "rotate", "import", "new_key"]
"#;
const SUBJECTS: &str = r#"
"svc.grafana": { "allOf": [ { "kind": "unix", "uid": 9002 } ] },
"svc.nats": { "allOf": [ { "kind": "unix", "uid": 9003 } ] },
"svc.minter": { "allOf": [ { "kind": "unix", "uid": 9004 } ] },
"svc.enroll": { "allOf": [ { "kind": "unix", "uid": 9005 } ] },
"svc.reload": { "allOf": [ { "kind": "unix", "uid": 9006 } ] },
"svc.revoke": { "allOf": [ { "kind": "unix", "uid": 9007 } ] },
"svc.watch": { "allOf": [ { "kind": "unix", "uid": 9008 } ] },
"ops.wheel": { "allOf": [ { "kind": "unix", "gid": 10 } ] },
"breakglass.root": { "breakGlass": true, "allOf": [ { "kind": "unix", "uid": 0 } ] }
"#;
const RULES: &str = r#"
{ "id": "grafana-reader", "subjects": ["svc.grafana"], "action": ["role:reader"], "target": ["grafana.admin_password"] },
{ "id": "nats-signer", "subjects": ["svc.nats"], "action": ["role:signer"], "target": ["nats.account"] },
{ "id": "nats-minter", "subjects": ["svc.minter"], "action": ["role:minter"], "target": ["nats.account"] },
{ "id": "enroll-sealer", "subjects": ["svc.enroll"], "action": ["role:sealer"], "target": ["enroll.sealing"] },
{ "id": "wheel-operator", "subjects": ["ops.wheel"], "action": ["role:operator"], "target": ["grafana.**", "locked.value", "web.tls.ca_cert"] },
{ "id": "reload-admin", "subjects": ["svc.reload"], "action": ["op:reload"], "target": ["broker.reload"] },
{ "id": "reload-wheel", "subjects": ["ops.wheel"], "action": ["op:reload"], "target": ["broker.reload"] },
{ "id": "revoke-admin", "subjects": ["svc.revoke"], "action": ["op:revoke"], "target": ["broker.revoke"] },
{ "id": "watch-admin", "subjects": ["svc.watch"], "action": ["op:watch"], "target": ["broker.watch"] },
{ "id": "root-all", "subjects": ["breakglass.root"], "action": ["*"], "target": ["*"] }
"#;
const CONFIG: &str = r#"
"names": {
"users": { "0": "root", "9002": "svc-grafana", "9003": "svc-nats", "9004": "svc-minter", "9005": "svc-enroll", "9006": "svc-admin", "9007": "svc-revoke", "9008": "svc-watch", "5000": "alice" },
"groups": { "10": "wheel" }
},
"memberships": { "5000": [5000, 10], "9002": [9002], "9003": [9003], "9004": [9004], "9005": [9005], "9006": [9006], "9007": [9007], "9008": [9008] }
"#;
fn policy_json() -> String {
format!(
r#"{{
"schemaVersion": 2,
"subjects": {{ {SUBJECTS} }},
"roles": {{ {ROLES} }},
"rules": [ {RULES} ],
"config": {{ {CONFIG} }}
}}"#
)
}
fn via(subject: &str) -> AllowVia {
AllowVia::Subject(subject.to_string())
}
fn fixture() -> (super::Catalog, super::ResolvedPolicy, Config) {
let pol = policy_json();
let (catalog, resolved, config, warnings) =
load(CATALOG, &pol).expect("fixture loads cleanly");
assert!(
warnings.is_empty(),
"fixture should have no warnings: {warnings:?}"
);
(catalog, resolved, config)
}
fn decide(pdp: &Pdp<'_>, uid: u32, op: Op, key: &str) -> Decision {
pdp.resolve_unix_actor(uid).map_or_else(
|_| Decision::Deny {
reason: DenyReason::NotPermitted,
},
|actor| pdp.decide(&actor, op, key),
)
}
fn explain(pdp: &Pdp<'_>, uid: u32, op: Op, key: &str) -> Explanation {
pdp.resolve_unix_actor(uid).map_or_else(
|_| Explanation::deny(DenyReason::NotPermitted),
|actor| pdp.explain(&actor, op, key),
)
}
fn decide_admin(pdp: &Pdp<'_>, uid: u32, op: Op) -> Decision {
pdp.resolve_unix_actor(uid).map_or_else(
|_| Decision::Deny {
reason: DenyReason::NotPermitted,
},
|actor| pdp.decide_admin(&actor, op),
)
}
#[test]
fn allow_via_user_rule() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide(&pdp, 9002, Op::Get, "grafana.admin_password"),
Decision::Allow {
via: via("svc.grafana")
}
);
assert_eq!(
decide(&pdp, 9002, Op::List, "grafana.admin_password"),
Decision::Allow {
via: via("svc.grafana")
}
);
}
#[test]
fn explain_all_of_subject_carries_subject_name() {
const CAT: &str = r#"{
"schemaVersion": 1,
"backends": { "bao": { "kind": "vault", "addr": "https://127.0.0.1:8200" } },
"keys": {
"grafana.admin_password": {
"class": "value", "backend": "bao", "engine": "kv2",
"path": "secret/data/grafana/admin", "writable": true,
"missing": "error", "description": "grafana admin value"
}
}
}"#;
const POL: &str = r#"{
"schemaVersion": 2,
"subjects": {
"svc.compound": { "allOf": [
{ "kind": "unix", "uid": 333 },
{ "kind": "unix", "gid": 10 }
] }
},
"roles": { "reader": ["get"] },
"rules": [
{ "id": "compound-reader", "subjects": ["svc.compound"],
"action": ["role:reader"], "target": ["grafana.admin_password"] }
],
"config": { "memberships": { "333": [333, 10] } }
}"#;
let (catalog, resolved, config, _warnings) =
load(CAT, POL).expect("compound fixture loads");
let pdp = Pdp::new(&catalog, &resolved, &config);
let ex = explain(&pdp, 333, Op::Get, "grafana.admin_password");
assert!(ex.is_allow());
let matched = ex
.matched
.expect("a rule-based allow carries its matched rule");
assert_eq!(matched.via, via("svc.compound"));
assert_eq!(matched.subject, "svc.compound");
assert!(explain(&pdp, 333, Op::Get, "grafana.admin_password").is_allow());
assert!(decide(&pdp, 334, Op::Get, "grafana.admin_password").is_deny());
}
#[test]
fn allow_via_supplementary_group() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide(&pdp, 5000, Op::Set, "grafana.admin_password"),
Decision::Allow {
via: via("ops.wheel")
}
);
}
#[test]
fn allow_public_class_read_for_resolved_subject() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide(&pdp, 9002, Op::Get, "web.tls.ca_cert"),
Decision::Allow {
via: AllowVia::PublicClass
}
);
assert_eq!(
decide(&pdp, 9002, Op::GetPublicKey, "web.tls.ca_cert"),
Decision::Allow {
via: AllowVia::PublicClass
}
);
}
#[test]
fn operator_write_allowed() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
for op in [Op::Set, Op::Rotate, Op::Import, Op::NewKey] {
assert_eq!(
decide(&pdp, 5000, op, "grafana.admin_password"),
Decision::Allow {
via: via("ops.wheel")
},
"operator {op:?} should be allowed"
);
}
}
#[test]
fn root_any_target_allowed() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide(&pdp, 0, Op::Verify, "nats.account"),
Decision::Allow {
via: via("breakglass.root")
}
);
assert_eq!(
decide(&pdp, 0, Op::Get, "grafana.admin_password"),
Decision::Allow {
via: via("breakglass.root")
}
);
assert_eq!(
decide(&pdp, 0, Op::Set, "grafana.admin_password"),
Decision::Allow {
via: via("breakglass.root")
}
);
}
#[test]
fn default_deny_for_unmatched_principal() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide(&pdp, 7777, Op::Get, "grafana.admin_password"),
Decision::Deny {
reason: DenyReason::NotPermitted
}
);
}
#[test]
fn missing_actor_never_gets_public_or_rule_based_permissions() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert!(pdp.resolve_unix_actor(424_242).is_err());
assert_eq!(
pdp.explain_subject("missing.subject", Op::Get, "web.tls.ca_cert"),
Explanation::deny(DenyReason::NotPermitted)
);
assert_eq!(
pdp.explain_subject("missing.subject", Op::Get, "grafana.admin_password"),
Explanation::deny(DenyReason::NotPermitted)
);
assert_eq!(
pdp.explain_subject("missing.subject", Op::Get, "does.not.exist"),
Explanation::deny(DenyReason::UnknownKey)
);
}
#[test]
fn non_public_key_read_denied_for_unauthorized_uid() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide(&pdp, 424_242, Op::Get, "nats.account"),
Decision::Deny {
reason: DenyReason::NotPermitted
}
);
assert_eq!(
decide(&pdp, 424_242, Op::GetPublicKey, "nats.account"),
Decision::Deny {
reason: DenyReason::NotPermitted
}
);
}
#[test]
fn write_denied_when_not_writable_even_with_operator_grant() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
for key in ["locked.value", "web.tls.ca_cert"] {
for op in [Op::Set, Op::Rotate, Op::Import, Op::NewKey] {
assert_eq!(
decide(&pdp, 5000, op, key),
Decision::Deny {
reason: DenyReason::NotWritable
},
"hard cap should deny {op:?} on non-writable {key}"
);
}
}
assert_eq!(
decide(&pdp, 0, Op::Set, "locked.value"),
Decision::Deny {
reason: DenyReason::NotWritable
}
);
}
#[test]
fn mint_allowed_via_minter_but_denied_for_signer_only() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide(&pdp, 9004, Op::Mint, "nats.account"),
Decision::Allow {
via: via("svc.minter")
}
);
assert_eq!(
decide(&pdp, 9003, Op::Verify, "nats.account"),
Decision::Allow {
via: via("svc.nats")
}
);
assert_eq!(
decide(&pdp, 9003, Op::Mint, "nats.account"),
Decision::Deny {
reason: DenyReason::NotPermitted
}
);
}
#[test]
fn issuer_raw_sign_hard_cap_denies_regardless_of_grant() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
for uid in [9003, 9004, 0] {
assert_eq!(
decide(&pdp, uid, Op::Sign, "nats.account"),
Decision::Deny {
reason: DenyReason::IssuerRawSign
},
"raw sign on an issuer key must be hard-capped for uid {uid}"
);
}
assert_eq!(
decide(&pdp, 9003, Op::Verify, "nats.account"),
Decision::Allow {
via: via("svc.nats")
}
);
assert_eq!(
decide(&pdp, 9004, Op::Mint, "nats.account"),
Decision::Allow {
via: via("svc.minter")
}
);
}
#[test]
fn unknown_key_denied() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide(&pdp, 0, Op::Get, "does.not.exist"),
Decision::Deny {
reason: DenyReason::UnknownKey
}
);
assert_eq!(
decide(&pdp, 9002, Op::Get, "does.not.exist"),
Decision::Deny {
reason: DenyReason::UnknownKey
}
);
}
#[test]
fn sealing_key_allows_wrap_unwrap_and_get_public_key_when_granted() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
for op in [Op::Encrypt, Op::Decrypt, Op::GetPublicKey] {
assert_eq!(
decide(&pdp, 9005, op, "enroll.sealing"),
Decision::Allow {
via: via("svc.enroll")
},
"granted sealer must be allowed to {op:?} a sealing key"
);
}
}
#[test]
fn sealing_key_denies_get_and_set_even_for_the_sealer() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
for op in [
Op::Get,
Op::Set,
Op::Sign,
Op::Rotate,
Op::Import,
Op::NewKey,
] {
assert_eq!(
decide(&pdp, 9005, op, "enroll.sealing"),
Decision::Deny {
reason: DenyReason::NotPermitted
},
"sealer must not be able to {op:?} a sealing key"
);
}
}
#[test]
fn sealing_key_default_denies_without_a_grant() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
for op in [Op::Decrypt, Op::GetPublicKey, Op::Get] {
assert_eq!(
decide(&pdp, 424_242, op, "enroll.sealing"),
Decision::Deny {
reason: DenyReason::NotPermitted
},
"ungranted {op:?} on a sealing key must default-deny"
);
}
}
#[test]
fn decide_admin_allows_explicit_reload_grant() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide_admin(&pdp, 9006, Op::Reload),
Decision::Allow {
via: via("svc.reload")
}
);
assert_eq!(
decide_admin(&pdp, 5000, Op::Reload),
Decision::Allow {
via: via("ops.wheel")
}
);
}
#[test]
fn decide_admin_allows_explicit_revoke_grant() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide_admin(&pdp, 9007, Op::Revoke),
Decision::Allow {
via: via("svc.revoke")
}
);
}
#[test]
fn decide_admin_allows_explicit_watch_grant() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide_admin(&pdp, 9008, Op::Watch),
Decision::Allow {
via: via("svc.watch")
}
);
for op in [Op::Reload, Op::Explain, Op::Revoke] {
assert_eq!(
decide_admin(&pdp, 9008, op),
Decision::Deny {
reason: DenyReason::NotPermitted
}
);
}
}
#[test]
fn decide_admin_default_denies_without_an_explicit_reload_grant() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(
decide_admin(&pdp, 7777, Op::Reload),
Decision::Deny {
reason: DenyReason::NotPermitted
}
);
}
#[test]
fn no_data_plane_grant_implies_admin_ops() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
for uid in [
9002, 9003, 9004, 9005, 0, ] {
for op in [Op::Reload, Op::Explain, Op::Revoke, Op::Watch] {
assert_eq!(
decide_admin(&pdp, uid, op),
Decision::Deny {
reason: DenyReason::NotPermitted
},
"uid {uid} (a data-plane/root grant) must NOT be able to {op:?}"
);
}
}
}
#[test]
fn breakglass_data_plane_wildcard_still_excludes_admin_ops() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
let actor = pdp
.resolve_unix_actor(0)
.expect("root breakglass subject resolves");
assert_eq!(
pdp.decide(&actor, Op::Get, "grafana.admin_password"),
Decision::Allow {
via: via("breakglass.root")
}
);
for op in [Op::Reload, Op::Explain, Op::Revoke, Op::Watch] {
assert_eq!(
pdp.decide_admin(&actor, op),
Decision::Deny {
reason: DenyReason::NotPermitted
},
"breakglass wildcard must not imply admin {op:?}"
);
}
}
#[test]
fn presenter_identity_does_not_borrow_actor_authority() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
let mut actor = pdp
.resolve_unix_actor(9002)
.expect("grafana subject resolves");
actor.presenter = PresenterInfo {
pid: None,
uid: Some(0),
gid: Some(0),
executable_path: None,
display_label: Some("root-presenter".to_string()),
};
assert_eq!(
pdp.decide(&actor, Op::Get, "grafana.admin_password"),
Decision::Allow {
via: via("svc.grafana")
}
);
assert_eq!(
pdp.decide(&actor, Op::Mint, "nats.account"),
Decision::Deny {
reason: DenyReason::NotPermitted
}
);
assert_eq!(
pdp.decide_admin(&actor, Op::Reload),
Decision::Deny {
reason: DenyReason::NotPermitted
}
);
}
#[test]
fn decision_predicates() {
let allow = Decision::Allow {
via: via("svc.grafana"),
};
let deny = Decision::Deny {
reason: DenyReason::UnknownKey,
};
assert!(allow.is_allow() && !allow.is_deny());
assert!(deny.is_deny() && !deny.is_allow());
}
#[test]
fn audit_labels_render_name_num() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
assert_eq!(pdp.user_label(9002), "svc-grafana(9002)");
assert_eq!(pdp.group_label(10), "wheel(10)");
assert_eq!(pdp.user_label(123), "123"); }
#[test]
fn explain_reports_matched_rule_on_user_allow() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
let ex = explain(&pdp, 9002, Op::Get, "grafana.admin_password");
assert!(ex.is_allow());
let m = ex.matched.expect("a rule-based allow carries provenance");
assert_eq!(m.rule_id, "grafana-reader");
assert_eq!(m.via, via("svc.grafana"));
assert_eq!(m.subject, "svc.grafana");
assert_eq!(m.action, "role:reader");
assert_eq!(m.target, "grafana.admin_password");
}
#[test]
fn explain_reports_specific_role_rule_not_a_sibling() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
let signer = explain(&pdp, 9003, Op::Verify, "nats.account");
assert_eq!(
signer.matched.as_ref().map(|m| m.rule_id.as_str()),
Some("nats-signer")
);
assert_eq!(
signer.matched.as_ref().map(|m| m.action.as_str()),
Some("role:signer")
);
let minter = explain(&pdp, 9004, Op::Mint, "nats.account");
assert_eq!(
minter.matched.as_ref().map(|m| m.rule_id.as_str()),
Some("nats-minter")
);
assert_eq!(
minter.matched.as_ref().map(|m| m.action.as_str()),
Some("role:minter")
);
}
#[test]
fn explain_reports_group_rule_with_glob_target() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
let ex = explain(&pdp, 5000, Op::Set, "grafana.admin_password");
let m = ex.matched.expect("group allow carries provenance");
assert_eq!(m.rule_id, "wheel-operator");
assert_eq!(m.via, via("ops.wheel"));
assert_eq!(m.subject, "ops.wheel");
assert_eq!(m.action, "role:operator");
assert_eq!(m.target, "grafana.**");
}
#[test]
fn explain_public_class_allow_has_no_rule() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
let ex = explain(&pdp, 9002, Op::Get, "web.tls.ca_cert");
assert_eq!(
ex.decision,
Decision::Allow {
via: AllowVia::PublicClass
}
);
assert!(
ex.matched.is_none(),
"public-class allow has no matched rule"
);
}
#[test]
fn explain_deny_is_default_deny_with_no_rule() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
let ex = explain(&pdp, 7777, Op::Get, "grafana.admin_password");
assert_eq!(
ex.decision,
Decision::Deny {
reason: DenyReason::NotPermitted
}
);
assert!(ex.matched.is_none());
assert!(
explain(&pdp, 0, Op::Get, "does.not.exist")
.matched
.is_none()
);
assert_eq!(
explain(&pdp, 0, Op::Set, "locked.value").decision,
Decision::Deny {
reason: DenyReason::NotWritable
}
);
}
#[test]
fn explain_decision_always_equals_decide() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
let uids = [0, 9002, 9003, 9004, 9005, 5000, 7777, 424_242];
let keys = [
"nats.account",
"web.tls.ca_cert",
"grafana.admin_password",
"locked.value",
"enroll.sealing",
"does.not.exist",
];
let ops = [
Op::Get,
Op::List,
Op::GetPublicKey,
Op::Verify,
Op::Sign,
Op::Encrypt,
Op::Decrypt,
Op::Mint,
Op::SignNatsJwt,
Op::Validate,
Op::Set,
Op::Rotate,
Op::Import,
Op::NewKey,
];
for uid in uids {
for &key in &keys {
for op in ops {
let decide = decide(&pdp, uid, op, key);
let explain = explain(&pdp, uid, op, key);
assert_eq!(
explain.decision, decide,
"explain/decide diverged at uid={uid} op={op:?} key={key}"
);
if let Decision::Allow { via } = explain.decision {
let has_rule = explain.matched.is_some();
assert_eq!(
has_rule,
via != AllowVia::PublicClass,
"rule provenance presence wrong at uid={uid} op={op:?} key={key}"
);
} else {
assert!(explain.matched.is_none());
}
}
}
}
}
#[test]
fn effective_lists_only_granted_pairs_with_provenance() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
let grants = pdp.effective("svc.grafana");
for g in &grants {
assert!(
decide(&pdp, 9002, g.op, &g.key).is_allow(),
"effective listed a non-granted pair: {} {:?}",
g.key,
g.op
);
}
for op in [Op::Get, Op::List, Op::GetPublicKey] {
let hit = grants
.iter()
.find(|g| g.key == "grafana.admin_password" && g.op == op)
.expect("reader grant present");
assert_eq!(hit.rule_id.as_deref(), Some("grafana-reader"));
assert_eq!(hit.via, via("svc.grafana"));
}
let public = grants
.iter()
.find(|g| g.key == "web.tls.ca_cert" && g.op == Op::Get)
.expect("public-class read present");
assert!(public.rule_id.is_none());
assert_eq!(public.via, AllowVia::PublicClass);
assert!(
!grants
.iter()
.any(|g| g.key == "grafana.admin_password" && g.op == Op::Sign)
);
}
#[test]
fn effective_is_computed_for_subjects() {
let (c, r, cfg) = fixture();
let pdp = Pdp::new(&c, &r, &cfg);
let grants = pdp.effective("ops.wheel");
let op_set = grants
.iter()
.find(|g| g.key == "grafana.admin_password" && g.op == Op::Set)
.expect("wheel subject yields the operator grant");
assert_eq!(op_set.via, via("ops.wheel"));
assert_eq!(op_set.rule_id.as_deref(), Some("wheel-operator"));
let missing = pdp.effective("missing.subject");
assert!(missing.is_empty());
}
}