test(authz): pin the editor role-floor on group pubsub + queue write caps
Mirrors group_kv_caps_resolve_by_role_up_the_chain for the two other group write capabilities: a Viewer is denied GroupPubsubPublish and GroupQueueEnqueue, an Editor is allowed, and an outsider is denied. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1382,6 +1382,64 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// §11.6 D2/D3: shared-topic PUBLISH and shared-queue ENQUEUE are writes, so
|
||||||
|
/// they require editor+ on the owning group. The three older group data caps
|
||||||
|
/// (KV/docs/files) have this test; these two — the newest, and the ones whose
|
||||||
|
/// blast radius is an entire subtree's handlers — did not. A `role_satisfies`
|
||||||
|
/// regression that let a Viewer publish/enqueue would go uncaught otherwise.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn group_pubsub_and_queue_write_caps_require_editor() {
|
||||||
|
let repo = InMemoryAuthzRepo::default();
|
||||||
|
let root = GroupId::new();
|
||||||
|
let team = GroupId::new();
|
||||||
|
repo.add_group(root, None).await;
|
||||||
|
repo.add_group(team, Some(root)).await;
|
||||||
|
|
||||||
|
// A Viewer at root is denied BOTH writes on a descendant group.
|
||||||
|
let viewer = principal(InstanceRole::Member);
|
||||||
|
repo.grant_group(viewer.user_id, root, AppRole::Viewer)
|
||||||
|
.await;
|
||||||
|
for cap in [
|
||||||
|
Capability::GroupPubsubPublish(team),
|
||||||
|
Capability::GroupQueueEnqueue(team),
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
can(&repo, &viewer, cap).await.unwrap(),
|
||||||
|
Decision::Deny,
|
||||||
|
"a Viewer must not publish/enqueue into a shared group collection"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Editor at root is allowed both (resolved up the chain).
|
||||||
|
let editor = principal(InstanceRole::Member);
|
||||||
|
repo.grant_group(editor.user_id, root, AppRole::Editor)
|
||||||
|
.await;
|
||||||
|
for cap in [
|
||||||
|
Capability::GroupPubsubPublish(team),
|
||||||
|
Capability::GroupQueueEnqueue(team),
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
can(&repo, &editor, cap).await.unwrap().is_allow(),
|
||||||
|
"an Editor on the owning group's ancestor must be allowed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An unrelated member gets neither.
|
||||||
|
let outsider = principal(InstanceRole::Member);
|
||||||
|
assert_eq!(
|
||||||
|
can(&repo, &outsider, Capability::GroupPubsubPublish(team))
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
Decision::Deny
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
can(&repo, &outsider, Capability::GroupQueueEnqueue(team))
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
Decision::Deny
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn group_docs_caps_resolve_by_role_up_the_chain() {
|
async fn group_docs_caps_resolve_by_role_up_the_chain() {
|
||||||
// §11.6 docs: same trust shape as shared KV — read is viewer+, write is
|
// §11.6 docs: same trust shape as shared KV — read is viewer+, write is
|
||||||
|
|||||||
Reference in New Issue
Block a user