Add KV prefix capability matching

This commit is contained in:
Eric Wendland 2026-05-17 18:26:30 +02:00
commit 02444a7cfd
5 changed files with 116 additions and 6 deletions

View file

@ -232,7 +232,14 @@ pub fn explain_from_view(
) -> AuthExplanation {
let group_principals = groups_for_subject(view, &resource, &subject);
for grant in view.grants.values() {
if grant.resource != resource || !grant.capabilities.contains(&capability) {
let Some(granted_capability) = grant
.capabilities
.iter()
.find(|granted| capability_allows(granted, &capability))
else {
continue;
};
if grant.resource != resource {
continue;
}
if grant.principal == subject {
@ -241,7 +248,10 @@ pub fn explain_from_view(
resource: resource.to_string(),
capability: capability.to_string(),
allowed: true,
reason: format!("direct grant {} allows capability", grant.id),
reason: format!(
"direct grant {} allows capability via {}",
grant.id, granted_capability
),
evaluated_ops,
};
}
@ -251,7 +261,10 @@ pub fn explain_from_view(
resource: resource.to_string(),
capability: capability.to_string(),
allowed: true,
reason: format!("group grant {} allows capability", grant.id),
reason: format!(
"group grant {} allows capability via {}",
grant.id, granted_capability
),
evaluated_ops,
};
}
@ -267,6 +280,37 @@ pub fn explain_from_view(
}
}
pub fn capability_allows(granted: &Capability, requested: &Capability) -> bool {
if granted == requested {
return true;
}
match (granted.as_str(), requested.as_str()) {
("kv.write", requested)
if requested == "kv.write"
|| requested.starts_with("kv.write_key:")
|| requested.starts_with("kv.write_prefix:") =>
{
true
}
(granted, requested) => {
let Some(granted_prefix) = granted.strip_prefix("kv.write_prefix:") else {
return false;
};
if let Some(requested_key) = requested.strip_prefix("kv.write_key:") {
return requested_key.starts_with(granted_prefix);
}
if let Some(requested_prefix) = requested.strip_prefix("kv.write_prefix:") {
return requested_prefix.starts_with(granted_prefix);
}
false
}
}
}
fn groups_for_subject(
view: &AuthView,
resource: &ResourceId,
@ -520,4 +564,57 @@ mod tests {
);
assert!(view.bearer_access.is_empty());
}
#[test]
fn prefix_scoped_kv_capabilities_allow_only_matching_keys() {
assert!(capability_allows(
&Capability::from("kv.write_prefix:apps/foo/"),
&Capability::from("kv.write_key:apps/foo/config"),
));
assert!(capability_allows(
&Capability::from("kv.write_prefix:apps/foo/"),
&Capability::from("kv.write_prefix:apps/foo/nested/"),
));
assert!(!capability_allows(
&Capability::from("kv.write_prefix:apps/foo/"),
&Capability::from("kv.write_key:apps/bar/config"),
));
assert!(!capability_allows(
&Capability::from("kv.write_prefix:apps/foo/"),
&Capability::from("kv.read"),
));
assert!(capability_allows(
&Capability::from("kv.write"),
&Capability::from("kv.write_key:apps/bar/config"),
));
}
#[test]
fn auth_explain_uses_prefix_scoped_kv_capabilities() {
let ops = vec![op(
1,
AuthOpKind::GrantCreate {
grant_id: "grant:kv-prefix".to_owned(),
principal: "node:laptop".into(),
capabilities: vec!["kv.write_prefix:apps/foo/".into()],
},
)];
let allowed = explain_auth_ops(
&ops,
"node:laptop".into(),
"resource:notes".into(),
"kv.write_key:apps/foo/config".into(),
);
assert!(allowed.allowed);
assert!(allowed.reason.contains("kv.write_prefix:apps/foo/"));
let denied = explain_auth_ops(
&ops,
"node:laptop".into(),
"resource:notes".into(),
"kv.write_key:apps/bar/config".into(),
);
assert!(!denied.allowed);
}
}