- roles: member invoke asserts the script's actual stdout, not just exit 0; - api: app-slug script filter asserts the returned script's name; - workflow_orchestrator: unknown-workflow asserts WorkflowError::NotFound, not just is_err(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
152 lines
5.4 KiB
Rust
152 lines
5.4 KiB
Rust
//! RBAC mirror of the dashboard's role-shadowing specs. A Member user
|
|
//! is minted via the admin API, granted (or denied) membership on an
|
|
//! app, then `pic` is driven against the member's bearer token to
|
|
//! confirm the server's capability gates surface as expected exit
|
|
//! codes / error messages.
|
|
|
|
use predicates::prelude::*;
|
|
|
|
use crate::common;
|
|
use crate::common::cleanup::AppGuard;
|
|
use crate::common::member;
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn member_apps_ls_only_shows_their_apps() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let admin_env = common::admin_env(fx);
|
|
|
|
let slug_visible = common::unique_slug("roles-visible");
|
|
let slug_hidden = common::unique_slug("roles-hidden");
|
|
common::pic_as(&admin_env)
|
|
.args(["apps", "create", &slug_visible])
|
|
.assert()
|
|
.success();
|
|
let _g1 = AppGuard::new(&admin_env.url, &admin_env.token, &slug_visible);
|
|
common::pic_as(&admin_env)
|
|
.args(["apps", "create", &slug_hidden])
|
|
.assert()
|
|
.success();
|
|
let _g2 = AppGuard::new(&admin_env.url, &admin_env.token, &slug_hidden);
|
|
|
|
let m = member::member_user(fx, &common::unique_username("rls"));
|
|
member::grant_membership(fx, &slug_visible, &m.id, "viewer");
|
|
let member_env = common::custom_env(&fx.url, &m.token);
|
|
common::seed_credentials(&member_env, &m.username);
|
|
|
|
let out = common::pic_as(&member_env)
|
|
.args(["apps", "ls"])
|
|
.output()
|
|
.expect("apps ls");
|
|
assert!(out.status.success(), "apps ls failed: {out:?}");
|
|
let stdout = String::from_utf8(out.stdout).unwrap();
|
|
assert!(
|
|
stdout.contains(&slug_visible),
|
|
"member should see {slug_visible}, got: {stdout}"
|
|
);
|
|
assert!(
|
|
!stdout.contains(&slug_hidden),
|
|
"member should NOT see {slug_hidden}, got: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn viewer_cannot_deploy_but_editor_can() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let admin_env = common::admin_env(fx);
|
|
let slug = common::unique_slug("roles-write");
|
|
common::pic_as(&admin_env)
|
|
.args(["apps", "create", &slug])
|
|
.assert()
|
|
.success();
|
|
let _g = AppGuard::new(&admin_env.url, &admin_env.token, &slug);
|
|
|
|
let m = member::member_user(fx, &common::unique_username("vw"));
|
|
member::grant_membership(fx, &slug, &m.id, "viewer");
|
|
let member_env = common::custom_env(&fx.url, &m.token);
|
|
common::seed_credentials(&member_env, &m.username);
|
|
|
|
let fixture = common::fixture_path("hello.rhai");
|
|
common::pic_as(&member_env)
|
|
.args([
|
|
"scripts",
|
|
"deploy",
|
|
fixture.to_str().unwrap(),
|
|
"--app",
|
|
&slug,
|
|
])
|
|
.assert()
|
|
.failure()
|
|
// `Forbidden` → 403. A regressed predicate of `"HTTP 4"` would
|
|
// have masked an auth break (401) as an authz issue.
|
|
.stderr(predicate::str::contains("HTTP 403"));
|
|
|
|
// Promote to Editor and retry — the same command should now succeed.
|
|
member::update_membership(fx, &slug, &m.id, "editor");
|
|
common::pic_as(&member_env)
|
|
.args([
|
|
"scripts",
|
|
"deploy",
|
|
fixture.to_str().unwrap(),
|
|
"--app",
|
|
&slug,
|
|
])
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("hello").and(predicate::str::contains("created")));
|
|
}
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn member_can_invoke_any_script_with_id() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
// `/api/v1/execute/{id}` is the unguarded data-plane ingress — even
|
|
// a member with no app membership can hit it as long as they hold
|
|
// a valid token (the orchestrator doesn't gate it).
|
|
let admin_env = common::admin_env(fx);
|
|
let (id, _guard) = common::deploy_fixture(&admin_env, "roles-inv", "hello.rhai");
|
|
|
|
let m = member::member_user(fx, &common::unique_username("inv"));
|
|
let member_env = common::custom_env(&fx.url, &m.token);
|
|
common::seed_credentials(&member_env, &m.username);
|
|
|
|
// Assert the script actually RAN, not just that the command exited 0 — a
|
|
// no-op that printed nothing, or a 200 with an error envelope the CLI didn't
|
|
// treat as failure, would pass a bare `.success()`. `hello.rhai` emits
|
|
// "hello". (The sibling roles test one screen up does exactly this.)
|
|
common::pic_as(&member_env)
|
|
.args(["scripts", "invoke", &id])
|
|
.assert()
|
|
.success()
|
|
.stdout(predicate::str::contains("hello"));
|
|
}
|
|
|
|
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
|
#[test]
|
|
fn non_member_cannot_read_logs() {
|
|
let Some(fx) = common::fixture_or_skip() else {
|
|
return;
|
|
};
|
|
let admin_env = common::admin_env(fx);
|
|
let (id, _guard) = common::deploy_fixture(&admin_env, "roles-log", "hello.rhai");
|
|
|
|
let m = member::member_user(fx, &common::unique_username("rl"));
|
|
let member_env = common::custom_env(&fx.url, &m.token);
|
|
common::seed_credentials(&member_env, &m.username);
|
|
|
|
common::pic_as(&member_env)
|
|
.args(["logs", &id])
|
|
.assert()
|
|
.failure()
|
|
// Non-member → 403 from the authz layer, not 404 — the script
|
|
// exists; the caller just can't see it.
|
|
.stderr(predicate::str::contains("HTTP 403"));
|
|
}
|