//! 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("Created hello v1")); } #[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); common::pic_as(&member_env) .args(["scripts", "invoke", &id]) .assert() .success(); } #[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")); }