Add KV prefix capability matching
This commit is contained in:
parent
6c85a341b8
commit
02444a7cfd
5 changed files with 116 additions and 6 deletions
|
|
@ -122,7 +122,9 @@ Roadmap items should be actionable and checkable:
|
|||
read-only SQLite schema summary/hash. cr-sqlite loading, change extraction,
|
||||
and sync are still roadmap work.
|
||||
- KV stores support local SQLite-backed create/set/get. Iroh Documents
|
||||
replication and prefix-capability enforcement are still roadmap work.
|
||||
replication and command-level prefix-capability enforcement are still roadmap
|
||||
work. The auth evaluator already understands `kv.write_prefix:<prefix>`
|
||||
grants for `kv.write_key:<key>` requests.
|
||||
- Document resources can be registered locally with empty JSON state and
|
||||
local-only status. Automerge editing/state and sync are still roadmap work.
|
||||
- Pubsub supports local daemon-lifetime publish/subscribe snapshots through a
|
||||
|
|
|
|||
|
|
@ -118,7 +118,10 @@ Everything meaningful is modeled as a resource. Planned resource kinds are:
|
|||
- `ssh-proxy`: authorized SSH proxy/admin access over Iroh
|
||||
|
||||
Authorization is resource-scoped and capability-based. Bearer secrets may grant
|
||||
specific resource capabilities but do not create trusted node identity.
|
||||
specific resource capabilities but do not create trusted node identity. The auth
|
||||
evaluator supports scoped KV write grants such as `kv.write_prefix:apps/foo/`
|
||||
for `kv.write_key:apps/foo/config` explain checks; command-level KV enforcement
|
||||
is still future work.
|
||||
|
||||
## Local State
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,11 @@ grant/revoke operations and `geth auth explain` evaluates that local operation
|
|||
log. Signature validation, replication, and module enforcement are still future
|
||||
work.
|
||||
|
||||
Capability evaluation supports exact matches plus explicit scoped forms. For KV,
|
||||
`kv.write_prefix:<prefix>` grants writes requested as `kv.write_key:<key>` only
|
||||
when the key is under that prefix; `kv.write` remains the broad write
|
||||
capability. Command-level KV enforcement is still future work.
|
||||
|
||||
Both keychain and auth operations use `geth-codec` canonical envelopes for
|
||||
signature payloads. The envelope includes a version, an explicit signature
|
||||
namespace, and the operation payload encoded with postcard. JSON remains useful
|
||||
|
|
|
|||
|
|
@ -225,7 +225,10 @@ authorization and durable-state boundaries clear.
|
|||
Acceptance criteria:
|
||||
- `[x]` `geth kv create/set/get` works against a named local KV resource.
|
||||
- `[x]` KV metadata and entries are durable in the local SQLite store.
|
||||
- `[ ]` Prefix-scoped capabilities can allow or deny writes.
|
||||
- `[x]` The auth evaluator allows `kv.write_prefix:<prefix>` grants to
|
||||
satisfy matching `kv.write_key:<key>` requests.
|
||||
- `[x]` Tests cover allowed and denied prefix-scoped KV write explanations.
|
||||
- `[ ]` `geth kv set` enforces local capability decisions for the caller.
|
||||
- `[ ]` KV metadata is replicated through Iroh Documents.
|
||||
|
||||
- `[~]` Iroh-gossip pubsub integration.
|
||||
|
|
|
|||
Loading…
Reference in a new issue