From 516719cb942d12df166246503aa30c7e8ca33f71 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 25 May 2022 13:25:50 -0700 Subject: [PATCH 01/32] some work --- nexus/src/authn/mod.rs | 14 +- nexus/src/authz/context.rs | 1 + nexus/src/authz/mod.rs | 3 + nexus/src/authz/oso_generic.rs | 5 +- nexus/src/authz/policy_test.rs | 491 +++++++++++++++++++++++++++++++++ 5 files changed, 508 insertions(+), 6 deletions(-) create mode 100644 nexus/src/authz/policy_test.rs diff --git a/nexus/src/authn/mod.rs b/nexus/src/authn/mod.rs index 2803bd07e3..a82477a2f9 100644 --- a/nexus/src/authn/mod.rs +++ b/nexus/src/authn/mod.rs @@ -197,12 +197,18 @@ impl Context { /// (for testing only) #[cfg(test)] pub fn unprivileged_test_user() -> Context { + Context::for_test_user( + USER_TEST_UNPRIVILEGED.identity().id, + USER_TEST_UNPRIVILEGED.silo_id, + ) + } + + /// Returns an authenticated context for the specific Silo user. + #[cfg(test)] + pub fn for_test_user(silo_user_id: Uuid, silo_id: Uuid) -> Context { Context { kind: Kind::Authenticated(Details { - actor: Actor::SiloUser { - silo_user_id: USER_TEST_UNPRIVILEGED.identity().id, - silo_id: USER_TEST_UNPRIVILEGED.silo_id, - }, + actor: Actor::SiloUser { silo_user_id, silo_id }, }), schemes_tried: Vec::new(), } diff --git a/nexus/src/authz/context.rs b/nexus/src/authz/context.rs index dffc32c47c..57d5b1eef6 100644 --- a/nexus/src/authz/context.rs +++ b/nexus/src/authz/context.rs @@ -161,6 +161,7 @@ pub trait AuthorizedResource: oso::ToPolar + Send + Sync + 'static { #[cfg(test)] mod test { // These are essentially unit tests for the policy itself. + // XXX-dap // TODO-coverage This is just a start. But we need better support for role // assignments for non-built-in users to do more here. // TODO If this gets any more complicated, we could consider automatically diff --git a/nexus/src/authz/mod.rs b/nexus/src/authz/mod.rs index 3697b324df..8fb7211ae6 100644 --- a/nexus/src/authz/mod.rs +++ b/nexus/src/authz/mod.rs @@ -182,3 +182,6 @@ pub use oso_generic::Action; pub use oso_generic::DATABASE; mod roles; + +#[cfg(test)] +mod policy_test; diff --git a/nexus/src/authz/oso_generic.rs b/nexus/src/authz/oso_generic.rs index 0147dfc0f1..26688af89e 100644 --- a/nexus/src/authz/oso_generic.rs +++ b/nexus/src/authz/oso_generic.rs @@ -95,15 +95,16 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result { /// There's currently just one enum of Actions for all of Omicron. We expect /// most objects to support mostly the same set of actions. #[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(test, derive(strum::EnumIter))] pub enum Action { Query, // only used for [`Database`] Read, + ListChildren, ReadPolicy, Modify, ModifyPolicy, - Delete, - ListChildren, CreateChild, + Delete, } impl oso::PolarClass for Action { diff --git a/nexus/src/authz/policy_test.rs b/nexus/src/authz/policy_test.rs new file mode 100644 index 0000000000..16b79cf551 --- /dev/null +++ b/nexus/src/authz/policy_test.rs @@ -0,0 +1,491 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Unit tests for the Oso policy +//! +//! This differs from the end-to-end integration tests for authz. This test is +//! intended to verify +//! - for resources covered by RBAC: that the roles in the policy grant the +//! permissions that we expect that they do +//! - for other policies: that the policy reflects the privileges that we expect +//! (e.g., ordinary users don't have internal roles) +//! +//! XXX-dap current status +//! XXX-dap TODO: +//! - check the actual output! +//! - review above comment +//! - review remaining XXX-dap +//! - clean up, document test +//! - figure out what other types to add +//! - is there a way to verify coverage of all authz types? + +use super::ApiResource; +use super::ApiResourceWithRoles; +use super::ApiResourceWithRolesType; +use crate::authn; +use crate::authz; +use crate::authz::AuthorizedResource; +use crate::context::OpContext; +use crate::db; +use crate::db::model::DatabaseString; +use crate::external_api::shared; +use futures::future::BoxFuture; +use futures::FutureExt; +use nexus_test_utils::db::test_setup_database; +use omicron_common::api::external::Error; +use omicron_common::api::external::LookupType; +use omicron_test_utils::dev; +use std::fmt::Write; +use std::sync::Arc; +use strum::IntoEnumIterator; +use uuid::Uuid; + +#[tokio::test] +async fn test_iam_roles() { + let logctx = dev::test_setup_log("test_iam_roles"); + let mut db = test_setup_database(&logctx.log).await; + let (opctx, datastore) = db::datastore::datastore_test(&logctx, &db).await; + + let test_resources = make_resources(); + let silo1_id = test_resources.silo1.resource_id(); + let mut users: Vec<(String, Uuid)> = Vec::new(); + + create_users( + &opctx, + &*datastore, + "fleet", + silo1_id, + &authz::FLEET, + &mut users, + ) + .await; + + create_users( + &opctx, + &*datastore, + "silo1", + silo1_id, + &test_resources.silo1, + &mut users, + ) + .await; + + create_users( + &opctx, + &*datastore, + "silo1-org1", + silo1_id, + &test_resources.silo1_org1, + &mut users, + ) + .await; + + create_users( + &opctx, + &*datastore, + "silo1-org1-proj1", + silo1_id, + &test_resources.silo1_org1_proj1, + &mut users, + ) + .await; + + // Create an OpContext for each user for testing. + let authz = Arc::new(authz::Authz::new(&logctx.log)); + let user_contexts: Vec<(String, Uuid, OpContext)> = users + .iter() + .map(|(username, user_id)| { + let user_id = *user_id; + let user_log = logctx.log.new(o!( + "user_id" => user_id.to_string(), + "username" => username.clone(), + )); + let opctx = OpContext::for_background( + user_log, + Arc::clone(&authz), + authn::Context::for_test_user(user_id, silo1_id), + Arc::clone(&datastore), + ); + + (username.clone(), user_id, opctx) + }) + .collect(); + + let mut buffer = String::new(); + { + let mut tee = StdoutTee::new(&mut buffer); + + run_test_operations( + &mut tee, + &logctx.log, + &user_contexts, + &test_resources, + ) + .await + .unwrap(); + } + + expectorate::assert_contents("tests/output/authz-roles.out", &buffer); + + // XXX-dap + panic!("boom"); + + db.cleanup().await.unwrap(); + logctx.cleanup_successful(); +} + +async fn run_test_operations( + mut out: W, + log: &slog::Logger, + user_contexts: &[(String, Uuid, OpContext)], + test_resources: &Resources, +) -> std::fmt::Result { + for resource in test_resources.all_resources() { + write!(out, "resource: {}\n\n", resource_name(resource),)?; + + write!(out, " {:31}", "USER")?; + for action in authz::Action::iter() { + write!(out, " {:>2}", action_abbreviation(action))?; + } + write!(out, "\n")?; + + for (username, _, opctx) in user_contexts.iter() { + write!(out, " {:31}", &username)?; + for action in authz::Action::iter() { + let result = resource.do_authorize(opctx, action).await; + trace!( + log, + "do_authorize result"; + "username" => username.clone(), + "resource" => ?resource, + "action" => ?action, + "result" => ?result, + ); + let summary = match result { + Ok(_) => '\u{2713}', + Err(Error::Forbidden) + | Err(Error::ObjectNotFound { .. }) => '\u{2717}', + Err(_) => '\u{26a0}', + }; + write!(out, " {:>2}", summary)?; + } + write!(out, "\n")?; + } + + write!(out, "\n")?; + } + + write!(out, "ACTIONS:\n\n")?; + for action in authz::Action::iter() { + write!(out, " {:>2} = {:?}\n", action_abbreviation(action), action)?; + } + write!(out, "\n")?; + + Ok(()) +} + +/// Describes the hierarchy of resources used in our RBAC test +// The hierarchy looks like this: +// fleet +// fleet/s1 +// fleet/s1/o1 +// fleet/s1/o1/p1 +// fleet/s1/o1/p1/vpc1 +// fleet/s1/o1/p2 +// fleet/s1/o1/p2/vpc1 +// fleet/s1/o2 +// fleet/s1/o2/p1 +// fleet/s1/o2/p1/vpc1 +// fleet/s2 +// fleet/s2/o1 +// fleet/s2/o1/p1 +// fleet/s2/o1/p1/vpc1 +struct Resources { + silo1: authz::Silo, + silo1_org1: authz::Organization, + silo1_org1_proj1: authz::Project, + silo1_org1_proj1_children: Vec>, + silo1_org1_proj2: authz::Project, + silo1_org1_proj2_children: Vec>, + silo1_org2: authz::Organization, + silo1_org2_proj1: authz::Project, + silo1_org2_proj1_children: Vec>, + silo2: authz::Silo, + silo2_org1: authz::Organization, + silo2_org1_proj1: authz::Project, + silo2_org1_proj1_children: Vec>, +} + +impl Resources { + fn all_resources( + &self, + ) -> impl std::iter::Iterator { + vec![ + &authz::FLEET as &dyn Authorizable, + &self.silo1 as &dyn Authorizable, + &self.silo1_org1 as &dyn Authorizable, + &self.silo1_org1_proj1 as &dyn Authorizable, + ] + .into_iter() + .chain( + self.silo1_org1_proj1_children + .iter() + .map(|d: &Box| d.as_ref()), + ) + .chain(std::iter::once(&self.silo1_org1_proj2 as &dyn Authorizable)) + .chain( + self.silo1_org1_proj2_children + .iter() + .map(|d: &Box| d.as_ref()), + ) + .chain(vec![ + &self.silo1_org2 as &dyn Authorizable, + &self.silo1_org2_proj1 as &dyn Authorizable, + ]) + .chain( + self.silo1_org2_proj1_children + .iter() + .map(|d: &Box| d.as_ref()), + ) + .chain(vec![ + &self.silo2 as &dyn Authorizable, + &self.silo2_org1 as &dyn Authorizable, + &self.silo2_org1_proj1 as &dyn Authorizable, + ]) + .chain( + self.silo2_org1_proj1_children + .iter() + .map(|d: &Box| d.as_ref()), + ) + } +} + +trait Authorizable: AuthorizedResource + ApiResource { + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a; +} + +impl Authorizable for T +where + T: ApiResource + AuthorizedResource + Clone, +{ + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a, + { + opctx.authorize(action, self).boxed() + } +} + +// XXX-dap make this deterministic +// Most of the uuids here are hardcoded rather than randomly generated for +// debuggability. +fn make_uuid() -> Uuid { + Uuid::new_v4() +} + +fn make_resources() -> Resources { + let silo1_id = make_uuid(); + let silo1 = authz::Silo::new( + authz::FLEET, + silo1_id, + LookupType::ByName(String::from("silo1")), + ); + + let silo1_org1 = authz::Organization::new( + silo1.clone(), + make_uuid(), + LookupType::ByName(String::from("silo1-org1")), + ); + let (silo1_org1_proj1, silo1_org1_proj1_children) = + make_project(&silo1_org1, "silo1-org1-proj1"); + let (silo1_org1_proj2, silo1_org1_proj2_children) = + make_project(&silo1_org1, "silo1-org1-proj2"); + + let silo1_org2 = authz::Organization::new( + silo1.clone(), + make_uuid(), + LookupType::ByName(String::from("silo1-org2")), + ); + let (silo1_org2_proj1, silo1_org2_proj1_children) = + make_project(&silo1_org2, "silo1-org2-proj1"); + + let silo2_id = make_uuid(); + let silo2 = authz::Silo::new( + authz::FLEET, + silo2_id, + LookupType::ByName(String::from("silo2")), + ); + + let silo2_org1 = authz::Organization::new( + silo2.clone(), + make_uuid(), + LookupType::ByName(String::from("silo2-org1")), + ); + let (silo2_org1_proj1, silo2_org1_proj1_children) = + make_project(&silo2_org1, "silo2-org1-proj1"); + + Resources { + silo1, + silo1_org1, + silo1_org1_proj1, + silo1_org1_proj1_children, + silo1_org1_proj2, + silo1_org1_proj2_children, + silo1_org2, + silo1_org2_proj1, + silo1_org2_proj1_children, + silo2, + silo2_org1, + silo2_org1_proj1, + silo2_org1_proj1_children, + } +} + +fn make_project( + organization: &authz::Organization, + project_name: &str, +) -> (authz::Project, Vec>) { + let project = authz::Project::new( + organization.clone(), + make_uuid(), + LookupType::ByName(project_name.to_string()), + ); + + let vpc1_name = format!("{}-vpc1", project_name); + let vpc1 = authz::Vpc::new( + project.clone(), + make_uuid(), + LookupType::ByName(vpc1_name.clone()), + ); + let children: Vec> = vec![ + // XXX-dap TODO-coverage add more different kinds of children + Box::new(authz::Disk::new( + project.clone(), + make_uuid(), + LookupType::ByName(format!("{}-instance1", project_name)), + )), + Box::new(authz::Instance::new( + project.clone(), + make_uuid(), + LookupType::ByName(format!("{}-instance1", project_name)), + )), + Box::new(vpc1.clone()), + // Test a resource nested two levels below Project + Box::new(authz::VpcSubnet::new( + vpc1, + make_uuid(), + LookupType::ByName(format!("{}-subnet1", vpc1_name)), + )), + ]; + + (project, children) +} + +async fn create_users( + opctx: &OpContext, + datastore: &db::DataStore, + resource_name: &str, + silo_id: Uuid, + authz_resource: &T, + users: &mut Vec<(String, Uuid)>, +) where + T: ApiResourceWithRolesType + Clone, + T::AllowedRoles: IntoEnumIterator, +{ + for role in T::AllowedRoles::iter() { + let role_name = role.to_database_string(); + let username = format!("{}-{}", resource_name, role_name); + let user_id = make_uuid(); + println!("creating user: {}", &username); + users.push((username, user_id)); + + let silo_user = db::model::SiloUser::new(silo_id, user_id); + datastore + .silo_user_create(silo_user) + .await + .expect("failed to create silo user"); + + let old_role_assignments = datastore + .role_assignment_fetch_visible(opctx, authz_resource) + .await + .expect("fetching policy"); + let new_role_assignments = old_role_assignments + .into_iter() + .map(|r| r.try_into().unwrap()) + .chain(std::iter::once(shared::RoleAssignment { + identity_type: shared::IdentityType::SiloUser, + identity_id: user_id, + role_name: role, + })) + .collect::>(); + datastore + .role_assignment_replace_visible( + opctx, + authz_resource, + &new_role_assignments, + ) + .await + .expect("failed to assign role"); + } +} + +fn action_abbreviation(action: authz::Action) -> &'static str { + match action { + authz::Action::Query => "Q", + authz::Action::Read => "R", + authz::Action::ListChildren => "LC", + authz::Action::ReadPolicy => "RP", + authz::Action::Modify => "M", + authz::Action::ModifyPolicy => "MP", + authz::Action::CreateChild => "CC", + authz::Action::Delete => "D", + } +} + +fn resource_name(authz_resource: &dyn Authorizable) -> String { + let my_ident = match authz_resource.lookup_type() { + LookupType::ByName(name) => format!("{:?}", name), + LookupType::ById(id) => format!("id {:?}", id.to_string()), + LookupType::BySessionToken(_) | LookupType::ByCompositeId(_) => { + unimplemented!() + } + }; + + format!("{:?} {}", authz_resource.resource_type(), my_ident) +} + +/// `Write` impl that writes everything it's given to both a destination `Write` +/// and stdout via `print!`. +/// +/// It'd be nice if this were instead a generic `Tee` that took an arbitrary +/// number of `Write`s and wrote data to all of them. That's possible, but it +/// wouldn't do what we want. We need to use `print!` in order for output to be +/// captured by the test runner. See rust-lang/rust#12309. +struct StdoutTee { + sink: W, +} + +impl StdoutTee { + fn new(sink: W) -> StdoutTee { + StdoutTee { sink } + } +} + +impl Write for StdoutTee { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.sink.write_str(s)?; + print!("{}", s); + Ok(()) + } +} From dad744a0f45e8b4ec0fe153f0ad5dd1d762964e1 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 25 May 2022 16:53:35 -0700 Subject: [PATCH 02/32] roll in proposed fix --- nexus/src/authz/omicron.polar | 6 +- nexus/tests/output/authz-roles.out | 427 +++++++++++++++++++++++++++++ 2 files changed, 430 insertions(+), 3 deletions(-) create mode 100644 nexus/tests/output/authz-roles.out diff --git a/nexus/src/authz/omicron.polar b/nexus/src/authz/omicron.polar index 98afd891bd..d96d680ba8 100644 --- a/nexus/src/authz/omicron.polar +++ b/nexus/src/authz/omicron.polar @@ -326,6 +326,6 @@ resource Database { # All authenticated users have the "query" permission on the database. has_permission(_actor: AuthenticatedActor, "query", _resource: Database); -# The "db-init" user is the only one with the "init" role. -has_permission(actor: AuthenticatedActor, "modify", _resource: Database) - if actor = USER_DB_INIT; +# The "db-init" user is the only one with the "modify" permission. +# XXX-dap TODO-coverage add a test for this +has_permission(USER_DB_INIT: AuthenticatedActor, "modify", _resource: Database); diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out new file mode 100644 index 0000000000..6de9f7ed2e --- /dev/null +++ b/nexus/tests/output/authz-roles.out @@ -0,0 +1,427 @@ +resource: Fleet id "001de000-1334-4000-8000-000000000000" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Silo "silo1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ + +resource: Organization "silo1-org1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Project "silo1-org1-proj1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ + silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + +resource: Disk "silo1-org1-proj1-instance1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + +resource: Instance "silo1-org1-proj1-instance1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + +resource: Vpc "silo1-org1-proj1-vpc1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + +resource: VpcSubnet "silo1-org1-proj1-vpc1-subnet1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + +resource: Project "silo1-org1-proj2" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Disk "silo1-org1-proj2-instance1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Instance "silo1-org1-proj2-instance1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Vpc "silo1-org1-proj2-vpc1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: VpcSubnet "silo1-org1-proj2-vpc1-subnet1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Organization "silo1-org2" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Project "silo1-org2-proj1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Disk "silo1-org2-proj1-instance1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Instance "silo1-org2-proj1-instance1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Vpc "silo1-org2-proj1-vpc1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: VpcSubnet "silo1-org2-proj1-vpc1-subnet1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Silo "silo2" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Organization "silo2-org1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Project "silo2-org1-proj1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Disk "silo2-org1-proj1-instance1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Instance "silo2-org1-proj1-instance1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: Vpc "silo2-org1-proj1-vpc1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +resource: VpcSubnet "silo2-org1-proj1-vpc1-subnet1" + + USER Q R LC RP M MP CC D + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + +ACTIONS: + + Q = Query + R = Read + LC = ListChildren + RP = ReadPolicy + M = Modify + MP = ModifyPolicy + CC = CreateChild + D = Delete + From f46c708fdc96026cb857f3573047180ea1db0e1a Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 26 May 2022 13:00:04 -0700 Subject: [PATCH 03/32] nits --- nexus/src/authz/policy_test.rs | 13 +++++-------- nexus/tests/output/authz-roles.out | 8 ++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/nexus/src/authz/policy_test.rs b/nexus/src/authz/policy_test.rs index 16b79cf551..d589d5c9b0 100644 --- a/nexus/src/authz/policy_test.rs +++ b/nexus/src/authz/policy_test.rs @@ -4,16 +4,16 @@ //! Unit tests for the Oso policy //! -//! This differs from the end-to-end integration tests for authz. This test is -//! intended to verify +//! This differs from the end-to-end integration tests for authz. The tests +//! here verify: +//! //! - for resources covered by RBAC: that the roles in the policy grant the //! permissions that we expect that they do +//! //! - for other policies: that the policy reflects the privileges that we expect //! (e.g., ordinary users don't have internal roles) //! -//! XXX-dap current status //! XXX-dap TODO: -//! - check the actual output! //! - review above comment //! - review remaining XXX-dap //! - clean up, document test @@ -128,9 +128,6 @@ async fn test_iam_roles() { expectorate::assert_contents("tests/output/authz-roles.out", &buffer); - // XXX-dap - panic!("boom"); - db.cleanup().await.unwrap(); logctx.cleanup_successful(); } @@ -373,7 +370,7 @@ fn make_project( Box::new(authz::Disk::new( project.clone(), make_uuid(), - LookupType::ByName(format!("{}-instance1", project_name)), + LookupType::ByName(format!("{}-disk1", project_name)), )), Box::new(authz::Instance::new( project.clone(), diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out index 6de9f7ed2e..db980fdf8e 100644 --- a/nexus/tests/output/authz-roles.out +++ b/nexus/tests/output/authz-roles.out @@ -62,7 +62,7 @@ resource: Project "silo1-org1-proj1" silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ -resource: Disk "silo1-org1-proj1-instance1" +resource: Disk "silo1-org1-proj1-disk1" USER Q R LC RP M MP CC D fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ @@ -142,7 +142,7 @@ resource: Project "silo1-org1-proj2" silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ -resource: Disk "silo1-org1-proj2-instance1" +resource: Disk "silo1-org1-proj2-disk1" USER Q R LC RP M MP CC D fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ @@ -238,7 +238,7 @@ resource: Project "silo1-org2-proj1" silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ -resource: Disk "silo1-org2-proj1-instance1" +resource: Disk "silo1-org2-proj1-disk1" USER Q R LC RP M MP CC D fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ @@ -350,7 +350,7 @@ resource: Project "silo2-org1-proj1" silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ -resource: Disk "silo2-org1-proj1-instance1" +resource: Disk "silo2-org1-proj1-disk1" USER Q R LC RP M MP CC D fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ From a1d12c0b79079fdb0f4b3e4e40cc41e50a4a42f6 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 15 Jun 2022 13:34:16 -0700 Subject: [PATCH 04/32] fix after merge with main --- nexus/src/authz/policy_test.rs | 1 + nexus/tests/output/authz-roles.out | 677 +++++++++++++++-------------- 2 files changed, 340 insertions(+), 338 deletions(-) diff --git a/nexus/src/authz/policy_test.rs b/nexus/src/authz/policy_test.rs index d589d5c9b0..2a82fafe86 100644 --- a/nexus/src/authz/policy_test.rs +++ b/nexus/src/authz/policy_test.rs @@ -447,6 +447,7 @@ fn action_abbreviation(action: authz::Action) -> &'static str { authz::Action::ModifyPolicy => "MP", authz::Action::CreateChild => "CC", authz::Action::Delete => "D", + authz::Action::ListIdentityProviders => "LP", } } diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out index db980fdf8e..d7e46e4e2d 100644 --- a/nexus/tests/output/authz-roles.out +++ b/nexus/tests/output/authz-roles.out @@ -1,418 +1,418 @@ resource: Fleet id "001de000-1334-4000-8000-000000000000" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Silo "silo1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✓ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ + silo1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ ✓ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✓ + silo1-org1-admin ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ + silo1-org1-collaborator ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ + silo1-org1-viewer ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ + silo1-org1-proj1-admin ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ + silo1-org1-proj1-collaborator ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ + silo1-org1-proj1-viewer ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ resource: Organization "silo1-org1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Project "silo1-org1-proj1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ - silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ resource: Disk "silo1-org1-proj1-disk1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ resource: Instance "silo1-org1-proj1-instance1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ resource: Vpc "silo1-org1-proj1-vpc1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ resource: VpcSubnet "silo1-org1-proj1-vpc1-subnet1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ resource: Project "silo1-org1-proj2" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Disk "silo1-org1-proj2-disk1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Instance "silo1-org1-proj2-instance1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Vpc "silo1-org1-proj2-vpc1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: VpcSubnet "silo1-org1-proj2-vpc1-subnet1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Organization "silo1-org2" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Project "silo1-org2-proj1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Disk "silo1-org2-proj1-disk1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Instance "silo1-org2-proj1-instance1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Vpc "silo1-org2-proj1-vpc1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: VpcSubnet "silo1-org2-proj1-vpc1-subnet1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Silo "silo2" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Organization "silo2-org1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Project "silo2-org1-proj1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Disk "silo2-org1-proj1-disk1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Instance "silo2-org1-proj1-instance1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: Vpc "silo2-org1-proj1-vpc1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ resource: VpcSubnet "silo2-org1-proj1-vpc1-subnet1" - USER Q R LC RP M MP CC D - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + USER Q R LC RP M MP CC D LP + fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ + fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ACTIONS: @@ -424,4 +424,5 @@ ACTIONS: MP = ModifyPolicy CC = CreateChild D = Delete + LP = ListIdentityProviders From bbdcb790d1ec05aa9398abcb6e70e016d3d09343 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 16 Jun 2022 13:41:36 -0700 Subject: [PATCH 05/32] parallelize test (ugly) --- nexus/src/authz/policy_test.rs | 156 +++++++++++++++++++-------------- 1 file changed, 90 insertions(+), 66 deletions(-) diff --git a/nexus/src/authz/policy_test.rs b/nexus/src/authz/policy_test.rs index 2a82fafe86..93a8796395 100644 --- a/nexus/src/authz/policy_test.rs +++ b/nexus/src/authz/policy_test.rs @@ -32,16 +32,18 @@ use crate::db::model::DatabaseString; use crate::external_api::shared; use futures::future::BoxFuture; use futures::FutureExt; +use futures::StreamExt; use nexus_test_utils::db::test_setup_database; use omicron_common::api::external::Error; use omicron_common::api::external::LookupType; use omicron_test_utils::dev; -use std::fmt::Write; +use std::io::Cursor; +use std::io::Write; use std::sync::Arc; use strum::IntoEnumIterator; use uuid::Uuid; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_iam_roles() { let logctx = dev::test_setup_log("test_iam_roles"); let mut db = test_setup_database(&logctx.log).await; @@ -93,7 +95,7 @@ async fn test_iam_roles() { // Create an OpContext for each user for testing. let authz = Arc::new(authz::Authz::new(&logctx.log)); - let user_contexts: Vec<(String, Uuid, OpContext)> = users + let user_contexts: Vec> = users .iter() .map(|(username, user_id)| { let user_id = *user_id; @@ -108,16 +110,15 @@ async fn test_iam_roles() { Arc::clone(&datastore), ); - (username.clone(), user_id, opctx) + Arc::new((username.clone(), user_id, opctx)) }) .collect(); - let mut buffer = String::new(); + let mut buffer = Vec::new(); { - let mut tee = StdoutTee::new(&mut buffer); - + let mut out = StdoutTee::new(&mut buffer); run_test_operations( - &mut tee, + &mut out, &logctx.log, &user_contexts, &test_resources, @@ -126,7 +127,10 @@ async fn test_iam_roles() { .unwrap(); } - expectorate::assert_contents("tests/output/authz-roles.out", &buffer); + expectorate::assert_contents( + "tests/output/authz-roles.out", + &std::str::from_utf8(buffer.as_ref()).expect("non-UTF8 output"), + ); db.cleanup().await.unwrap(); logctx.cleanup_successful(); @@ -135,11 +139,43 @@ async fn test_iam_roles() { async fn run_test_operations( mut out: W, log: &slog::Logger, - user_contexts: &[(String, Uuid, OpContext)], + user_contexts: &[Arc<(String, Uuid, OpContext)>], test_resources: &Resources, -) -> std::fmt::Result { +) -> std::io::Result<()> { + let mut futures = futures::stream::FuturesOrdered::new(); + for resource in test_resources.all_resources() { - write!(out, "resource: {}\n\n", resource_name(resource),)?; + let log = log.new(o!("resource" => format!("{:?}", resource))); + futures.push(test_one_resource( + log, + user_contexts.to_owned(), + Arc::clone(&resource), + )); + } + + let outputs: Vec = futures.collect().await; + for o in outputs { + write!(out, "{}", o)?; + } + + write!(out, "ACTIONS:\n\n")?; + for action in authz::Action::iter() { + write!(out, " {:>2} = {:?}\n", action_abbreviation(action), action)?; + } + write!(out, "\n")?; + + Ok(()) +} + +async fn test_one_resource( + log: slog::Logger, + user_contexts: Vec>, + resource: Arc, +) -> String { + let task = tokio::spawn(async move { + let mut buffer = Vec::new(); + let mut out = Cursor::new(&mut buffer); + write!(out, "resource: {}\n\n", resource_name(resource.as_ref()),)?; write!(out, " {:31}", "USER")?; for action in authz::Action::iter() { @@ -147,7 +183,8 @@ async fn run_test_operations( } write!(out, "\n")?; - for (username, _, opctx) in user_contexts.iter() { + for ctx_tuple in user_contexts.iter() { + let (ref username, _, ref opctx) = **ctx_tuple; write!(out, " {:31}", &username)?; for action in authz::Action::iter() { let result = resource.do_authorize(opctx, action).await; @@ -171,15 +208,13 @@ async fn run_test_operations( } write!(out, "\n")?; - } - - write!(out, "ACTIONS:\n\n")?; - for action in authz::Action::iter() { - write!(out, " {:>2} = {:?}\n", action_abbreviation(action), action)?; - } - write!(out, "\n")?; + Ok(buffer) + }); - Ok(()) + let result: std::io::Result> = + task.await.expect("failed to wait for task"); + let result_str = result.expect("failed to write to string buffer"); + String::from_utf8(result_str).expect("unexpected non-UTF8 output") } /// Describes the hierarchy of resources used in our RBAC test @@ -202,59 +237,45 @@ struct Resources { silo1: authz::Silo, silo1_org1: authz::Organization, silo1_org1_proj1: authz::Project, - silo1_org1_proj1_children: Vec>, + silo1_org1_proj1_children: Vec>, silo1_org1_proj2: authz::Project, - silo1_org1_proj2_children: Vec>, + silo1_org1_proj2_children: Vec>, silo1_org2: authz::Organization, silo1_org2_proj1: authz::Project, - silo1_org2_proj1_children: Vec>, + silo1_org2_proj1_children: Vec>, silo2: authz::Silo, silo2_org1: authz::Organization, silo2_org1_proj1: authz::Project, - silo2_org1_proj1_children: Vec>, + silo2_org1_proj1_children: Vec>, } impl Resources { fn all_resources( &self, - ) -> impl std::iter::Iterator { + ) -> impl std::iter::Iterator> + '_ { vec![ - &authz::FLEET as &dyn Authorizable, - &self.silo1 as &dyn Authorizable, - &self.silo1_org1 as &dyn Authorizable, - &self.silo1_org1_proj1 as &dyn Authorizable, + Arc::new(authz::FLEET.clone()) as Arc, + Arc::new(self.silo1.clone()) as Arc, + Arc::new(self.silo1_org1.clone()) as Arc, + Arc::new(self.silo1_org1_proj1.clone()) as Arc, ] .into_iter() - .chain( - self.silo1_org1_proj1_children - .iter() - .map(|d: &Box| d.as_ref()), - ) - .chain(std::iter::once(&self.silo1_org1_proj2 as &dyn Authorizable)) - .chain( - self.silo1_org1_proj2_children - .iter() - .map(|d: &Box| d.as_ref()), - ) + .chain(self.silo1_org1_proj1_children.iter().cloned()) + .chain(std::iter::once( + Arc::new(self.silo1_org1_proj2.clone()) as Arc + )) + .chain(self.silo1_org1_proj2_children.iter().cloned()) .chain(vec![ - &self.silo1_org2 as &dyn Authorizable, - &self.silo1_org2_proj1 as &dyn Authorizable, + Arc::new(self.silo1_org2.clone()) as Arc, + Arc::new(self.silo1_org2_proj1.clone()) as Arc, ]) - .chain( - self.silo1_org2_proj1_children - .iter() - .map(|d: &Box| d.as_ref()), - ) + .chain(self.silo1_org2_proj1_children.iter().cloned()) .chain(vec![ - &self.silo2 as &dyn Authorizable, - &self.silo2_org1 as &dyn Authorizable, - &self.silo2_org1_proj1 as &dyn Authorizable, + Arc::new(self.silo2.clone()) as Arc, + Arc::new(self.silo2_org1.clone()) as Arc, + Arc::new(self.silo2_org1_proj1.clone()) as Arc, ]) - .chain( - self.silo2_org1_proj1_children - .iter() - .map(|d: &Box| d.as_ref()), - ) + .chain(self.silo2_org1_proj1_children.iter().cloned()) } } @@ -352,7 +373,7 @@ fn make_resources() -> Resources { fn make_project( organization: &authz::Organization, project_name: &str, -) -> (authz::Project, Vec>) { +) -> (authz::Project, Vec>) { let project = authz::Project::new( organization.clone(), make_uuid(), @@ -365,21 +386,21 @@ fn make_project( make_uuid(), LookupType::ByName(vpc1_name.clone()), ); - let children: Vec> = vec![ + let children: Vec> = vec![ // XXX-dap TODO-coverage add more different kinds of children - Box::new(authz::Disk::new( + Arc::new(authz::Disk::new( project.clone(), make_uuid(), LookupType::ByName(format!("{}-disk1", project_name)), )), - Box::new(authz::Instance::new( + Arc::new(authz::Instance::new( project.clone(), make_uuid(), LookupType::ByName(format!("{}-instance1", project_name)), )), - Box::new(vpc1.clone()), + Arc::new(vpc1.clone()), // Test a resource nested two levels below Project - Box::new(authz::VpcSubnet::new( + Arc::new(authz::VpcSubnet::new( vpc1, make_uuid(), LookupType::ByName(format!("{}-subnet1", vpc1_name)), @@ -481,9 +502,12 @@ impl StdoutTee { } impl Write for StdoutTee { - fn write_str(&mut self, s: &str) -> std::fmt::Result { - self.sink.write_str(s)?; - print!("{}", s); - Ok(()) + fn write(&mut self, buf: &[u8]) -> std::io::Result { + print!("{}", std::str::from_utf8(buf).expect("non-UTF8 in stdout tee")); + self.sink.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.sink.flush() } } From 57136c57b59756d1f6e52290bbbec4cf082ccf54 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 16 Jun 2022 14:08:26 -0700 Subject: [PATCH 06/32] add database tests --- nexus/src/authz/omicron.polar | 1 - nexus/src/authz/policy_test.rs | 49 +++++++++++++++++++++--------- nexus/tests/output/authz-roles.out | 16 ++++++++++ 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/nexus/src/authz/omicron.polar b/nexus/src/authz/omicron.polar index 902979670f..9461695116 100644 --- a/nexus/src/authz/omicron.polar +++ b/nexus/src/authz/omicron.polar @@ -386,5 +386,4 @@ resource Database { has_permission(_actor: AuthenticatedActor, "query", _resource: Database); # The "db-init" user is the only one with the "modify" permission. -# XXX-dap TODO-coverage add a test for this has_permission(USER_DB_INIT: AuthenticatedActor, "modify", _resource: Database); diff --git a/nexus/src/authz/policy_test.rs b/nexus/src/authz/policy_test.rs index 93a8796395..5944f2a8c8 100644 --- a/nexus/src/authz/policy_test.rs +++ b/nexus/src/authz/policy_test.rs @@ -144,6 +144,7 @@ async fn run_test_operations( ) -> std::io::Result<()> { let mut futures = futures::stream::FuturesOrdered::new(); + // Run the per-resource tests in parallel. for resource in test_resources.all_resources() { let log = log.new(o!("resource" => format!("{:?}", resource))); futures.push(test_one_resource( @@ -175,7 +176,7 @@ async fn test_one_resource( let task = tokio::spawn(async move { let mut buffer = Vec::new(); let mut out = Cursor::new(&mut buffer); - write!(out, "resource: {}\n\n", resource_name(resource.as_ref()),)?; + write!(out, "resource: {}\n\n", resource.resource_name())?; write!(out, " {:31}", "USER")?; for action in authz::Action::iter() { @@ -254,6 +255,7 @@ impl Resources { &self, ) -> impl std::iter::Iterator> + '_ { vec![ + Arc::new(authz::DATABASE.clone()) as Arc, Arc::new(authz::FLEET.clone()) as Arc, Arc::new(self.silo1.clone()) as Arc, Arc::new(self.silo1_org1.clone()) as Arc, @@ -279,7 +281,9 @@ impl Resources { } } -trait Authorizable: AuthorizedResource + ApiResource { +trait Authorizable: AuthorizedResource + std::fmt::Debug { + fn resource_name(&self) -> String; + fn do_authorize<'a, 'b>( &'a self, opctx: &'b OpContext, @@ -303,6 +307,35 @@ where { opctx.authorize(action, self).boxed() } + + fn resource_name(&self) -> String { + let my_ident = match self.lookup_type() { + LookupType::ByName(name) => format!("{:?}", name), + LookupType::ById(id) => format!("id {:?}", id.to_string()), + LookupType::BySessionToken(_) | LookupType::ByCompositeId(_) => { + unimplemented!() + } + }; + + format!("{:?} {}", self.resource_type(), my_ident) + } +} + +impl Authorizable for authz::oso_generic::Database { + fn resource_name(&self) -> String { + String::from("DATABASE") + } + + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a, + { + opctx.authorize(action, self).boxed() + } } // XXX-dap make this deterministic @@ -472,18 +505,6 @@ fn action_abbreviation(action: authz::Action) -> &'static str { } } -fn resource_name(authz_resource: &dyn Authorizable) -> String { - let my_ident = match authz_resource.lookup_type() { - LookupType::ByName(name) => format!("{:?}", name), - LookupType::ById(id) => format!("id {:?}", id.to_string()), - LookupType::BySessionToken(_) | LookupType::ByCompositeId(_) => { - unimplemented!() - } - }; - - format!("{:?} {}", authz_resource.resource_type(), my_ident) -} - /// `Write` impl that writes everything it's given to both a destination `Write` /// and stdout via `print!`. /// diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out index d7e46e4e2d..eaf2893cfa 100644 --- a/nexus/tests/output/authz-roles.out +++ b/nexus/tests/output/authz-roles.out @@ -1,3 +1,19 @@ +resource: DATABASE + + USER Q R LC RP M MP CC D LP + fleet-admin ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-collaborator ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-viewer ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-admin ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-collaborator ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-viewer ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-admin ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-collaborator ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-viewer ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-admin ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-collaborator ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + silo1-org1-proj1-viewer ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + resource: Fleet id "001de000-1334-4000-8000-000000000000" USER Q R LC RP M MP CC D LP From ad3e2337acf7a2ff6cdc3f446aabebe8d86793df Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 16 Jun 2022 14:31:40 -0700 Subject: [PATCH 07/32] test unauthenticated user --- nexus/src/authz/policy_test.rs | 31 +++++++++++++++++++++++++----- nexus/tests/output/authz-roles.out | 27 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/nexus/src/authz/policy_test.rs b/nexus/src/authz/policy_test.rs index 5944f2a8c8..860ef62b83 100644 --- a/nexus/src/authz/policy_test.rs +++ b/nexus/src/authz/policy_test.rs @@ -95,7 +95,7 @@ async fn test_iam_roles() { // Create an OpContext for each user for testing. let authz = Arc::new(authz::Authz::new(&logctx.log)); - let user_contexts: Vec> = users + let mut user_contexts: Vec> = users .iter() .map(|(username, user_id)| { let user_id = *user_id; @@ -110,10 +110,30 @@ async fn test_iam_roles() { Arc::clone(&datastore), ); - Arc::new((username.clone(), user_id, opctx)) + Arc::new((username.clone(), opctx)) }) .collect(); + // Create and test an unauthenticated OpContext as well. + // + // We could test the "test-privileged" and "test-unprivileged" users, but it + // wouldn't be very interesting: they're in a different Silo than the + // resources that we're checking against so even "test-privileged" won't + // have privileges here. Anyway, they're composed of ordinary role + // assignments so they're just a special case of what we're already testing. + let user_log = logctx.log.new(o!( + "username" => "unauthenticated", + )); + user_contexts.push(Arc::new(( + String::from("unauthenticated"), + OpContext::for_background( + user_log, + Arc::clone(&authz), + authn::Context::internal_unauthenticated(), + Arc::clone(&datastore), + ), + ))); + let mut buffer = Vec::new(); { let mut out = StdoutTee::new(&mut buffer); @@ -139,7 +159,7 @@ async fn test_iam_roles() { async fn run_test_operations( mut out: W, log: &slog::Logger, - user_contexts: &[Arc<(String, Uuid, OpContext)>], + user_contexts: &[Arc<(String, OpContext)>], test_resources: &Resources, ) -> std::io::Result<()> { let mut futures = futures::stream::FuturesOrdered::new(); @@ -170,7 +190,7 @@ async fn run_test_operations( async fn test_one_resource( log: slog::Logger, - user_contexts: Vec>, + user_contexts: Vec>, resource: Arc, ) -> String { let task = tokio::spawn(async move { @@ -185,7 +205,7 @@ async fn test_one_resource( write!(out, "\n")?; for ctx_tuple in user_contexts.iter() { - let (ref username, _, ref opctx) = **ctx_tuple; + let (ref username, ref opctx) = **ctx_tuple; write!(out, " {:31}", &username)?; for action in authz::Action::iter() { let result = resource.do_authorize(opctx, action).await; @@ -201,6 +221,7 @@ async fn test_one_resource( Ok(_) => '\u{2713}', Err(Error::Forbidden) | Err(Error::ObjectNotFound { .. }) => '\u{2717}', + Err(Error::Unauthenticated { .. }) => '!', Err(_) => '\u{26a0}', }; write!(out, " {:>2}", summary)?; diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out index eaf2893cfa..a4a011c174 100644 --- a/nexus/tests/output/authz-roles.out +++ b/nexus/tests/output/authz-roles.out @@ -13,6 +13,7 @@ resource: DATABASE silo1-org1-proj1-admin ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Fleet id "001de000-1334-4000-8000-000000000000" @@ -29,6 +30,7 @@ resource: Fleet id "001de000-1334-4000-8000-000000000000" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Silo "silo1" @@ -45,6 +47,7 @@ resource: Silo "silo1" silo1-org1-proj1-admin ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ silo1-org1-proj1-collaborator ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ silo1-org1-proj1-viewer ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Organization "silo1-org1" @@ -61,6 +64,7 @@ resource: Organization "silo1-org1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Project "silo1-org1-proj1" @@ -77,6 +81,7 @@ resource: Project "silo1-org1-proj1" silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ ✗ silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Disk "silo1-org1-proj1-disk1" @@ -93,6 +98,7 @@ resource: Disk "silo1-org1-proj1-disk1" silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Instance "silo1-org1-proj1-instance1" @@ -109,6 +115,7 @@ resource: Instance "silo1-org1-proj1-instance1" silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Vpc "silo1-org1-proj1-vpc1" @@ -125,6 +132,7 @@ resource: Vpc "silo1-org1-proj1-vpc1" silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: VpcSubnet "silo1-org1-proj1-vpc1-subnet1" @@ -141,6 +149,7 @@ resource: VpcSubnet "silo1-org1-proj1-vpc1-subnet1" silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Project "silo1-org1-proj2" @@ -157,6 +166,7 @@ resource: Project "silo1-org1-proj2" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Disk "silo1-org1-proj2-disk1" @@ -173,6 +183,7 @@ resource: Disk "silo1-org1-proj2-disk1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Instance "silo1-org1-proj2-instance1" @@ -189,6 +200,7 @@ resource: Instance "silo1-org1-proj2-instance1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Vpc "silo1-org1-proj2-vpc1" @@ -205,6 +217,7 @@ resource: Vpc "silo1-org1-proj2-vpc1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: VpcSubnet "silo1-org1-proj2-vpc1-subnet1" @@ -221,6 +234,7 @@ resource: VpcSubnet "silo1-org1-proj2-vpc1-subnet1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Organization "silo1-org2" @@ -237,6 +251,7 @@ resource: Organization "silo1-org2" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Project "silo1-org2-proj1" @@ -253,6 +268,7 @@ resource: Project "silo1-org2-proj1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Disk "silo1-org2-proj1-disk1" @@ -269,6 +285,7 @@ resource: Disk "silo1-org2-proj1-disk1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Instance "silo1-org2-proj1-instance1" @@ -285,6 +302,7 @@ resource: Instance "silo1-org2-proj1-instance1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Vpc "silo1-org2-proj1-vpc1" @@ -301,6 +319,7 @@ resource: Vpc "silo1-org2-proj1-vpc1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: VpcSubnet "silo1-org2-proj1-vpc1-subnet1" @@ -317,6 +336,7 @@ resource: VpcSubnet "silo1-org2-proj1-vpc1-subnet1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Silo "silo2" @@ -333,6 +353,7 @@ resource: Silo "silo2" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Organization "silo2-org1" @@ -349,6 +370,7 @@ resource: Organization "silo2-org1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Project "silo2-org1-proj1" @@ -365,6 +387,7 @@ resource: Project "silo2-org1-proj1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Disk "silo2-org1-proj1-disk1" @@ -381,6 +404,7 @@ resource: Disk "silo2-org1-proj1-disk1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Instance "silo2-org1-proj1-instance1" @@ -397,6 +421,7 @@ resource: Instance "silo2-org1-proj1-instance1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: Vpc "silo2-org1-proj1-vpc1" @@ -413,6 +438,7 @@ resource: Vpc "silo2-org1-proj1-vpc1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! resource: VpcSubnet "silo2-org1-proj1-vpc1-subnet1" @@ -429,6 +455,7 @@ resource: VpcSubnet "silo2-org1-proj1-vpc1-subnet1" silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + unauthenticated ! ! ! ! ! ! ! ! ! ACTIONS: From 553487c560a9a0a1a368826f38ebfb065389c7c7 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 16 Jun 2022 14:36:53 -0700 Subject: [PATCH 08/32] use clearer symbols --- nexus/src/authz/policy_test.rs | 4 +- nexus/tests/output/authz-roles.out | 648 ++++++++++++++--------------- 2 files changed, 326 insertions(+), 326 deletions(-) diff --git a/nexus/src/authz/policy_test.rs b/nexus/src/authz/policy_test.rs index 860ef62b83..141dac419f 100644 --- a/nexus/src/authz/policy_test.rs +++ b/nexus/src/authz/policy_test.rs @@ -218,9 +218,9 @@ async fn test_one_resource( "result" => ?result, ); let summary = match result { - Ok(_) => '\u{2713}', + Ok(_) => '\u{2714}', Err(Error::Forbidden) - | Err(Error::ObjectNotFound { .. }) => '\u{2717}', + | Err(Error::ObjectNotFound { .. }) => '\u{2718}', Err(Error::Unauthenticated { .. }) => '!', Err(_) => '\u{26a0}', }; diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out index a4a011c174..c045392dfb 100644 --- a/nexus/tests/output/authz-roles.out +++ b/nexus/tests/output/authz-roles.out @@ -1,460 +1,460 @@ resource: DATABASE USER Q R LC RP M MP CC D LP - fleet-admin ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - fleet-collaborator ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - fleet-viewer ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Fleet id "001de000-1334-4000-8000-000000000000" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✘ ✘ ✔ ✘ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Silo "silo1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✓ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ - silo1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ ✓ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✓ - silo1-org1-admin ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ - silo1-org1-collaborator ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ - silo1-org1-viewer ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ - silo1-org1-proj1-admin ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ - silo1-org1-proj1-collaborator ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ - silo1-org1-proj1-viewer ✗ ✓ ✗ ✓ ✗ ✗ ✗ ✗ ✓ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✔ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔ + silo1-collaborator ✘ ✔ ✔ ✔ ✘ ✘ ✔ ✘ ✔ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✔ + silo1-org1-admin ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ ✔ + silo1-org1-collaborator ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ ✔ + silo1-org1-viewer ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ ✔ + silo1-org1-proj1-admin ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ ✔ + silo1-org1-proj1-collaborator ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ ✔ + silo1-org1-proj1-viewer ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ ✔ unauthenticated ! ! ! ! ! ! ! ! ! resource: Organization "silo1-org1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✘ ✘ ✔ ✘ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Project "silo1-org1-proj1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✗ ✗ ✓ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-collaborator ✘ ✔ ✔ ✔ ✘ ✘ ✔ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Disk "silo1-org1-proj1-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Instance "silo1-org1-proj1-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Vpc "silo1-org1-proj1-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: VpcSubnet "silo1-org1-proj1-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-proj1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-proj1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Project "silo1-org1-proj2" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Disk "silo1-org1-proj2-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Instance "silo1-org1-proj2-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Vpc "silo1-org1-proj2-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: VpcSubnet "silo1-org1-proj2-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-org1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Organization "silo1-org2" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Project "silo1-org2-proj1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Disk "silo1-org2-proj1-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Instance "silo1-org2-proj1-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Vpc "silo1-org2-proj1-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: VpcSubnet "silo1-org2-proj1-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - silo1-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Silo "silo2" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Organization "silo2-org1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Project "silo2-org1-proj1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Disk "silo2-org1-proj1-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Instance "silo2-org1-proj1-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: Vpc "silo2-org1-proj1-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! resource: VpcSubnet "silo2-org1-proj1-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-collaborator ✗ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✗ - fleet-viewer ✗ ✓ ✓ ✓ ✗ ✗ ✗ ✗ ✗ - silo1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-admin ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-collaborator ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ - silo1-org1-proj1-viewer ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! ACTIONS: From 2b14b1adec741aeb0947518953d0cf200b307e7e Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 16 Jun 2022 14:38:25 -0700 Subject: [PATCH 09/32] existing context tests are obviated by the new test --- nexus/src/authz/context.rs | 127 ------------------------------------- 1 file changed, 127 deletions(-) diff --git a/nexus/src/authz/context.rs b/nexus/src/authz/context.rs index 57d5b1eef6..f480212a1d 100644 --- a/nexus/src/authz/context.rs +++ b/nexus/src/authz/context.rs @@ -157,130 +157,3 @@ pub trait AuthorizedResource: oso::ToPolar + Send + Sync + 'static { action: Action, ) -> Error; } - -#[cfg(test)] -mod test { - // These are essentially unit tests for the policy itself. - // XXX-dap - // TODO-coverage This is just a start. But we need better support for role - // assignments for non-built-in users to do more here. - // TODO If this gets any more complicated, we could consider automatically - // generating the test cases. We could precreate a bunch of resources and - // some users with different roles. Then we could run through a table that - // says exactly which users should be able to do what to each resource. - use crate::authn; - use crate::authz::Action; - use crate::authz::Authz; - use crate::authz::Context; - use crate::authz::DATABASE; - use crate::authz::FLEET; - use crate::db::DataStore; - use nexus_test_utils::db::test_setup_database; - use omicron_test_utils::dev; - use std::sync::Arc; - - fn authz_context_for_actor( - log: &slog::Logger, - authn: authn::Context, - datastore: Arc, - ) -> Context { - let authz = Authz::new(log); - Context::new(Arc::new(authn), Arc::new(authz), datastore) - } - - fn authz_context_noauth( - log: &slog::Logger, - datastore: Arc, - ) -> Context { - let authn = authn::Context::internal_unauthenticated(); - let authz = Authz::new(log); - Context::new(Arc::new(authn), Arc::new(authz), datastore) - } - - #[tokio::test] - async fn test_database() { - let logctx = dev::test_setup_log("test_database"); - let mut db = test_setup_database(&logctx.log).await; - let (opctx, datastore) = - crate::db::datastore::datastore_test(&logctx, &db).await; - let authz_privileged = authz_context_for_actor( - &logctx.log, - authn::Context::privileged_test_user(), - Arc::clone(&datastore), - ); - authz_privileged - .authorize(&opctx, Action::Query, DATABASE) - .await - .expect("expected privileged user to be able to query database"); - let error = authz_privileged - .authorize(&opctx, Action::Modify, DATABASE) - .await - .expect_err( - "expected privileged test user not to be able to modify \ - database", - ); - assert!(matches!( - error, - omicron_common::api::external::Error::Forbidden - )); - let authz_nobody = authz_context_for_actor( - &logctx.log, - authn::Context::unprivileged_test_user(), - Arc::clone(&datastore), - ); - authz_nobody - .authorize(&opctx, Action::Query, DATABASE) - .await - .expect("expected unprivileged user to be able to query database"); - let authz_noauth = authz_context_noauth(&logctx.log, datastore); - authz_noauth - .authorize(&opctx, Action::Query, DATABASE) - .await - .expect_err( - "expected unauthenticated user not to be able to query database", - ); - db.cleanup().await.unwrap(); - logctx.cleanup_successful(); - } - - #[tokio::test] - async fn test_organization() { - let logctx = dev::test_setup_log("test_organization"); - let mut db = test_setup_database(&logctx.log).await; - let (opctx, datastore) = - crate::db::datastore::datastore_test(&logctx, &db).await; - - let authz_privileged = authz_context_for_actor( - &logctx.log, - authn::Context::privileged_test_user(), - Arc::clone(&datastore), - ); - authz_privileged - .authorize(&opctx, Action::CreateChild, FLEET) - .await - .expect( - "expected privileged user to be able to create organization", - ); - let authz_nobody = authz_context_for_actor( - &logctx.log, - authn::Context::unprivileged_test_user(), - Arc::clone(&datastore), - ); - authz_nobody - .authorize(&opctx, Action::CreateChild, FLEET) - .await - .expect_err( - "expected unprivileged user not to be able to create organization", - ); - let authz_noauth = authz_context_noauth(&logctx.log, datastore); - authz_noauth - .authorize(&opctx, Action::Query, DATABASE) - .await - .expect_err( - "expected unauthenticated user not to be able \ - to create organization", - ); - db.cleanup().await.unwrap(); - logctx.cleanup_successful(); - } -} From 7381322226dd8355b26e5951df6dcffadb24de8d Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Tue, 9 Aug 2022 21:11:35 -0700 Subject: [PATCH 10/32] add coverage test for IAM role policy test --- nexus/src/authz/context.rs | 5 + nexus/src/authz/policy_test.rs | 165 +++++++++++++++++++++++++++++++-- 2 files changed, 162 insertions(+), 8 deletions(-) diff --git a/nexus/src/authz/context.rs b/nexus/src/authz/context.rs index 073d490222..9d530847a9 100644 --- a/nexus/src/authz/context.rs +++ b/nexus/src/authz/context.rs @@ -50,6 +50,11 @@ impl Authz { { self.oso.is_allowed(actor.clone(), action, resource.clone()) } + + #[cfg(test)] + pub fn into_class_names(self) -> BTreeSet { + self.class_names + } } /// Operation-specific authorization context diff --git a/nexus/src/authz/policy_test.rs b/nexus/src/authz/policy_test.rs index b70ed99ddc..ee5410f539 100644 --- a/nexus/src/authz/policy_test.rs +++ b/nexus/src/authz/policy_test.rs @@ -18,7 +18,6 @@ //! - review remaining XXX-dap //! - clean up, document test //! - figure out what other types to add -//! - is there a way to verify coverage of all authz types? use super::ApiResource; use super::ApiResourceWithRoles; @@ -37,6 +36,8 @@ use nexus_test_utils::db::test_setup_database; use omicron_common::api::external::Error; use omicron_common::api::external::LookupType; use omicron_test_utils::dev; +use oso::PolarClass; +use std::collections::BTreeSet; use std::io::Cursor; use std::io::Write; use std::sync::Arc; @@ -44,12 +45,15 @@ use strum::IntoEnumIterator; use uuid::Uuid; #[tokio::test(flavor = "multi_thread")] -async fn test_iam_roles() { +async fn test_iam_roles_behavior() { let logctx = dev::test_setup_log("test_iam_roles"); let mut db = test_setup_database(&logctx.log).await; let (opctx, datastore) = db::datastore::datastore_test(&logctx, &db).await; - let test_resources = make_resources(); + let mut coverage = Coverage::new(&logctx.log); + let test_resources = make_resources(&mut coverage); + coverage.verify(); + let silo1_id = test_resources.silo1.resource_id(); let mut users: Vec<(String, Uuid)> = Vec::new(); @@ -156,6 +160,128 @@ async fn test_iam_roles() { logctx.cleanup_successful(); } +struct Coverage { + log: slog::Logger, + class_names: BTreeSet, + exempted: BTreeSet, + covered: BTreeSet, +} + +impl Coverage { + fn new(log: &slog::Logger) -> Coverage { + let log = log.new(o!("component" => "IamTestCoverage")); + let authz = authz::Authz::new(&log); + let class_names = authz.into_class_names(); + + // Class names should be added to this exemption list when their Polar + // code snippets and authz behavior is identical to another class. This + // is primarily for performance reasons because this test takes a long + // time. But with every exemption comes the risk of a security issue! + // + // PLEASE: instead of adding a class to this list, consider updating + // this test to create an instance of the class and then test it. + let exempted = [ + // Non-resources + super::Action::get_polar_class(), + super::actor::AnyActor::get_polar_class(), + super::actor::AuthenticatedActor::get_polar_class(), + // XXX-dap TODO-coverage Not yet implemented, but not exempted for a + // good reason. + super::IpPoolList::get_polar_class(), + super::GlobalImageList::get_polar_class(), + super::ConsoleSessionList::get_polar_class(), + super::DeviceAuthRequestList::get_polar_class(), + super::IpPool::get_polar_class(), + super::NetworkInterface::get_polar_class(), + super::VpcRouter::get_polar_class(), + super::RouterRoute::get_polar_class(), + super::ConsoleSession::get_polar_class(), + super::DeviceAuthRequest::get_polar_class(), + super::DeviceAccessToken::get_polar_class(), + super::Rack::get_polar_class(), + super::RoleBuiltin::get_polar_class(), + super::SshKey::get_polar_class(), + super::SiloUser::get_polar_class(), + super::SiloGroup::get_polar_class(), + super::IdentityProvider::get_polar_class(), + super::SamlIdentityProvider::get_polar_class(), + super::Sled::get_polar_class(), + super::UpdateAvailableArtifact::get_polar_class(), + super::UserBuiltin::get_polar_class(), + super::GlobalImage::get_polar_class(), + ] + .into_iter() + .map(|c| c.name.clone()) + .collect(); + + Coverage { log, class_names, exempted, covered: BTreeSet::new() } + } + + fn covered(&mut self, _covered: &T) { + self.covered_class(T::get_polar_class()) + } + + fn covered_class(&mut self, class: oso::Class) { + let class_name = class.name.clone(); + debug!(&self.log, "covering"; "class_name" => &class_name); + self.covered.insert(class_name); + } + + fn verify(&self) { + let mut uncovered = Vec::new(); + let mut bad_exemptions = Vec::new(); + + for class_name in &self.class_names { + let class_name = class_name.as_str(); + let exempted = self.exempted.contains(class_name); + let covered = self.covered.contains(class_name); + + match (exempted, covered) { + (true, false) => { + // XXX-dap consider checking whether the Polar snippet + // exactly matches that of another class? + debug!(&self.log, "exempt"; "class_name" => class_name); + } + (false, true) => { + debug!(&self.log, "covered"; "class_name" => class_name); + } + (true, true) => { + error!( + &self.log, + "bad exemption (class was covered)"; + "class_name" => class_name + ); + bad_exemptions.push(class_name); + } + (false, false) => { + error!( + &self.log, + "uncovered class"; + "class_name" => class_name + ); + uncovered.push(class_name); + } + }; + } + + if !bad_exemptions.is_empty() { + panic!( + "these classes were covered by the tests and should \ + not have been part of the exemption list: {}", + bad_exemptions.join(", ") + ); + } + + if !uncovered.is_empty() { + panic!( + "these classes were not covered by the IAM role \ + policy test: {}", + uncovered.join(", ") + ); + } + } +} + async fn run_test_operations( mut out: W, log: &slog::Logger, @@ -312,6 +438,8 @@ trait Authorizable: AuthorizedResource + std::fmt::Debug { ) -> BoxFuture<'a, Result<(), Error>> where 'b: 'a; + + fn polar_class(&self) -> oso::Class; } impl Authorizable for T @@ -340,6 +468,10 @@ where format!("{:?} {}", self.resource_type(), my_ident) } + + fn polar_class(&self) -> oso::Class { + T::get_polar_class() + } } impl Authorizable for authz::oso_generic::Database { @@ -357,6 +489,10 @@ impl Authorizable for authz::oso_generic::Database { { opctx.authorize(action, self).boxed() } + + fn polar_class(&self) -> oso::Class { + authz::oso_generic::Database::get_polar_class() + } } // XXX-dap make this deterministic @@ -366,23 +502,30 @@ fn make_uuid() -> Uuid { Uuid::new_v4() } -fn make_resources() -> Resources { +fn make_resources(coverage: &mut Coverage) -> Resources { + // "Database" and "Fleet" are implicitly included by virtue of being + // returned by the iterator. + coverage.covered(&authz::DATABASE); + coverage.covered(&authz::FLEET); + let silo1_id = make_uuid(); let silo1 = authz::Silo::new( authz::FLEET, silo1_id, LookupType::ByName(String::from("silo1")), ); + coverage.covered(&silo1); let silo1_org1 = authz::Organization::new( silo1.clone(), make_uuid(), LookupType::ByName(String::from("silo1-org1")), ); + coverage.covered(&silo1_org1); let (silo1_org1_proj1, silo1_org1_proj1_children) = - make_project(&silo1_org1, "silo1-org1-proj1"); + make_project(coverage, &silo1_org1, "silo1-org1-proj1"); let (silo1_org1_proj2, silo1_org1_proj2_children) = - make_project(&silo1_org1, "silo1-org1-proj2"); + make_project(coverage, &silo1_org1, "silo1-org1-proj2"); let silo1_org2 = authz::Organization::new( silo1.clone(), @@ -390,7 +533,7 @@ fn make_resources() -> Resources { LookupType::ByName(String::from("silo1-org2")), ); let (silo1_org2_proj1, silo1_org2_proj1_children) = - make_project(&silo1_org2, "silo1-org2-proj1"); + make_project(coverage, &silo1_org2, "silo1-org2-proj1"); let silo2_id = make_uuid(); let silo2 = authz::Silo::new( @@ -405,7 +548,7 @@ fn make_resources() -> Resources { LookupType::ByName(String::from("silo2-org1")), ); let (silo2_org1_proj1, silo2_org1_proj1_children) = - make_project(&silo2_org1, "silo2-org1-proj1"); + make_project(coverage, &silo2_org1, "silo2-org1-proj1"); Resources { silo1, @@ -425,6 +568,7 @@ fn make_resources() -> Resources { } fn make_project( + coverage: &mut Coverage, organization: &authz::Organization, project_name: &str, ) -> (authz::Project, Vec>) { @@ -433,6 +577,7 @@ fn make_project( make_uuid(), LookupType::ByName(project_name.to_string()), ); + coverage.covered(&project); let vpc1_name = format!("{}-vpc1", project_name); let vpc1 = authz::Vpc::new( @@ -440,6 +585,7 @@ fn make_project( make_uuid(), LookupType::ByName(vpc1_name.clone()), ); + coverage.covered(&vpc1); let children: Vec> = vec![ // XXX-dap TODO-coverage add more different kinds of children Arc::new(authz::Disk::new( @@ -460,6 +606,9 @@ fn make_project( LookupType::ByName(format!("{}-subnet1", vpc1_name)), )), ]; + for c in &children { + coverage.covered_class(c.polar_class()); + } (project, children) } From 98a9c57931d44352539014a06058c0a8709f23e9 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 10 Aug 2022 13:36:14 -0700 Subject: [PATCH 11/32] break up the policy test into more manageable modules --- nexus/src/authz/policy_test.rs | 704 ----------------------- nexus/src/authz/policy_test/coverage.rs | 134 +++++ nexus/src/authz/policy_test/mod.rs | 340 +++++++++++ nexus/src/authz/policy_test/resources.rs | 269 +++++++++ 4 files changed, 743 insertions(+), 704 deletions(-) delete mode 100644 nexus/src/authz/policy_test.rs create mode 100644 nexus/src/authz/policy_test/coverage.rs create mode 100644 nexus/src/authz/policy_test/mod.rs create mode 100644 nexus/src/authz/policy_test/resources.rs diff --git a/nexus/src/authz/policy_test.rs b/nexus/src/authz/policy_test.rs deleted file mode 100644 index ee5410f539..0000000000 --- a/nexus/src/authz/policy_test.rs +++ /dev/null @@ -1,704 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Unit tests for the Oso policy -//! -//! This differs from the end-to-end integration tests for authz. The tests -//! here verify: -//! -//! - for resources covered by RBAC: that the roles in the policy grant the -//! permissions that we expect that they do -//! -//! - for other policies: that the policy reflects the privileges that we expect -//! (e.g., ordinary users don't have internal roles) -//! -//! XXX-dap TODO: -//! - review above comment -//! - review remaining XXX-dap -//! - clean up, document test -//! - figure out what other types to add - -use super::ApiResource; -use super::ApiResourceWithRoles; -use super::ApiResourceWithRolesType; -use crate::authn; -use crate::authz; -use crate::authz::AuthorizedResource; -use crate::context::OpContext; -use crate::db; -use crate::db::model::DatabaseString; -use crate::external_api::shared; -use futures::future::BoxFuture; -use futures::FutureExt; -use futures::StreamExt; -use nexus_test_utils::db::test_setup_database; -use omicron_common::api::external::Error; -use omicron_common::api::external::LookupType; -use omicron_test_utils::dev; -use oso::PolarClass; -use std::collections::BTreeSet; -use std::io::Cursor; -use std::io::Write; -use std::sync::Arc; -use strum::IntoEnumIterator; -use uuid::Uuid; - -#[tokio::test(flavor = "multi_thread")] -async fn test_iam_roles_behavior() { - let logctx = dev::test_setup_log("test_iam_roles"); - let mut db = test_setup_database(&logctx.log).await; - let (opctx, datastore) = db::datastore::datastore_test(&logctx, &db).await; - - let mut coverage = Coverage::new(&logctx.log); - let test_resources = make_resources(&mut coverage); - coverage.verify(); - - let silo1_id = test_resources.silo1.resource_id(); - let mut users: Vec<(String, Uuid)> = Vec::new(); - - create_users( - &opctx, - &*datastore, - "fleet", - silo1_id, - &authz::FLEET, - &mut users, - ) - .await; - - create_users( - &opctx, - &*datastore, - "silo1", - silo1_id, - &test_resources.silo1, - &mut users, - ) - .await; - - create_users( - &opctx, - &*datastore, - "silo1-org1", - silo1_id, - &test_resources.silo1_org1, - &mut users, - ) - .await; - - create_users( - &opctx, - &*datastore, - "silo1-org1-proj1", - silo1_id, - &test_resources.silo1_org1_proj1, - &mut users, - ) - .await; - - // Create an OpContext for each user for testing. - let authz = Arc::new(authz::Authz::new(&logctx.log)); - let mut user_contexts: Vec> = users - .iter() - .map(|(username, user_id)| { - let user_id = *user_id; - let user_log = logctx.log.new(o!( - "user_id" => user_id.to_string(), - "username" => username.clone(), - )); - let opctx = OpContext::for_background( - user_log, - Arc::clone(&authz), - authn::Context::for_test_user(user_id, silo1_id), - Arc::clone(&datastore), - ); - - Arc::new((username.clone(), opctx)) - }) - .collect(); - - // Create and test an unauthenticated OpContext as well. - // - // We could test the "test-privileged" and "test-unprivileged" users, but it - // wouldn't be very interesting: they're in a different Silo than the - // resources that we're checking against so even "test-privileged" won't - // have privileges here. Anyway, they're composed of ordinary role - // assignments so they're just a special case of what we're already testing. - let user_log = logctx.log.new(o!( - "username" => "unauthenticated", - )); - user_contexts.push(Arc::new(( - String::from("unauthenticated"), - OpContext::for_background( - user_log, - Arc::clone(&authz), - authn::Context::internal_unauthenticated(), - Arc::clone(&datastore), - ), - ))); - - let mut buffer = Vec::new(); - { - let mut out = StdoutTee::new(&mut buffer); - run_test_operations( - &mut out, - &logctx.log, - &user_contexts, - &test_resources, - ) - .await - .unwrap(); - } - - expectorate::assert_contents( - "tests/output/authz-roles.out", - &std::str::from_utf8(buffer.as_ref()).expect("non-UTF8 output"), - ); - - db.cleanup().await.unwrap(); - logctx.cleanup_successful(); -} - -struct Coverage { - log: slog::Logger, - class_names: BTreeSet, - exempted: BTreeSet, - covered: BTreeSet, -} - -impl Coverage { - fn new(log: &slog::Logger) -> Coverage { - let log = log.new(o!("component" => "IamTestCoverage")); - let authz = authz::Authz::new(&log); - let class_names = authz.into_class_names(); - - // Class names should be added to this exemption list when their Polar - // code snippets and authz behavior is identical to another class. This - // is primarily for performance reasons because this test takes a long - // time. But with every exemption comes the risk of a security issue! - // - // PLEASE: instead of adding a class to this list, consider updating - // this test to create an instance of the class and then test it. - let exempted = [ - // Non-resources - super::Action::get_polar_class(), - super::actor::AnyActor::get_polar_class(), - super::actor::AuthenticatedActor::get_polar_class(), - // XXX-dap TODO-coverage Not yet implemented, but not exempted for a - // good reason. - super::IpPoolList::get_polar_class(), - super::GlobalImageList::get_polar_class(), - super::ConsoleSessionList::get_polar_class(), - super::DeviceAuthRequestList::get_polar_class(), - super::IpPool::get_polar_class(), - super::NetworkInterface::get_polar_class(), - super::VpcRouter::get_polar_class(), - super::RouterRoute::get_polar_class(), - super::ConsoleSession::get_polar_class(), - super::DeviceAuthRequest::get_polar_class(), - super::DeviceAccessToken::get_polar_class(), - super::Rack::get_polar_class(), - super::RoleBuiltin::get_polar_class(), - super::SshKey::get_polar_class(), - super::SiloUser::get_polar_class(), - super::SiloGroup::get_polar_class(), - super::IdentityProvider::get_polar_class(), - super::SamlIdentityProvider::get_polar_class(), - super::Sled::get_polar_class(), - super::UpdateAvailableArtifact::get_polar_class(), - super::UserBuiltin::get_polar_class(), - super::GlobalImage::get_polar_class(), - ] - .into_iter() - .map(|c| c.name.clone()) - .collect(); - - Coverage { log, class_names, exempted, covered: BTreeSet::new() } - } - - fn covered(&mut self, _covered: &T) { - self.covered_class(T::get_polar_class()) - } - - fn covered_class(&mut self, class: oso::Class) { - let class_name = class.name.clone(); - debug!(&self.log, "covering"; "class_name" => &class_name); - self.covered.insert(class_name); - } - - fn verify(&self) { - let mut uncovered = Vec::new(); - let mut bad_exemptions = Vec::new(); - - for class_name in &self.class_names { - let class_name = class_name.as_str(); - let exempted = self.exempted.contains(class_name); - let covered = self.covered.contains(class_name); - - match (exempted, covered) { - (true, false) => { - // XXX-dap consider checking whether the Polar snippet - // exactly matches that of another class? - debug!(&self.log, "exempt"; "class_name" => class_name); - } - (false, true) => { - debug!(&self.log, "covered"; "class_name" => class_name); - } - (true, true) => { - error!( - &self.log, - "bad exemption (class was covered)"; - "class_name" => class_name - ); - bad_exemptions.push(class_name); - } - (false, false) => { - error!( - &self.log, - "uncovered class"; - "class_name" => class_name - ); - uncovered.push(class_name); - } - }; - } - - if !bad_exemptions.is_empty() { - panic!( - "these classes were covered by the tests and should \ - not have been part of the exemption list: {}", - bad_exemptions.join(", ") - ); - } - - if !uncovered.is_empty() { - panic!( - "these classes were not covered by the IAM role \ - policy test: {}", - uncovered.join(", ") - ); - } - } -} - -async fn run_test_operations( - mut out: W, - log: &slog::Logger, - user_contexts: &[Arc<(String, OpContext)>], - test_resources: &Resources, -) -> std::io::Result<()> { - let mut futures = futures::stream::FuturesOrdered::new(); - - // Run the per-resource tests in parallel. - for resource in test_resources.all_resources() { - let log = log.new(o!("resource" => format!("{:?}", resource))); - futures.push(test_one_resource( - log, - user_contexts.to_owned(), - Arc::clone(&resource), - )); - } - - let outputs: Vec = futures.collect().await; - for o in outputs { - write!(out, "{}", o)?; - } - - write!(out, "ACTIONS:\n\n")?; - for action in authz::Action::iter() { - write!(out, " {:>2} = {:?}\n", action_abbreviation(action), action)?; - } - write!(out, "\n")?; - - Ok(()) -} - -async fn test_one_resource( - log: slog::Logger, - user_contexts: Vec>, - resource: Arc, -) -> String { - let task = tokio::spawn(async move { - let mut buffer = Vec::new(); - let mut out = Cursor::new(&mut buffer); - write!(out, "resource: {}\n\n", resource.resource_name())?; - - write!(out, " {:31}", "USER")?; - for action in authz::Action::iter() { - write!(out, " {:>2}", action_abbreviation(action))?; - } - write!(out, "\n")?; - - for ctx_tuple in user_contexts.iter() { - let (ref username, ref opctx) = **ctx_tuple; - write!(out, " {:31}", &username)?; - for action in authz::Action::iter() { - let result = resource.do_authorize(opctx, action).await; - trace!( - log, - "do_authorize result"; - "username" => username.clone(), - "resource" => ?resource, - "action" => ?action, - "result" => ?result, - ); - let summary = match result { - Ok(_) => '\u{2714}', - Err(Error::Forbidden) - | Err(Error::ObjectNotFound { .. }) => '\u{2718}', - Err(Error::Unauthenticated { .. }) => '!', - Err(_) => '\u{26a0}', - }; - write!(out, " {:>2}", summary)?; - } - write!(out, "\n")?; - } - - write!(out, "\n")?; - Ok(buffer) - }); - - let result: std::io::Result> = - task.await.expect("failed to wait for task"); - let result_str = result.expect("failed to write to string buffer"); - String::from_utf8(result_str).expect("unexpected non-UTF8 output") -} - -/// Describes the hierarchy of resources used in our RBAC test -// The hierarchy looks like this: -// fleet -// fleet/s1 -// fleet/s1/o1 -// fleet/s1/o1/p1 -// fleet/s1/o1/p1/vpc1 -// fleet/s1/o1/p2 -// fleet/s1/o1/p2/vpc1 -// fleet/s1/o2 -// fleet/s1/o2/p1 -// fleet/s1/o2/p1/vpc1 -// fleet/s2 -// fleet/s2/o1 -// fleet/s2/o1/p1 -// fleet/s2/o1/p1/vpc1 -struct Resources { - silo1: authz::Silo, - silo1_org1: authz::Organization, - silo1_org1_proj1: authz::Project, - silo1_org1_proj1_children: Vec>, - silo1_org1_proj2: authz::Project, - silo1_org1_proj2_children: Vec>, - silo1_org2: authz::Organization, - silo1_org2_proj1: authz::Project, - silo1_org2_proj1_children: Vec>, - silo2: authz::Silo, - silo2_org1: authz::Organization, - silo2_org1_proj1: authz::Project, - silo2_org1_proj1_children: Vec>, -} - -impl Resources { - fn all_resources( - &self, - ) -> impl std::iter::Iterator> + '_ { - vec![ - Arc::new(authz::DATABASE.clone()) as Arc, - Arc::new(authz::FLEET.clone()) as Arc, - Arc::new(self.silo1.clone()) as Arc, - Arc::new(self.silo1_org1.clone()) as Arc, - Arc::new(self.silo1_org1_proj1.clone()) as Arc, - ] - .into_iter() - .chain(self.silo1_org1_proj1_children.iter().cloned()) - .chain(std::iter::once( - Arc::new(self.silo1_org1_proj2.clone()) as Arc - )) - .chain(self.silo1_org1_proj2_children.iter().cloned()) - .chain(vec![ - Arc::new(self.silo1_org2.clone()) as Arc, - Arc::new(self.silo1_org2_proj1.clone()) as Arc, - ]) - .chain(self.silo1_org2_proj1_children.iter().cloned()) - .chain(vec![ - Arc::new(self.silo2.clone()) as Arc, - Arc::new(self.silo2_org1.clone()) as Arc, - Arc::new(self.silo2_org1_proj1.clone()) as Arc, - ]) - .chain(self.silo2_org1_proj1_children.iter().cloned()) - } -} - -trait Authorizable: AuthorizedResource + std::fmt::Debug { - fn resource_name(&self) -> String; - - fn do_authorize<'a, 'b>( - &'a self, - opctx: &'b OpContext, - action: authz::Action, - ) -> BoxFuture<'a, Result<(), Error>> - where - 'b: 'a; - - fn polar_class(&self) -> oso::Class; -} - -impl Authorizable for T -where - T: ApiResource + AuthorizedResource + oso::PolarClass + Clone, -{ - fn do_authorize<'a, 'b>( - &'a self, - opctx: &'b OpContext, - action: authz::Action, - ) -> BoxFuture<'a, Result<(), Error>> - where - 'b: 'a, - { - opctx.authorize(action, self).boxed() - } - - fn resource_name(&self) -> String { - let my_ident = match self.lookup_type() { - LookupType::ByName(name) => format!("{:?}", name), - LookupType::ById(id) => format!("id {:?}", id.to_string()), - LookupType::BySessionToken(_) | LookupType::ByCompositeId(_) => { - unimplemented!() - } - }; - - format!("{:?} {}", self.resource_type(), my_ident) - } - - fn polar_class(&self) -> oso::Class { - T::get_polar_class() - } -} - -impl Authorizable for authz::oso_generic::Database { - fn resource_name(&self) -> String { - String::from("DATABASE") - } - - fn do_authorize<'a, 'b>( - &'a self, - opctx: &'b OpContext, - action: authz::Action, - ) -> BoxFuture<'a, Result<(), Error>> - where - 'b: 'a, - { - opctx.authorize(action, self).boxed() - } - - fn polar_class(&self) -> oso::Class { - authz::oso_generic::Database::get_polar_class() - } -} - -// XXX-dap make this deterministic -// Most of the uuids here are hardcoded rather than randomly generated for -// debuggability. -fn make_uuid() -> Uuid { - Uuid::new_v4() -} - -fn make_resources(coverage: &mut Coverage) -> Resources { - // "Database" and "Fleet" are implicitly included by virtue of being - // returned by the iterator. - coverage.covered(&authz::DATABASE); - coverage.covered(&authz::FLEET); - - let silo1_id = make_uuid(); - let silo1 = authz::Silo::new( - authz::FLEET, - silo1_id, - LookupType::ByName(String::from("silo1")), - ); - coverage.covered(&silo1); - - let silo1_org1 = authz::Organization::new( - silo1.clone(), - make_uuid(), - LookupType::ByName(String::from("silo1-org1")), - ); - coverage.covered(&silo1_org1); - let (silo1_org1_proj1, silo1_org1_proj1_children) = - make_project(coverage, &silo1_org1, "silo1-org1-proj1"); - let (silo1_org1_proj2, silo1_org1_proj2_children) = - make_project(coverage, &silo1_org1, "silo1-org1-proj2"); - - let silo1_org2 = authz::Organization::new( - silo1.clone(), - make_uuid(), - LookupType::ByName(String::from("silo1-org2")), - ); - let (silo1_org2_proj1, silo1_org2_proj1_children) = - make_project(coverage, &silo1_org2, "silo1-org2-proj1"); - - let silo2_id = make_uuid(); - let silo2 = authz::Silo::new( - authz::FLEET, - silo2_id, - LookupType::ByName(String::from("silo2")), - ); - - let silo2_org1 = authz::Organization::new( - silo2.clone(), - make_uuid(), - LookupType::ByName(String::from("silo2-org1")), - ); - let (silo2_org1_proj1, silo2_org1_proj1_children) = - make_project(coverage, &silo2_org1, "silo2-org1-proj1"); - - Resources { - silo1, - silo1_org1, - silo1_org1_proj1, - silo1_org1_proj1_children, - silo1_org1_proj2, - silo1_org1_proj2_children, - silo1_org2, - silo1_org2_proj1, - silo1_org2_proj1_children, - silo2, - silo2_org1, - silo2_org1_proj1, - silo2_org1_proj1_children, - } -} - -fn make_project( - coverage: &mut Coverage, - organization: &authz::Organization, - project_name: &str, -) -> (authz::Project, Vec>) { - let project = authz::Project::new( - organization.clone(), - make_uuid(), - LookupType::ByName(project_name.to_string()), - ); - coverage.covered(&project); - - let vpc1_name = format!("{}-vpc1", project_name); - let vpc1 = authz::Vpc::new( - project.clone(), - make_uuid(), - LookupType::ByName(vpc1_name.clone()), - ); - coverage.covered(&vpc1); - let children: Vec> = vec![ - // XXX-dap TODO-coverage add more different kinds of children - Arc::new(authz::Disk::new( - project.clone(), - make_uuid(), - LookupType::ByName(format!("{}-disk1", project_name)), - )), - Arc::new(authz::Instance::new( - project.clone(), - make_uuid(), - LookupType::ByName(format!("{}-instance1", project_name)), - )), - Arc::new(vpc1.clone()), - // Test a resource nested two levels below Project - Arc::new(authz::VpcSubnet::new( - vpc1, - make_uuid(), - LookupType::ByName(format!("{}-subnet1", vpc1_name)), - )), - ]; - for c in &children { - coverage.covered_class(c.polar_class()); - } - - (project, children) -} - -async fn create_users( - opctx: &OpContext, - datastore: &db::DataStore, - resource_name: &str, - silo_id: Uuid, - authz_resource: &T, - users: &mut Vec<(String, Uuid)>, -) where - T: ApiResourceWithRolesType + oso::PolarClass + Clone, - T::AllowedRoles: IntoEnumIterator, -{ - for role in T::AllowedRoles::iter() { - let role_name = role.to_database_string(); - let username = format!("{}-{}", resource_name, role_name); - let user_id = make_uuid(); - println!("creating user: {}", &username); - users.push((username.clone(), user_id)); - - let silo_user = db::model::SiloUser::new(silo_id, user_id, username); - datastore - .silo_user_create(silo_user) - .await - .expect("failed to create silo user"); - - let old_role_assignments = datastore - .role_assignment_fetch_visible(opctx, authz_resource) - .await - .expect("fetching policy"); - let new_role_assignments = old_role_assignments - .into_iter() - .map(|r| r.try_into().unwrap()) - .chain(std::iter::once(shared::RoleAssignment { - identity_type: shared::IdentityType::SiloUser, - identity_id: user_id, - role_name: role, - })) - .collect::>(); - datastore - .role_assignment_replace_visible( - opctx, - authz_resource, - &new_role_assignments, - ) - .await - .expect("failed to assign role"); - } -} - -fn action_abbreviation(action: authz::Action) -> &'static str { - match action { - authz::Action::Query => "Q", - authz::Action::Read => "R", - authz::Action::ListChildren => "LC", - authz::Action::ReadPolicy => "RP", - authz::Action::Modify => "M", - authz::Action::ModifyPolicy => "MP", - authz::Action::CreateChild => "CC", - authz::Action::Delete => "D", - authz::Action::ListIdentityProviders => "LP", - } -} - -/// `Write` impl that writes everything it's given to both a destination `Write` -/// and stdout via `print!`. -/// -/// It'd be nice if this were instead a generic `Tee` that took an arbitrary -/// number of `Write`s and wrote data to all of them. That's possible, but it -/// wouldn't do what we want. We need to use `print!` in order for output to be -/// captured by the test runner. See rust-lang/rust#12309. -struct StdoutTee { - sink: W, -} - -impl StdoutTee { - fn new(sink: W) -> StdoutTee { - StdoutTee { sink } - } -} - -impl Write for StdoutTee { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - print!("{}", std::str::from_utf8(buf).expect("non-UTF8 in stdout tee")); - self.sink.write(buf) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.sink.flush() - } -} diff --git a/nexus/src/authz/policy_test/coverage.rs b/nexus/src/authz/policy_test/coverage.rs new file mode 100644 index 0000000000..d597ec0d31 --- /dev/null +++ b/nexus/src/authz/policy_test/coverage.rs @@ -0,0 +1,134 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::authz; +use oso::PolarClass; +use std::collections::BTreeSet; + +/// Helper for identifying authz resources not covered by the IAM role policy +/// test +pub struct Coverage { + log: slog::Logger, + /// names of all authz classes + class_names: BTreeSet, + /// names of authz classes for which we expect to find no tests + exempted: BTreeSet, + /// names of authz classes for which we have found a test + covered: BTreeSet, +} + +impl Coverage { + pub fn new(log: &slog::Logger) -> Coverage { + let log = log.new(o!("component" => "IamTestCoverage")); + let authz = authz::Authz::new(&log); + let class_names = authz.into_class_names(); + + // Class names should be added to this exemption list when their Polar + // code snippets and authz behavior is identical to another class. This + // is primarily for performance reasons because this test takes a long + // time. But with every exemption comes the risk of a security issue! + // + // PLEASE: instead of adding a class to this list, consider updating + // this test to create an instance of the class and then test it. + let exempted = [ + // Non-resources + authz::Action::get_polar_class(), + authz::actor::AnyActor::get_polar_class(), + authz::actor::AuthenticatedActor::get_polar_class(), + // XXX-dap TODO-coverage Not yet implemented, but not exempted for a + // good reason. + authz::IpPoolList::get_polar_class(), + authz::GlobalImageList::get_polar_class(), + authz::ConsoleSessionList::get_polar_class(), + authz::DeviceAuthRequestList::get_polar_class(), + authz::IpPool::get_polar_class(), + authz::NetworkInterface::get_polar_class(), + authz::VpcRouter::get_polar_class(), + authz::RouterRoute::get_polar_class(), + authz::ConsoleSession::get_polar_class(), + authz::DeviceAuthRequest::get_polar_class(), + authz::DeviceAccessToken::get_polar_class(), + authz::Rack::get_polar_class(), + authz::RoleBuiltin::get_polar_class(), + authz::SshKey::get_polar_class(), + authz::SiloUser::get_polar_class(), + authz::SiloGroup::get_polar_class(), + authz::IdentityProvider::get_polar_class(), + authz::SamlIdentityProvider::get_polar_class(), + authz::Sled::get_polar_class(), + authz::UpdateAvailableArtifact::get_polar_class(), + authz::UserBuiltin::get_polar_class(), + authz::GlobalImage::get_polar_class(), + ] + .into_iter() + .map(|c| c.name.clone()) + .collect(); + + Coverage { log, class_names, exempted, covered: BTreeSet::new() } + } + + pub fn covered(&mut self, _covered: &T) { + self.covered_class(T::get_polar_class()) + } + + pub fn covered_class(&mut self, class: oso::Class) { + let class_name = class.name.clone(); + debug!(&self.log, "covering"; "class_name" => &class_name); + self.covered.insert(class_name); + } + + pub fn verify(&self) { + let mut uncovered = Vec::new(); + let mut bad_exemptions = Vec::new(); + + for class_name in &self.class_names { + let class_name = class_name.as_str(); + let exempted = self.exempted.contains(class_name); + let covered = self.covered.contains(class_name); + + match (exempted, covered) { + (true, false) => { + // XXX-dap consider checking whether the Polar snippet + // exactly matches that of another class? + debug!(&self.log, "exempt"; "class_name" => class_name); + } + (false, true) => { + debug!(&self.log, "covered"; "class_name" => class_name); + } + (true, true) => { + error!( + &self.log, + "bad exemption (class was covered)"; + "class_name" => class_name + ); + bad_exemptions.push(class_name); + } + (false, false) => { + error!( + &self.log, + "uncovered class"; + "class_name" => class_name + ); + uncovered.push(class_name); + } + }; + } + + if !bad_exemptions.is_empty() { + panic!( + "these classes were covered by the tests and should \ + not have been part of the exemption list: {}", + bad_exemptions.join(", ") + ); + } + + if !uncovered.is_empty() { + panic!( + "these classes were not covered by the IAM role \ + policy test: {}", + uncovered.join(", ") + ); + } + } +} diff --git a/nexus/src/authz/policy_test/mod.rs b/nexus/src/authz/policy_test/mod.rs new file mode 100644 index 0000000000..a77f9721be --- /dev/null +++ b/nexus/src/authz/policy_test/mod.rs @@ -0,0 +1,340 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Unit tests for the Oso policy +//! +//! This differs from the end-to-end integration tests for authz. The tests +//! here verify: +//! +//! - for resources covered by RBAC: that the roles in the policy grant the +//! permissions that we expect that they do +//! +//! - for other policies: that the policy reflects the privileges that we expect +//! (e.g., ordinary users don't have internal roles) +//! +//! XXX-dap TODO: +//! - review above comment +//! - review remaining XXX-dap +//! - clean up, document test +//! - figure out what other types to add + +use super::ApiResourceWithRolesType; +use crate::authn; +use crate::authz; +use crate::context::OpContext; +use crate::db; +use crate::db::model::DatabaseString; +use crate::external_api::shared; +use authz::ApiResourceWithRoles; +use futures::StreamExt; +use nexus_test_utils::db::test_setup_database; +use omicron_common::api::external::Error; +use omicron_test_utils::dev; +use std::io::Cursor; +use std::io::Write; +use std::sync::Arc; +use strum::IntoEnumIterator; +use uuid::Uuid; + +mod coverage; +mod resources; +use self::resources::Authorizable; +use coverage::Coverage; +use resources::Resources; + +#[tokio::test(flavor = "multi_thread")] +async fn test_iam_roles_behavior() { + let logctx = dev::test_setup_log("test_iam_roles"); + let mut db = test_setup_database(&logctx.log).await; + let (opctx, datastore) = db::datastore::datastore_test(&logctx, &db).await; + + let mut coverage = Coverage::new(&logctx.log); + let test_resources = resources::make_resources(&mut coverage); + coverage.verify(); + + let silo1_id = test_resources.silo1.resource_id(); + let mut users: Vec<(String, Uuid)> = Vec::new(); + + create_users( + &opctx, + &*datastore, + "fleet", + silo1_id, + &authz::FLEET, + &mut users, + ) + .await; + + create_users( + &opctx, + &*datastore, + "silo1", + silo1_id, + &test_resources.silo1, + &mut users, + ) + .await; + + create_users( + &opctx, + &*datastore, + "silo1-org1", + silo1_id, + &test_resources.silo1_org1, + &mut users, + ) + .await; + + create_users( + &opctx, + &*datastore, + "silo1-org1-proj1", + silo1_id, + &test_resources.silo1_org1_proj1, + &mut users, + ) + .await; + + // Create an OpContext for each user for testing. + let authz = Arc::new(authz::Authz::new(&logctx.log)); + let mut user_contexts: Vec> = users + .iter() + .map(|(username, user_id)| { + let user_id = *user_id; + let user_log = logctx.log.new(o!( + "user_id" => user_id.to_string(), + "username" => username.clone(), + )); + let opctx = OpContext::for_background( + user_log, + Arc::clone(&authz), + authn::Context::for_test_user(user_id, silo1_id), + Arc::clone(&datastore), + ); + + Arc::new((username.clone(), opctx)) + }) + .collect(); + + // Create and test an unauthenticated OpContext as well. + // + // We could test the "test-privileged" and "test-unprivileged" users, but it + // wouldn't be very interesting: they're in a different Silo than the + // resources that we're checking against so even "test-privileged" won't + // have privileges here. Anyway, they're composed of ordinary role + // assignments so they're just a special case of what we're already testing. + let user_log = logctx.log.new(o!( + "username" => "unauthenticated", + )); + user_contexts.push(Arc::new(( + String::from("unauthenticated"), + OpContext::for_background( + user_log, + Arc::clone(&authz), + authn::Context::internal_unauthenticated(), + Arc::clone(&datastore), + ), + ))); + + let mut buffer = Vec::new(); + { + let mut out = StdoutTee::new(&mut buffer); + run_test_operations( + &mut out, + &logctx.log, + &user_contexts, + &test_resources, + ) + .await + .unwrap(); + } + + expectorate::assert_contents( + "tests/output/authz-roles.out", + &std::str::from_utf8(buffer.as_ref()).expect("non-UTF8 output"), + ); + + db.cleanup().await.unwrap(); + logctx.cleanup_successful(); +} + +async fn run_test_operations( + mut out: W, + log: &slog::Logger, + user_contexts: &[Arc<(String, OpContext)>], + test_resources: &Resources, +) -> std::io::Result<()> { + let mut futures = futures::stream::FuturesOrdered::new(); + + // Run the per-resource tests in parallel. + for resource in test_resources.all_resources() { + let log = log.new(o!("resource" => format!("{:?}", resource))); + futures.push(test_one_resource( + log, + user_contexts.to_owned(), + Arc::clone(&resource), + )); + } + + let outputs: Vec = futures.collect().await; + for o in outputs { + write!(out, "{}", o)?; + } + + write!(out, "ACTIONS:\n\n")?; + for action in authz::Action::iter() { + write!(out, " {:>2} = {:?}\n", action_abbreviation(action), action)?; + } + write!(out, "\n")?; + + Ok(()) +} + +async fn test_one_resource( + log: slog::Logger, + user_contexts: Vec>, + resource: Arc, +) -> String { + let task = tokio::spawn(async move { + let mut buffer = Vec::new(); + let mut out = Cursor::new(&mut buffer); + write!(out, "resource: {}\n\n", resource.resource_name())?; + + write!(out, " {:31}", "USER")?; + for action in authz::Action::iter() { + write!(out, " {:>2}", action_abbreviation(action))?; + } + write!(out, "\n")?; + + for ctx_tuple in user_contexts.iter() { + let (ref username, ref opctx) = **ctx_tuple; + write!(out, " {:31}", &username)?; + for action in authz::Action::iter() { + let result = resource.do_authorize(opctx, action).await; + trace!( + log, + "do_authorize result"; + "username" => username.clone(), + "resource" => ?resource, + "action" => ?action, + "result" => ?result, + ); + let summary = match result { + Ok(_) => '\u{2714}', + Err(Error::Forbidden) + | Err(Error::ObjectNotFound { .. }) => '\u{2718}', + Err(Error::Unauthenticated { .. }) => '!', + Err(_) => '\u{26a0}', + }; + write!(out, " {:>2}", summary)?; + } + write!(out, "\n")?; + } + + write!(out, "\n")?; + Ok(buffer) + }); + + let result: std::io::Result> = + task.await.expect("failed to wait for task"); + let result_str = result.expect("failed to write to string buffer"); + String::from_utf8(result_str).expect("unexpected non-UTF8 output") +} + +async fn create_users( + opctx: &OpContext, + datastore: &db::DataStore, + resource_name: &str, + silo_id: Uuid, + authz_resource: &T, + users: &mut Vec<(String, Uuid)>, +) where + T: ApiResourceWithRolesType + oso::PolarClass + Clone, + T::AllowedRoles: IntoEnumIterator, +{ + for role in T::AllowedRoles::iter() { + let role_name = role.to_database_string(); + let username = format!("{}-{}", resource_name, role_name); + let user_id = make_uuid(); + println!("creating user: {}", &username); + users.push((username.clone(), user_id)); + + let silo_user = db::model::SiloUser::new(silo_id, user_id, username); + datastore + .silo_user_create(silo_user) + .await + .expect("failed to create silo user"); + + let old_role_assignments = datastore + .role_assignment_fetch_visible(opctx, authz_resource) + .await + .expect("fetching policy"); + let new_role_assignments = old_role_assignments + .into_iter() + .map(|r| r.try_into().unwrap()) + .chain(std::iter::once(shared::RoleAssignment { + identity_type: shared::IdentityType::SiloUser, + identity_id: user_id, + role_name: role, + })) + .collect::>(); + datastore + .role_assignment_replace_visible( + opctx, + authz_resource, + &new_role_assignments, + ) + .await + .expect("failed to assign role"); + } +} + +fn action_abbreviation(action: authz::Action) -> &'static str { + match action { + authz::Action::Query => "Q", + authz::Action::Read => "R", + authz::Action::ListChildren => "LC", + authz::Action::ReadPolicy => "RP", + authz::Action::Modify => "M", + authz::Action::ModifyPolicy => "MP", + authz::Action::CreateChild => "CC", + authz::Action::Delete => "D", + authz::Action::ListIdentityProviders => "LP", + } +} + +// XXX-dap make this deterministic +// Most of the uuids here are hardcoded rather than randomly generated for +// debuggability. +pub fn make_uuid() -> Uuid { + Uuid::new_v4() +} + +/// `Write` impl that writes everything it's given to both a destination `Write` +/// and stdout via `print!`. +/// +/// It'd be nice if this were instead a generic `Tee` that took an arbitrary +/// number of `Write`s and wrote data to all of them. That's possible, but it +/// wouldn't do what we want. We need to use `print!` in order for output to be +/// captured by the test runner. See rust-lang/rust#12309. +struct StdoutTee { + sink: W, +} + +impl StdoutTee { + fn new(sink: W) -> StdoutTee { + StdoutTee { sink } + } +} + +impl Write for StdoutTee { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + print!("{}", std::str::from_utf8(buf).expect("non-UTF8 in stdout tee")); + self.sink.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.sink.flush() + } +} diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs new file mode 100644 index 0000000000..6b263f5478 --- /dev/null +++ b/nexus/src/authz/policy_test/resources.rs @@ -0,0 +1,269 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Structures and functions related to the resources created for the IAM role +//! policy test + +use super::coverage::Coverage; +use super::make_uuid; +use crate::authz; +use crate::authz::AuthorizedResource; +use crate::context::OpContext; +use authz::ApiResource; +use futures::future::BoxFuture; +use futures::FutureExt; +use lazy_static::lazy_static; +use omicron_common::api::external::Error; +use omicron_common::api::external::LookupType; +use oso::PolarClass; +use std::sync::Arc; +use uuid::Uuid; + +lazy_static! { + pub static ref SILO1_ID: Uuid = make_uuid(); +} + +/// Describes the hierarchy of resources used in our RBAC test +// The hierarchy looks like this: +// fleet +// fleet/s1 +// fleet/s1/o1 +// fleet/s1/o1/p1 +// fleet/s1/o1/p1/vpc1 +// fleet/s1/o1/p2 +// fleet/s1/o1/p2/vpc1 +// fleet/s1/o2 +// fleet/s1/o2/p1 +// fleet/s1/o2/p1/vpc1 +// fleet/s2 +// fleet/s2/o1 +// fleet/s2/o1/p1 +// fleet/s2/o1/p1/vpc1 +pub struct Resources { + // XXX-dap + //all_resources: Vec>, + //role_targets: Vec>, + pub silo1: authz::Silo, + pub silo1_org1: authz::Organization, + pub silo1_org1_proj1: authz::Project, + pub silo1_org1_proj1_children: Vec>, + pub silo1_org1_proj2: authz::Project, + pub silo1_org1_proj2_children: Vec>, + pub silo1_org2: authz::Organization, + pub silo1_org2_proj1: authz::Project, + pub silo1_org2_proj1_children: Vec>, + pub silo2: authz::Silo, + pub silo2_org1: authz::Organization, + pub silo2_org1_proj1: authz::Project, + pub silo2_org1_proj1_children: Vec>, +} + +impl Resources { + pub fn all_resources( + &self, + ) -> impl std::iter::Iterator> + '_ { + vec![ + Arc::new(authz::DATABASE.clone()) as Arc, + Arc::new(authz::FLEET.clone()) as Arc, + Arc::new(self.silo1.clone()) as Arc, + Arc::new(self.silo1_org1.clone()) as Arc, + Arc::new(self.silo1_org1_proj1.clone()) as Arc, + ] + .into_iter() + .chain(self.silo1_org1_proj1_children.iter().cloned()) + .chain(std::iter::once( + Arc::new(self.silo1_org1_proj2.clone()) as Arc + )) + .chain(self.silo1_org1_proj2_children.iter().cloned()) + .chain(vec![ + Arc::new(self.silo1_org2.clone()) as Arc, + Arc::new(self.silo1_org2_proj1.clone()) as Arc, + ]) + .chain(self.silo1_org2_proj1_children.iter().cloned()) + .chain(vec![ + Arc::new(self.silo2.clone()) as Arc, + Arc::new(self.silo2_org1.clone()) as Arc, + Arc::new(self.silo2_org1_proj1.clone()) as Arc, + ]) + .chain(self.silo2_org1_proj1_children.iter().cloned()) + } +} + +pub trait Authorizable: AuthorizedResource + std::fmt::Debug { + fn resource_name(&self) -> String; + + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a; + + fn polar_class(&self) -> oso::Class; +} + +impl Authorizable for T +where + T: ApiResource + AuthorizedResource + oso::PolarClass + Clone, +{ + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a, + { + opctx.authorize(action, self).boxed() + } + + fn resource_name(&self) -> String { + let my_ident = match self.lookup_type() { + LookupType::ByName(name) => format!("{:?}", name), + LookupType::ById(id) => format!("id {:?}", id.to_string()), + LookupType::BySessionToken(_) | LookupType::ByCompositeId(_) => { + unimplemented!() + } + }; + + format!("{:?} {}", self.resource_type(), my_ident) + } + + fn polar_class(&self) -> oso::Class { + T::get_polar_class() + } +} + +impl Authorizable for authz::oso_generic::Database { + fn resource_name(&self) -> String { + String::from("DATABASE") + } + + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a, + { + opctx.authorize(action, self).boxed() + } + + fn polar_class(&self) -> oso::Class { + authz::oso_generic::Database::get_polar_class() + } +} + +pub fn make_resources(coverage: &mut Coverage) -> Resources { + // "Database" and "Fleet" are implicitly included by virtue of being + // returned by the iterator. + coverage.covered(&authz::DATABASE); + coverage.covered(&authz::FLEET); + + let silo1_id = make_uuid(); + let silo1 = authz::Silo::new( + authz::FLEET, + silo1_id, + LookupType::ByName(String::from("silo1")), + ); + coverage.covered(&silo1); + + let silo1_org1 = authz::Organization::new( + silo1.clone(), + make_uuid(), + LookupType::ByName(String::from("silo1-org1")), + ); + coverage.covered(&silo1_org1); + let (silo1_org1_proj1, silo1_org1_proj1_children) = + make_project(coverage, &silo1_org1, "silo1-org1-proj1"); + let (silo1_org1_proj2, silo1_org1_proj2_children) = + make_project(coverage, &silo1_org1, "silo1-org1-proj2"); + + let silo1_org2 = authz::Organization::new( + silo1.clone(), + make_uuid(), + LookupType::ByName(String::from("silo1-org2")), + ); + let (silo1_org2_proj1, silo1_org2_proj1_children) = + make_project(coverage, &silo1_org2, "silo1-org2-proj1"); + + let silo2_id = make_uuid(); + let silo2 = authz::Silo::new( + authz::FLEET, + silo2_id, + LookupType::ByName(String::from("silo2")), + ); + + let silo2_org1 = authz::Organization::new( + silo2.clone(), + make_uuid(), + LookupType::ByName(String::from("silo2-org1")), + ); + let (silo2_org1_proj1, silo2_org1_proj1_children) = + make_project(coverage, &silo2_org1, "silo2-org1-proj1"); + + Resources { + silo1, + silo1_org1, + silo1_org1_proj1, + silo1_org1_proj1_children, + silo1_org1_proj2, + silo1_org1_proj2_children, + silo1_org2, + silo1_org2_proj1, + silo1_org2_proj1_children, + silo2, + silo2_org1, + silo2_org1_proj1, + silo2_org1_proj1_children, + } +} + +fn make_project( + coverage: &mut Coverage, + organization: &authz::Organization, + project_name: &str, +) -> (authz::Project, Vec>) { + let project = authz::Project::new( + organization.clone(), + make_uuid(), + LookupType::ByName(project_name.to_string()), + ); + coverage.covered(&project); + + let vpc1_name = format!("{}-vpc1", project_name); + let vpc1 = authz::Vpc::new( + project.clone(), + make_uuid(), + LookupType::ByName(vpc1_name.clone()), + ); + coverage.covered(&vpc1); + let children: Vec> = vec![ + // XXX-dap TODO-coverage add more different kinds of children + Arc::new(authz::Disk::new( + project.clone(), + make_uuid(), + LookupType::ByName(format!("{}-disk1", project_name)), + )), + Arc::new(authz::Instance::new( + project.clone(), + make_uuid(), + LookupType::ByName(format!("{}-instance1", project_name)), + )), + Arc::new(vpc1.clone()), + // Test a resource nested two levels below Project + Arc::new(authz::VpcSubnet::new( + vpc1, + make_uuid(), + LookupType::ByName(format!("{}-subnet1", vpc1_name)), + )), + ]; + for c in &children { + coverage.covered_class(c.polar_class()); + } + + (project, children) +} From 8b8cc539687306e453716ef36cf7361a4c092e25 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 10 Aug 2022 13:56:23 -0700 Subject: [PATCH 12/32] move polar_class stuff around -- use dynamic dispatch --- nexus/src/authz/api_resources.rs | 23 ++++++++++++++++++++++- nexus/src/authz/context.rs | 11 +++++++++-- nexus/src/authz/oso_generic.rs | 4 ++++ nexus/src/authz/policy_test/resources.rs | 11 ----------- nexus/src/context.rs | 2 +- nexus/src/db/datastore/role.rs | 7 ++++--- 6 files changed, 40 insertions(+), 18 deletions(-) diff --git a/nexus/src/authz/api_resources.rs b/nexus/src/authz/api_resources.rs index d0428b69b0..2ed038a128 100644 --- a/nexus/src/authz/api_resources.rs +++ b/nexus/src/authz/api_resources.rs @@ -46,6 +46,7 @@ use futures::future::BoxFuture; use futures::FutureExt; use lazy_static::lazy_static; use omicron_common::api::external::{Error, LookupType, ResourceType}; +use oso::PolarClass; use parse_display::Display; use parse_display::FromStr; use schemars::JsonSchema; @@ -93,7 +94,7 @@ pub trait ApiResourceWithRolesType: ApiResourceWithRoles { + Clone; } -impl AuthorizedResource for T { +impl AuthorizedResource for T { fn load_roles<'a, 'b, 'c, 'd, 'e, 'f>( &'a self, opctx: &'b OpContext, @@ -135,6 +136,10 @@ impl AuthorizedResource for T { Ok(true) => error, } } + + fn polar_class(&self) -> oso::Class { + Self::get_polar_class() + } } /// Represents the Oxide fleet for authz purposes @@ -299,6 +304,10 @@ impl AuthorizedResource for ConsoleSessionList { ) -> Error { error } + + fn polar_class(&self) -> oso::Class { + Self::get_polar_class() + } } #[derive(Clone, Copy, Debug)] @@ -360,6 +369,10 @@ impl AuthorizedResource for GlobalImageList { ) -> Error { error } + + fn polar_class(&self) -> oso::Class { + Self::get_polar_class() + } } #[derive(Clone, Copy, Debug)] @@ -422,6 +435,10 @@ impl AuthorizedResource for IpPoolList { ) -> Error { error } + + fn polar_class(&self) -> oso::Class { + Self::get_polar_class() + } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -476,6 +493,10 @@ impl AuthorizedResource for DeviceAuthRequestList { ) -> Error { error } + + fn polar_class(&self) -> oso::Class { + Self::get_polar_class() + } } // Main resource hierarchy: Organizations, Projects, and their resources diff --git a/nexus/src/authz/context.rs b/nexus/src/authz/context.rs index 9d530847a9..e95c2cbb01 100644 --- a/nexus/src/authz/context.rs +++ b/nexus/src/authz/context.rs @@ -86,7 +86,7 @@ impl Context { resource: Resource, ) -> Result<(), Error> where - Resource: AuthorizedResource + oso::PolarClass + Clone, + Resource: AuthorizedResource + Clone, { // If we're given a resource whose PolarClass was never registered with // Oso, then the call to `is_allowed()` below will always return false @@ -102,7 +102,7 @@ impl Context { // of a programmer error than an operational error. But unlike most // programmer errors, the nature of the problem and the blast radius are // well understood, so we may as well avoid crashing.) - let class_name = &Resource::get_polar_class().name; + let class_name = &resource.polar_class().name; bail_unless!( self.authz.class_names.contains(class_name), "attempted authz check on unregistered resource: {:?}", @@ -186,6 +186,9 @@ pub trait AuthorizedResource: oso::ToPolar + Send + Sync + 'static { actor: AnyActor, action: Action, ) -> Error; + + /// Returns the Polar class that implements this resource + fn polar_class(&self) -> oso::Class; } #[cfg(test)] @@ -253,6 +256,10 @@ mod test { // authorize() shouldn't get far enough to call this. unimplemented!(); } + + fn polar_class(&self) -> oso::Class { + Self::get_polar_class() + } } // Make sure an authz check with this resource fails with a clear diff --git a/nexus/src/authz/oso_generic.rs b/nexus/src/authz/oso_generic.rs index 8f7de9c064..79dd23a5e4 100644 --- a/nexus/src/authz/oso_generic.rs +++ b/nexus/src/authz/oso_generic.rs @@ -289,6 +289,10 @@ impl AuthorizedResource for Database { ) -> Error { error } + + fn polar_class(&self) -> oso::Class { + Self::get_polar_class() + } } #[cfg(test)] diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index 6b263f5478..786d968b14 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -16,7 +16,6 @@ use futures::FutureExt; use lazy_static::lazy_static; use omicron_common::api::external::Error; use omicron_common::api::external::LookupType; -use oso::PolarClass; use std::sync::Arc; use uuid::Uuid; @@ -100,8 +99,6 @@ pub trait Authorizable: AuthorizedResource + std::fmt::Debug { ) -> BoxFuture<'a, Result<(), Error>> where 'b: 'a; - - fn polar_class(&self) -> oso::Class; } impl Authorizable for T @@ -130,10 +127,6 @@ where format!("{:?} {}", self.resource_type(), my_ident) } - - fn polar_class(&self) -> oso::Class { - T::get_polar_class() - } } impl Authorizable for authz::oso_generic::Database { @@ -151,10 +144,6 @@ impl Authorizable for authz::oso_generic::Database { { opctx.authorize(action, self).boxed() } - - fn polar_class(&self) -> oso::Class { - authz::oso_generic::Database::get_polar_class() - } } pub fn make_resources(coverage: &mut Coverage) -> Resources { diff --git a/nexus/src/context.rs b/nexus/src/context.rs index d4a2679893..231f235a2f 100644 --- a/nexus/src/context.rs +++ b/nexus/src/context.rs @@ -446,7 +446,7 @@ impl OpContext { resource: &Resource, ) -> Result<(), Error> where - Resource: AuthorizedResource + Debug + Clone + oso::PolarClass, + Resource: AuthorizedResource + Debug + Clone, { // TODO-cleanup In an ideal world, Oso would consume &Action and // &Resource. Instead, it consumes owned types. As a result, they're diff --git a/nexus/src/db/datastore/role.rs b/nexus/src/db/datastore/role.rs index f3b4fd9982..03ce9ecb25 100644 --- a/nexus/src/db/datastore/role.rs +++ b/nexus/src/db/datastore/role.rs @@ -6,6 +6,7 @@ use super::DataStore; use crate::authz; +use crate::authz::AuthorizedResource; use crate::context::OpContext; use crate::db; use crate::db::datastore::RunnableQuery; @@ -185,7 +186,7 @@ impl DataStore { // is mitigated because we cap the number of role assignments per resource // pretty tightly. pub async fn role_assignment_fetch_visible< - T: authz::ApiResourceWithRoles + Clone + oso::PolarClass, + T: authz::ApiResourceWithRoles + AuthorizedResource + Clone, >( &self, opctx: &OpContext, @@ -231,7 +232,7 @@ impl DataStore { new_assignments: &[shared::RoleAssignment], ) -> ListResultVec where - T: authz::ApiResourceWithRolesType + Clone + oso::PolarClass, + T: authz::ApiResourceWithRolesType + AuthorizedResource + Clone, { // TODO-security We should carefully review what permissions are // required for modifying the policy of a resource. @@ -283,7 +284,7 @@ impl DataStore { Error, > where - T: authz::ApiResourceWithRolesType + oso::PolarClass + Clone, + T: authz::ApiResourceWithRolesType + AuthorizedResource + Clone, { opctx.authorize(authz::Action::ModifyPolicy, authz_resource).await?; From 02d391119cf1b08d8fd6860ca13fc471a0c3e488 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 10 Aug 2022 16:37:15 -0700 Subject: [PATCH 13/32] refactor resource generation to be less ad hoc --- nexus/src/authz/policy_test/coverage.rs | 5 +- nexus/src/authz/policy_test/mod.rs | 107 +------ nexus/src/authz/policy_test/resources.rs | 370 +++++++++++++---------- 3 files changed, 228 insertions(+), 254 deletions(-) diff --git a/nexus/src/authz/policy_test/coverage.rs b/nexus/src/authz/policy_test/coverage.rs index d597ec0d31..8208f65902 100644 --- a/nexus/src/authz/policy_test/coverage.rs +++ b/nexus/src/authz/policy_test/coverage.rs @@ -3,6 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::authz; +use crate::authz::AuthorizedResource; use oso::PolarClass; use std::collections::BTreeSet; @@ -68,8 +69,8 @@ impl Coverage { Coverage { log, class_names, exempted, covered: BTreeSet::new() } } - pub fn covered(&mut self, _covered: &T) { - self.covered_class(T::get_polar_class()) + pub fn covered(&mut self, covered: &dyn AuthorizedResource) { + self.covered_class(covered.polar_class()) } pub fn covered_class(&mut self, class: oso::Class) { diff --git a/nexus/src/authz/policy_test/mod.rs b/nexus/src/authz/policy_test/mod.rs index a77f9721be..56fe65ae04 100644 --- a/nexus/src/authz/policy_test/mod.rs +++ b/nexus/src/authz/policy_test/mod.rs @@ -19,14 +19,11 @@ //! - clean up, document test //! - figure out what other types to add -use super::ApiResourceWithRolesType; +use self::resources::Authorizable; use crate::authn; use crate::authz; use crate::context::OpContext; use crate::db; -use crate::db::model::DatabaseString; -use crate::external_api::shared; -use authz::ApiResourceWithRoles; use futures::StreamExt; use nexus_test_utils::db::test_setup_database; use omicron_common::api::external::Error; @@ -39,7 +36,6 @@ use uuid::Uuid; mod coverage; mod resources; -use self::resources::Authorizable; use coverage::Coverage; use resources::Resources; @@ -50,56 +46,15 @@ async fn test_iam_roles_behavior() { let (opctx, datastore) = db::datastore::datastore_test(&logctx, &db).await; let mut coverage = Coverage::new(&logctx.log); - let test_resources = resources::make_resources(&mut coverage); + let silo1_id = *resources::SILO1_ID; + let test_resources = + Resources::new(&opctx, &datastore, &mut coverage, silo1_id).await; coverage.verify(); - let silo1_id = test_resources.silo1.resource_id(); - let mut users: Vec<(String, Uuid)> = Vec::new(); - - create_users( - &opctx, - &*datastore, - "fleet", - silo1_id, - &authz::FLEET, - &mut users, - ) - .await; - - create_users( - &opctx, - &*datastore, - "silo1", - silo1_id, - &test_resources.silo1, - &mut users, - ) - .await; - - create_users( - &opctx, - &*datastore, - "silo1-org1", - silo1_id, - &test_resources.silo1_org1, - &mut users, - ) - .await; - - create_users( - &opctx, - &*datastore, - "silo1-org1-proj1", - silo1_id, - &test_resources.silo1_org1_proj1, - &mut users, - ) - .await; - // Create an OpContext for each user for testing. let authz = Arc::new(authz::Authz::new(&logctx.log)); - let mut user_contexts: Vec> = users - .iter() + let mut user_contexts: Vec> = test_resources + .users() .map(|(username, user_id)| { let user_id = *user_id; let user_log = logctx.log.new(o!( @@ -168,7 +123,7 @@ async fn run_test_operations( let mut futures = futures::stream::FuturesOrdered::new(); // Run the per-resource tests in parallel. - for resource in test_resources.all_resources() { + for resource in test_resources.resources() { let log = log.new(o!("resource" => format!("{:?}", resource))); futures.push(test_one_resource( log, @@ -242,54 +197,6 @@ async fn test_one_resource( String::from_utf8(result_str).expect("unexpected non-UTF8 output") } -async fn create_users( - opctx: &OpContext, - datastore: &db::DataStore, - resource_name: &str, - silo_id: Uuid, - authz_resource: &T, - users: &mut Vec<(String, Uuid)>, -) where - T: ApiResourceWithRolesType + oso::PolarClass + Clone, - T::AllowedRoles: IntoEnumIterator, -{ - for role in T::AllowedRoles::iter() { - let role_name = role.to_database_string(); - let username = format!("{}-{}", resource_name, role_name); - let user_id = make_uuid(); - println!("creating user: {}", &username); - users.push((username.clone(), user_id)); - - let silo_user = db::model::SiloUser::new(silo_id, user_id, username); - datastore - .silo_user_create(silo_user) - .await - .expect("failed to create silo user"); - - let old_role_assignments = datastore - .role_assignment_fetch_visible(opctx, authz_resource) - .await - .expect("fetching policy"); - let new_role_assignments = old_role_assignments - .into_iter() - .map(|r| r.try_into().unwrap()) - .chain(std::iter::once(shared::RoleAssignment { - identity_type: shared::IdentityType::SiloUser, - identity_id: user_id, - role_name: role, - })) - .collect::>(); - datastore - .role_assignment_replace_visible( - opctx, - authz_resource, - &new_role_assignments, - ) - .await - .expect("failed to assign role"); - } -} - fn action_abbreviation(action: authz::Action) -> &'static str { match action { authz::Action::Query => "Q", diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index 786d968b14..b738f48810 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -8,21 +8,124 @@ use super::coverage::Coverage; use super::make_uuid; use crate::authz; +use crate::authz::ApiResourceWithRolesType; use crate::authz::AuthorizedResource; use crate::context::OpContext; +use crate::db; +use crate::db::fixed_data::FLEET_ID; use authz::ApiResource; use futures::future::BoxFuture; use futures::FutureExt; use lazy_static::lazy_static; +use nexus_db_model::DatabaseString; +use nexus_types::external_api::shared; use omicron_common::api::external::Error; use omicron_common::api::external::LookupType; use std::sync::Arc; +use strum::IntoEnumIterator; use uuid::Uuid; lazy_static! { pub static ref SILO1_ID: Uuid = make_uuid(); } +/// Manages the construction of the resource hierarchy used in the test, plus +/// associated users and role assignments +struct ResourceBuilder<'a> { + opctx: &'a OpContext, + coverage: &'a mut Coverage, + datastore: &'a db::DataStore, + // XXX-dap Arc? + resources: Vec>, + main_silo_id: Uuid, + users: Vec<(String, Uuid)>, +} + +// XXX-dap TODO-doc +impl<'a> ResourceBuilder<'a> { + fn new( + opctx: &'a OpContext, + datastore: &'a db::DataStore, + coverage: &'a mut Coverage, + main_silo_id: Uuid, + ) -> ResourceBuilder<'a> { + ResourceBuilder { + opctx, + coverage, + datastore, + resources: Vec::new(), + main_silo_id, + users: Vec::new(), + } + } + + fn new_resource(&mut self, resource: T) { + self.coverage.covered(&resource); + self.resources.push(Arc::new(resource)); + } + + async fn new_resource_with_roles(&mut self, resource: T) + where + T: Authorizable + ApiResourceWithRolesType + AuthorizedResource + Clone, + T::AllowedRoles: IntoEnumIterator, + { + self.new_resource(resource.clone()); + let resource_name = match resource.lookup_type() { + LookupType::ByName(name) => name, + LookupType::ById(id) if *id == *FLEET_ID => "fleet", + LookupType::ById(_) + | LookupType::BySessionToken(_) + | LookupType::ByCompositeId(_) => { + panic!("test resources must be given names"); + } + }; + let silo_id = self.main_silo_id; + + let opctx = self.opctx; + let datastore = self.datastore; + for role in T::AllowedRoles::iter() { + let role_name = role.to_database_string(); + let username = format!("{}-{}", resource_name, role_name); + let user_id = make_uuid(); + println!("creating user: {}", &username); + self.users.push((username.clone(), user_id)); + + let silo_user = + db::model::SiloUser::new(silo_id, user_id, username); + datastore + .silo_user_create(silo_user) + .await + .expect("failed to create silo user"); + + let old_role_assignments = datastore + .role_assignment_fetch_visible(opctx, &resource) + .await + .expect("fetching policy"); + let new_role_assignments = old_role_assignments + .into_iter() + .map(|r| r.try_into().unwrap()) + .chain(std::iter::once(shared::RoleAssignment { + identity_type: shared::IdentityType::SiloUser, + identity_id: user_id, + role_name: role, + })) + .collect::>(); + datastore + .role_assignment_replace_visible( + opctx, + &resource, + &new_role_assignments, + ) + .await + .expect("failed to assign role"); + } + } + + fn build(self) -> Resources { + Resources { resources: self.resources, users: self.users } + } +} + /// Describes the hierarchy of resources used in our RBAC test // The hierarchy looks like this: // fleet @@ -40,52 +143,126 @@ lazy_static! { // fleet/s2/o1/p1 // fleet/s2/o1/p1/vpc1 pub struct Resources { - // XXX-dap - //all_resources: Vec>, - //role_targets: Vec>, - pub silo1: authz::Silo, - pub silo1_org1: authz::Organization, - pub silo1_org1_proj1: authz::Project, - pub silo1_org1_proj1_children: Vec>, - pub silo1_org1_proj2: authz::Project, - pub silo1_org1_proj2_children: Vec>, - pub silo1_org2: authz::Organization, - pub silo1_org2_proj1: authz::Project, - pub silo1_org2_proj1_children: Vec>, - pub silo2: authz::Silo, - pub silo2_org1: authz::Organization, - pub silo2_org1_proj1: authz::Project, - pub silo2_org1_proj1_children: Vec>, + resources: Vec>, + users: Vec<(String, Uuid)>, } impl Resources { - pub fn all_resources( + pub async fn new<'a>( + opctx: &'a OpContext, + datastore: &'a db::DataStore, + coverage: &'a mut Coverage, + main_silo_id: Uuid, + ) -> Resources { + let mut builder = + ResourceBuilder::new(opctx, datastore, coverage, main_silo_id); + // XXX-dap consider moving this back into mod.rs + Self::init(&mut builder).await; + builder.build() + } + + async fn init<'a>(builder: &mut ResourceBuilder<'a>) { + builder.new_resource(authz::DATABASE.clone()); + builder.new_resource_with_roles(authz::FLEET.clone()).await; + + let silo1 = authz::Silo::new( + authz::FLEET, + *SILO1_ID, + LookupType::ByName(String::from("silo1")), + ); + builder.new_resource_with_roles(silo1.clone()).await; + + let silo1_org1 = authz::Organization::new( + silo1.clone(), + make_uuid(), + LookupType::ByName(String::from("silo1-org1")), + ); + builder.new_resource_with_roles(silo1_org1.clone()).await; + + Self::make_project(builder, &silo1_org1, "silo1-org1-proj1", true) + .await; + Self::make_project(builder, &silo1_org1, "silo1-org1-proj2", false) + .await; + + let silo1_org2 = authz::Organization::new( + silo1.clone(), + make_uuid(), + LookupType::ByName(String::from("silo1-org2")), + ); + builder.new_resource(silo1_org2.clone()); + Self::make_project(builder, &silo1_org2, "silo1-org2-proj1", false) + .await; + + let silo2 = authz::Silo::new( + authz::FLEET, + make_uuid(), + LookupType::ByName(String::from("silo2")), + ); + builder.new_resource(silo2.clone()); + let silo2_org1 = authz::Organization::new( + silo2.clone(), + make_uuid(), + LookupType::ByName(String::from("silo2-org1")), + ); + builder.new_resource(silo2_org1.clone()); + Self::make_project(builder, &silo2_org1, "silo2-org1-proj1", false) + .await; + } + + async fn make_project( + builder: &mut ResourceBuilder<'_>, + organization: &authz::Organization, + project_name: &str, + with_roles: bool, + ) { + let project = authz::Project::new( + organization.clone(), + make_uuid(), + LookupType::ByName(project_name.to_string()), + ); + if with_roles { + builder.new_resource_with_roles(project.clone()).await; + } else { + builder.new_resource(project.clone()); + } + + let vpc1_name = format!("{}-vpc1", project_name); + let vpc1 = authz::Vpc::new( + project.clone(), + make_uuid(), + LookupType::ByName(vpc1_name.clone()), + ); + + // XXX-dap TODO-coverage add more different kinds of children + builder.new_resource(authz::Disk::new( + project.clone(), + make_uuid(), + LookupType::ByName(format!("{}-disk1", project_name)), + )); + builder.new_resource(authz::Instance::new( + project.clone(), + make_uuid(), + LookupType::ByName(format!("{}-instance1", project_name)), + )); + builder.new_resource(vpc1.clone()); + // Test a resource nested two levels below Project + builder.new_resource(authz::VpcSubnet::new( + vpc1, + make_uuid(), + LookupType::ByName(format!("{}-subnet1", vpc1_name)), + )); + } + + pub fn resources( &self, ) -> impl std::iter::Iterator> + '_ { - vec![ - Arc::new(authz::DATABASE.clone()) as Arc, - Arc::new(authz::FLEET.clone()) as Arc, - Arc::new(self.silo1.clone()) as Arc, - Arc::new(self.silo1_org1.clone()) as Arc, - Arc::new(self.silo1_org1_proj1.clone()) as Arc, - ] - .into_iter() - .chain(self.silo1_org1_proj1_children.iter().cloned()) - .chain(std::iter::once( - Arc::new(self.silo1_org1_proj2.clone()) as Arc - )) - .chain(self.silo1_org1_proj2_children.iter().cloned()) - .chain(vec![ - Arc::new(self.silo1_org2.clone()) as Arc, - Arc::new(self.silo1_org2_proj1.clone()) as Arc, - ]) - .chain(self.silo1_org2_proj1_children.iter().cloned()) - .chain(vec![ - Arc::new(self.silo2.clone()) as Arc, - Arc::new(self.silo2_org1.clone()) as Arc, - Arc::new(self.silo2_org1_proj1.clone()) as Arc, - ]) - .chain(self.silo2_org1_proj1_children.iter().cloned()) + self.resources.iter().cloned() + } + + pub fn users( + &self, + ) -> impl std::iter::Iterator + '_ { + self.users.iter() } } @@ -145,114 +322,3 @@ impl Authorizable for authz::oso_generic::Database { opctx.authorize(action, self).boxed() } } - -pub fn make_resources(coverage: &mut Coverage) -> Resources { - // "Database" and "Fleet" are implicitly included by virtue of being - // returned by the iterator. - coverage.covered(&authz::DATABASE); - coverage.covered(&authz::FLEET); - - let silo1_id = make_uuid(); - let silo1 = authz::Silo::new( - authz::FLEET, - silo1_id, - LookupType::ByName(String::from("silo1")), - ); - coverage.covered(&silo1); - - let silo1_org1 = authz::Organization::new( - silo1.clone(), - make_uuid(), - LookupType::ByName(String::from("silo1-org1")), - ); - coverage.covered(&silo1_org1); - let (silo1_org1_proj1, silo1_org1_proj1_children) = - make_project(coverage, &silo1_org1, "silo1-org1-proj1"); - let (silo1_org1_proj2, silo1_org1_proj2_children) = - make_project(coverage, &silo1_org1, "silo1-org1-proj2"); - - let silo1_org2 = authz::Organization::new( - silo1.clone(), - make_uuid(), - LookupType::ByName(String::from("silo1-org2")), - ); - let (silo1_org2_proj1, silo1_org2_proj1_children) = - make_project(coverage, &silo1_org2, "silo1-org2-proj1"); - - let silo2_id = make_uuid(); - let silo2 = authz::Silo::new( - authz::FLEET, - silo2_id, - LookupType::ByName(String::from("silo2")), - ); - - let silo2_org1 = authz::Organization::new( - silo2.clone(), - make_uuid(), - LookupType::ByName(String::from("silo2-org1")), - ); - let (silo2_org1_proj1, silo2_org1_proj1_children) = - make_project(coverage, &silo2_org1, "silo2-org1-proj1"); - - Resources { - silo1, - silo1_org1, - silo1_org1_proj1, - silo1_org1_proj1_children, - silo1_org1_proj2, - silo1_org1_proj2_children, - silo1_org2, - silo1_org2_proj1, - silo1_org2_proj1_children, - silo2, - silo2_org1, - silo2_org1_proj1, - silo2_org1_proj1_children, - } -} - -fn make_project( - coverage: &mut Coverage, - organization: &authz::Organization, - project_name: &str, -) -> (authz::Project, Vec>) { - let project = authz::Project::new( - organization.clone(), - make_uuid(), - LookupType::ByName(project_name.to_string()), - ); - coverage.covered(&project); - - let vpc1_name = format!("{}-vpc1", project_name); - let vpc1 = authz::Vpc::new( - project.clone(), - make_uuid(), - LookupType::ByName(vpc1_name.clone()), - ); - coverage.covered(&vpc1); - let children: Vec> = vec![ - // XXX-dap TODO-coverage add more different kinds of children - Arc::new(authz::Disk::new( - project.clone(), - make_uuid(), - LookupType::ByName(format!("{}-disk1", project_name)), - )), - Arc::new(authz::Instance::new( - project.clone(), - make_uuid(), - LookupType::ByName(format!("{}-instance1", project_name)), - )), - Arc::new(vpc1.clone()), - // Test a resource nested two levels below Project - Arc::new(authz::VpcSubnet::new( - vpc1, - make_uuid(), - LookupType::ByName(format!("{}-subnet1", vpc1_name)), - )), - ]; - for c in &children { - coverage.covered_class(c.polar_class()); - } - - (project, children) -} From de3b399236e0dd1d4a224659a9d8e2d965ebf4d3 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 10 Aug 2022 16:56:42 -0700 Subject: [PATCH 14/32] move specific resources out of generic builder stuff --- nexus/src/authz/policy_test/mod.rs | 112 ++++++++++++++++++- nexus/src/authz/policy_test/resources.rs | 131 +---------------------- 2 files changed, 115 insertions(+), 128 deletions(-) diff --git a/nexus/src/authz/policy_test/mod.rs b/nexus/src/authz/policy_test/mod.rs index 56fe65ae04..feee2d3826 100644 --- a/nexus/src/authz/policy_test/mod.rs +++ b/nexus/src/authz/policy_test/mod.rs @@ -20,6 +20,7 @@ //! - figure out what other types to add use self::resources::Authorizable; +use self::resources::ResourceBuilder; use crate::authn; use crate::authz; use crate::context::OpContext; @@ -27,6 +28,7 @@ use crate::db; use futures::StreamExt; use nexus_test_utils::db::test_setup_database; use omicron_common::api::external::Error; +use omicron_common::api::external::LookupType; use omicron_test_utils::dev; use std::io::Cursor; use std::io::Write; @@ -47,8 +49,9 @@ async fn test_iam_roles_behavior() { let mut coverage = Coverage::new(&logctx.log); let silo1_id = *resources::SILO1_ID; - let test_resources = - Resources::new(&opctx, &datastore, &mut coverage, silo1_id).await; + let builder = + ResourceBuilder::new(&opctx, &datastore, &mut coverage, silo1_id); + let test_resources = make_resources(builder).await; coverage.verify(); // Create an OpContext for each user for testing. @@ -114,6 +117,111 @@ async fn test_iam_roles_behavior() { logctx.cleanup_successful(); } +// The hierarchy looks like this: +// fleet +// fleet/s1 +// fleet/s1/o1 +// fleet/s1/o1/p1 +// fleet/s1/o1/p1/vpc1 +// fleet/s1/o1/p2 +// fleet/s1/o1/p2/vpc1 +// fleet/s1/o2 +// fleet/s1/o2/p1 +// fleet/s1/o2/p1/vpc1 +// fleet/s2 +// fleet/s2/o1 +// fleet/s2/o1/p1 +// fleet/s2/o1/p1/vpc1 +async fn make_resources<'a>(mut builder: ResourceBuilder<'a>) -> Resources { + builder.new_resource(authz::DATABASE.clone()); + builder.new_resource_with_roles(authz::FLEET.clone()).await; + + let silo1 = authz::Silo::new( + authz::FLEET, + *resources::SILO1_ID, + LookupType::ByName(String::from("silo1")), + ); + builder.new_resource_with_roles(silo1.clone()).await; + + let silo1_org1 = authz::Organization::new( + silo1.clone(), + make_uuid(), + LookupType::ByName(String::from("silo1-org1")), + ); + builder.new_resource_with_roles(silo1_org1.clone()).await; + + make_project(&mut builder, &silo1_org1, "silo1-org1-proj1", true).await; + make_project(&mut builder, &silo1_org1, "silo1-org1-proj2", false).await; + + let silo1_org2 = authz::Organization::new( + silo1.clone(), + make_uuid(), + LookupType::ByName(String::from("silo1-org2")), + ); + builder.new_resource(silo1_org2.clone()); + make_project(&mut builder, &silo1_org2, "silo1-org2-proj1", false).await; + + let silo2 = authz::Silo::new( + authz::FLEET, + make_uuid(), + LookupType::ByName(String::from("silo2")), + ); + builder.new_resource(silo2.clone()); + let silo2_org1 = authz::Organization::new( + silo2.clone(), + make_uuid(), + LookupType::ByName(String::from("silo2-org1")), + ); + builder.new_resource(silo2_org1.clone()); + make_project(&mut builder, &silo2_org1, "silo2-org1-proj1", false).await; + + builder.build() +} + +async fn make_project( + builder: &mut ResourceBuilder<'_>, + organization: &authz::Organization, + project_name: &str, + with_roles: bool, +) { + let project = authz::Project::new( + organization.clone(), + make_uuid(), + LookupType::ByName(project_name.to_string()), + ); + if with_roles { + builder.new_resource_with_roles(project.clone()).await; + } else { + builder.new_resource(project.clone()); + } + + let vpc1_name = format!("{}-vpc1", project_name); + let vpc1 = authz::Vpc::new( + project.clone(), + make_uuid(), + LookupType::ByName(vpc1_name.clone()), + ); + + // XXX-dap TODO-coverage add more different kinds of children + builder.new_resource(authz::Disk::new( + project.clone(), + make_uuid(), + LookupType::ByName(format!("{}-disk1", project_name)), + )); + builder.new_resource(authz::Instance::new( + project.clone(), + make_uuid(), + LookupType::ByName(format!("{}-instance1", project_name)), + )); + builder.new_resource(vpc1.clone()); + // Test a resource nested two levels below Project + builder.new_resource(authz::VpcSubnet::new( + vpc1, + make_uuid(), + LookupType::ByName(format!("{}-subnet1", vpc1_name)), + )); +} + async fn run_test_operations( mut out: W, log: &slog::Logger, diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index b738f48810..a0d1d84f25 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -31,11 +31,10 @@ lazy_static! { /// Manages the construction of the resource hierarchy used in the test, plus /// associated users and role assignments -struct ResourceBuilder<'a> { +pub struct ResourceBuilder<'a> { opctx: &'a OpContext, coverage: &'a mut Coverage, datastore: &'a db::DataStore, - // XXX-dap Arc? resources: Vec>, main_silo_id: Uuid, users: Vec<(String, Uuid)>, @@ -43,7 +42,7 @@ struct ResourceBuilder<'a> { // XXX-dap TODO-doc impl<'a> ResourceBuilder<'a> { - fn new( + pub fn new( opctx: &'a OpContext, datastore: &'a db::DataStore, coverage: &'a mut Coverage, @@ -59,12 +58,12 @@ impl<'a> ResourceBuilder<'a> { } } - fn new_resource(&mut self, resource: T) { + pub fn new_resource(&mut self, resource: T) { self.coverage.covered(&resource); self.resources.push(Arc::new(resource)); } - async fn new_resource_with_roles(&mut self, resource: T) + pub async fn new_resource_with_roles(&mut self, resource: T) where T: Authorizable + ApiResourceWithRolesType + AuthorizedResource + Clone, T::AllowedRoles: IntoEnumIterator, @@ -121,138 +120,18 @@ impl<'a> ResourceBuilder<'a> { } } - fn build(self) -> Resources { + pub fn build(self) -> Resources { Resources { resources: self.resources, users: self.users } } } /// Describes the hierarchy of resources used in our RBAC test -// The hierarchy looks like this: -// fleet -// fleet/s1 -// fleet/s1/o1 -// fleet/s1/o1/p1 -// fleet/s1/o1/p1/vpc1 -// fleet/s1/o1/p2 -// fleet/s1/o1/p2/vpc1 -// fleet/s1/o2 -// fleet/s1/o2/p1 -// fleet/s1/o2/p1/vpc1 -// fleet/s2 -// fleet/s2/o1 -// fleet/s2/o1/p1 -// fleet/s2/o1/p1/vpc1 pub struct Resources { resources: Vec>, users: Vec<(String, Uuid)>, } impl Resources { - pub async fn new<'a>( - opctx: &'a OpContext, - datastore: &'a db::DataStore, - coverage: &'a mut Coverage, - main_silo_id: Uuid, - ) -> Resources { - let mut builder = - ResourceBuilder::new(opctx, datastore, coverage, main_silo_id); - // XXX-dap consider moving this back into mod.rs - Self::init(&mut builder).await; - builder.build() - } - - async fn init<'a>(builder: &mut ResourceBuilder<'a>) { - builder.new_resource(authz::DATABASE.clone()); - builder.new_resource_with_roles(authz::FLEET.clone()).await; - - let silo1 = authz::Silo::new( - authz::FLEET, - *SILO1_ID, - LookupType::ByName(String::from("silo1")), - ); - builder.new_resource_with_roles(silo1.clone()).await; - - let silo1_org1 = authz::Organization::new( - silo1.clone(), - make_uuid(), - LookupType::ByName(String::from("silo1-org1")), - ); - builder.new_resource_with_roles(silo1_org1.clone()).await; - - Self::make_project(builder, &silo1_org1, "silo1-org1-proj1", true) - .await; - Self::make_project(builder, &silo1_org1, "silo1-org1-proj2", false) - .await; - - let silo1_org2 = authz::Organization::new( - silo1.clone(), - make_uuid(), - LookupType::ByName(String::from("silo1-org2")), - ); - builder.new_resource(silo1_org2.clone()); - Self::make_project(builder, &silo1_org2, "silo1-org2-proj1", false) - .await; - - let silo2 = authz::Silo::new( - authz::FLEET, - make_uuid(), - LookupType::ByName(String::from("silo2")), - ); - builder.new_resource(silo2.clone()); - let silo2_org1 = authz::Organization::new( - silo2.clone(), - make_uuid(), - LookupType::ByName(String::from("silo2-org1")), - ); - builder.new_resource(silo2_org1.clone()); - Self::make_project(builder, &silo2_org1, "silo2-org1-proj1", false) - .await; - } - - async fn make_project( - builder: &mut ResourceBuilder<'_>, - organization: &authz::Organization, - project_name: &str, - with_roles: bool, - ) { - let project = authz::Project::new( - organization.clone(), - make_uuid(), - LookupType::ByName(project_name.to_string()), - ); - if with_roles { - builder.new_resource_with_roles(project.clone()).await; - } else { - builder.new_resource(project.clone()); - } - - let vpc1_name = format!("{}-vpc1", project_name); - let vpc1 = authz::Vpc::new( - project.clone(), - make_uuid(), - LookupType::ByName(vpc1_name.clone()), - ); - - // XXX-dap TODO-coverage add more different kinds of children - builder.new_resource(authz::Disk::new( - project.clone(), - make_uuid(), - LookupType::ByName(format!("{}-disk1", project_name)), - )); - builder.new_resource(authz::Instance::new( - project.clone(), - make_uuid(), - LookupType::ByName(format!("{}-instance1", project_name)), - )); - builder.new_resource(vpc1.clone()); - // Test a resource nested two levels below Project - builder.new_resource(authz::VpcSubnet::new( - vpc1, - make_uuid(), - LookupType::ByName(format!("{}-subnet1", vpc1_name)), - )); - } - pub fn resources( &self, ) -> impl std::iter::Iterator> + '_ { From d56269afd46a99a884a76a57aa42d3e7a1ac9939 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 09:20:53 -0700 Subject: [PATCH 15/32] edits, docs --- nexus/src/authz/policy_test/mod.rs | 176 +++++++++++++++-------- nexus/src/authz/policy_test/resources.rs | 8 +- 2 files changed, 114 insertions(+), 70 deletions(-) diff --git a/nexus/src/authz/policy_test/mod.rs b/nexus/src/authz/policy_test/mod.rs index feee2d3826..699b92c76d 100644 --- a/nexus/src/authz/policy_test/mod.rs +++ b/nexus/src/authz/policy_test/mod.rs @@ -4,57 +4,77 @@ //! Unit tests for the Oso policy //! -//! This differs from the end-to-end integration tests for authz. The tests -//! here verify: -//! -//! - for resources covered by RBAC: that the roles in the policy grant the -//! permissions that we expect that they do -//! -//! - for other policies: that the policy reflects the privileges that we expect -//! (e.g., ordinary users don't have internal roles) +//! These differ from the end-to-end integration tests for authz. The tests +//! here only exercise the code path for [`OpContext::authorize()`] and below +//! (including the Oso policy file). They do not verify HTTP endpoint behavior. +//! The integration tests verify HTTP endpoint behavior but are not nearly so +//! exhaustive in testing the policy itself. //! //! XXX-dap TODO: -//! - review above comment -//! - review remaining XXX-dap +//! - add test for other policies: that the policy reflects the privileges that +//! we expect (e.g., ordinary users don't have internal roles) //! - clean up, document test //! - figure out what other types to add +//! - review remaining XXX-dap + +mod coverage; +mod resources; -use self::resources::Authorizable; -use self::resources::ResourceBuilder; use crate::authn; use crate::authz; use crate::context::OpContext; use crate::db; +use coverage::Coverage; use futures::StreamExt; use nexus_test_utils::db::test_setup_database; use omicron_common::api::external::Error; use omicron_common::api::external::LookupType; use omicron_test_utils::dev; +use resources::Authorizable; +use resources::ResourceBuilder; +use resources::Resources; use std::io::Cursor; use std::io::Write; use std::sync::Arc; use strum::IntoEnumIterator; use uuid::Uuid; -mod coverage; -mod resources; -use coverage::Coverage; -use resources::Resources; - +/// Verifies that all roles grant precisely the privileges that we expect them +/// to +/// +/// This test constructs a hierarchy of resources, from the Fleet all the way +/// down to things like Instances and Disks. For every resource that supports +/// roles (Fleet, Silo, Organization, and Project), for every supported role, we +/// create one user that has that role on one of the resources that supports it +/// (i.e., one "fleet-admin", one "fleet-viewer", one "silo-admin" for one Silo, +/// etc.). Then we exhaustively test `authorize()` for all of these users +/// attempting every possible action on every resource we created. This tests +/// not only whether "silo1-admin" has all privileges on "silo1", but also that +/// they have no privileges on "silo2" or "fleet". +/// +/// When we say we create resources in this test, we just create the `authz` +/// objects needed to do an authz check. We're not going through the API and we +/// don't do anything with Nexus or the database except for the creation of +/// users and role assignments. #[tokio::test(flavor = "multi_thread")] async fn test_iam_roles_behavior() { let logctx = dev::test_setup_log("test_iam_roles"); let mut db = test_setup_database(&logctx.log).await; let (opctx, datastore) = db::datastore::datastore_test(&logctx, &db).await; + // Assemble the list of resources that we'll use for testing. As we create + // these resources, create the users and role assignments needed for the + // exhaustive test. `Coverage` is used to help verify that all resources + // are tested or explicitly opted out. let mut coverage = Coverage::new(&logctx.log); - let silo1_id = *resources::SILO1_ID; + let main_silo_id = Uuid::new_v4(); let builder = - ResourceBuilder::new(&opctx, &datastore, &mut coverage, silo1_id); - let test_resources = make_resources(builder).await; + ResourceBuilder::new(&opctx, &datastore, &mut coverage, main_silo_id); + let test_resources = make_resources(builder, main_silo_id).await; coverage.verify(); - // Create an OpContext for each user for testing. + // For each user that was created, create an OpContext that we'll use to + // authorize various actions as that user. let authz = Arc::new(authz::Authz::new(&logctx.log)); let mut user_contexts: Vec> = test_resources .users() @@ -67,7 +87,7 @@ async fn test_iam_roles_behavior() { let opctx = OpContext::for_background( user_log, Arc::clone(&authz), - authn::Context::for_test_user(user_id, silo1_id), + authn::Context::for_test_user(user_id, main_silo_id), Arc::clone(&datastore), ); @@ -77,8 +97,8 @@ async fn test_iam_roles_behavior() { // Create and test an unauthenticated OpContext as well. // - // We could test the "test-privileged" and "test-unprivileged" users, but it - // wouldn't be very interesting: they're in a different Silo than the + // We could also test the "test-privileged" and "test-unprivileged" users, + // but it wouldn't be very interesting: they're in a different Silo than the // resources that we're checking against so even "test-privileged" won't // have privileges here. Anyway, they're composed of ordinary role // assignments so they're just a special case of what we're already testing. @@ -95,10 +115,19 @@ async fn test_iam_roles_behavior() { ), ))); + // Create an output stream that writes to stdout as well as an in-memory + // buffer. The test run will write a textual summary to the stream. Then + // we'll use use expectorate to verify it. We do this rather than assert + // the conditions we expect for a few reasons: first, it's handy to have a + // printed summary of this information anyway. Second, when there's a + // change in behavior, it's a lot easier to review a diff of the output + // table than to debug individual panics from deep in this test, especially + // in the common case where there are many results that changed, not just + // one. let mut buffer = Vec::new(); { let mut out = StdoutTee::new(&mut buffer); - run_test_operations( + authorize_everything( &mut out, &logctx.log, &user_contexts, @@ -117,35 +146,51 @@ async fn test_iam_roles_behavior() { logctx.cleanup_successful(); } -// The hierarchy looks like this: -// fleet -// fleet/s1 -// fleet/s1/o1 -// fleet/s1/o1/p1 -// fleet/s1/o1/p1/vpc1 -// fleet/s1/o1/p2 -// fleet/s1/o1/p2/vpc1 -// fleet/s1/o2 -// fleet/s1/o2/p1 -// fleet/s1/o2/p1/vpc1 -// fleet/s2 -// fleet/s2/o1 -// fleet/s2/o1/p1 -// fleet/s2/o1/p1/vpc1 -async fn make_resources<'a>(mut builder: ResourceBuilder<'a>) -> Resources { +/// Assemble the set of resources that we'll test +// The main hierarchy looks like this: +// +// fleet +// fleet/s1 +// fleet/s1/o1 +// fleet/s1/o1/p1 +// fleet/s1/o1/p1/vpc1 +// fleet/s1/o1/p2 +// fleet/s1/o1/p2/vpc1 +// fleet/s1/o2 +// fleet/s1/o2/p1 +// fleet/s1/o2/p1/vpc1 +// fleet/s2 +// fleet/s2/o1 +// fleet/s2/o1/p1 +// fleet/s2/o1/p1/vpc1 +// +// For one branch of the hierarchy, for each resource that supports roles, for +// each supported role, we will create one user with that role on that resource. +// Concretely, we'll create users like fleet-admin, silo1-admin, +// silo1-org1-viewer, silo1-org1-proj1-viewer, etc. This is enough to check +// what privileges are granted by that role (i.e., privileges on that resource) +// as well as verify that those privileges are _not_ granted on resources in the +// other branches. We don't need to explicitly create users to test silo2 or +// silo1-org2 or silo1-org1-proj2 (for examples) because those cases are +// identical. +async fn make_resources<'a>( + mut builder: ResourceBuilder<'a>, + main_silo_id: Uuid, +) -> Resources { builder.new_resource(authz::DATABASE.clone()); builder.new_resource_with_roles(authz::FLEET.clone()).await; + // Create the "silo1" hierarchy. let silo1 = authz::Silo::new( authz::FLEET, - *resources::SILO1_ID, + main_silo_id, LookupType::ByName(String::from("silo1")), ); builder.new_resource_with_roles(silo1.clone()).await; let silo1_org1 = authz::Organization::new( silo1.clone(), - make_uuid(), + Uuid::new_v4(), LookupType::ByName(String::from("silo1-org1")), ); builder.new_resource_with_roles(silo1_org1.clone()).await; @@ -155,21 +200,22 @@ async fn make_resources<'a>(mut builder: ResourceBuilder<'a>) -> Resources { let silo1_org2 = authz::Organization::new( silo1.clone(), - make_uuid(), + Uuid::new_v4(), LookupType::ByName(String::from("silo1-org2")), ); builder.new_resource(silo1_org2.clone()); make_project(&mut builder, &silo1_org2, "silo1-org2-proj1", false).await; + // Create the "silo2" hierarchy. let silo2 = authz::Silo::new( authz::FLEET, - make_uuid(), + Uuid::new_v4(), LookupType::ByName(String::from("silo2")), ); builder.new_resource(silo2.clone()); let silo2_org1 = authz::Organization::new( silo2.clone(), - make_uuid(), + Uuid::new_v4(), LookupType::ByName(String::from("silo2-org1")), ); builder.new_resource(silo2_org1.clone()); @@ -178,6 +224,7 @@ async fn make_resources<'a>(mut builder: ResourceBuilder<'a>) -> Resources { builder.build() } +/// Helper for `make_resources()` that constructs a small Project hierarchy async fn make_project( builder: &mut ResourceBuilder<'_>, organization: &authz::Organization, @@ -186,7 +233,7 @@ async fn make_project( ) { let project = authz::Project::new( organization.clone(), - make_uuid(), + Uuid::new_v4(), LookupType::ByName(project_name.to_string()), ); if with_roles { @@ -198,42 +245,49 @@ async fn make_project( let vpc1_name = format!("{}-vpc1", project_name); let vpc1 = authz::Vpc::new( project.clone(), - make_uuid(), + Uuid::new_v4(), LookupType::ByName(vpc1_name.clone()), ); // XXX-dap TODO-coverage add more different kinds of children builder.new_resource(authz::Disk::new( project.clone(), - make_uuid(), + Uuid::new_v4(), LookupType::ByName(format!("{}-disk1", project_name)), )); builder.new_resource(authz::Instance::new( project.clone(), - make_uuid(), + Uuid::new_v4(), LookupType::ByName(format!("{}-instance1", project_name)), )); builder.new_resource(vpc1.clone()); // Test a resource nested two levels below Project builder.new_resource(authz::VpcSubnet::new( vpc1, - make_uuid(), + Uuid::new_v4(), LookupType::ByName(format!("{}-subnet1", vpc1_name)), )); } -async fn run_test_operations( +/// Now that we've set up the resource hierarchy and users with associated +/// roles, exhaustively attempt to authorize every action for every resource by +/// every user and write a human-readable summary to `out` +/// +/// The caller is responsible for checking that the output matches what's +/// expected. +async fn authorize_everything( mut out: W, log: &slog::Logger, user_contexts: &[Arc<(String, OpContext)>], test_resources: &Resources, ) -> std::io::Result<()> { + // Run the per-resource tests in parallel. Since the caller will be + // checking the overall output against some expected output, it's important + // to emit the results in a consistent order. let mut futures = futures::stream::FuturesOrdered::new(); - - // Run the per-resource tests in parallel. for resource in test_resources.resources() { let log = log.new(o!("resource" => format!("{:?}", resource))); - futures.push(test_one_resource( + futures.push(authorize_one_resource( log, user_contexts.to_owned(), Arc::clone(&resource), @@ -254,7 +308,9 @@ async fn run_test_operations( Ok(()) } -async fn test_one_resource( +/// Exhaustively attempt to authorize every action on this resource by each user +/// in `user_contexts`, returning a human-readable summary of what succeeded +async fn authorize_one_resource( log: slog::Logger, user_contexts: Vec>, resource: Arc, @@ -305,6 +361,7 @@ async fn test_one_resource( String::from_utf8(result_str).expect("unexpected non-UTF8 output") } +/// Return the column header used for each action fn action_abbreviation(action: authz::Action) -> &'static str { match action { authz::Action::Query => "Q", @@ -319,13 +376,6 @@ fn action_abbreviation(action: authz::Action) -> &'static str { } } -// XXX-dap make this deterministic -// Most of the uuids here are hardcoded rather than randomly generated for -// debuggability. -pub fn make_uuid() -> Uuid { - Uuid::new_v4() -} - /// `Write` impl that writes everything it's given to both a destination `Write` /// and stdout via `print!`. /// diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index a0d1d84f25..12fbfed6cd 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -6,7 +6,6 @@ //! policy test use super::coverage::Coverage; -use super::make_uuid; use crate::authz; use crate::authz::ApiResourceWithRolesType; use crate::authz::AuthorizedResource; @@ -16,7 +15,6 @@ use crate::db::fixed_data::FLEET_ID; use authz::ApiResource; use futures::future::BoxFuture; use futures::FutureExt; -use lazy_static::lazy_static; use nexus_db_model::DatabaseString; use nexus_types::external_api::shared; use omicron_common::api::external::Error; @@ -25,10 +23,6 @@ use std::sync::Arc; use strum::IntoEnumIterator; use uuid::Uuid; -lazy_static! { - pub static ref SILO1_ID: Uuid = make_uuid(); -} - /// Manages the construction of the resource hierarchy used in the test, plus /// associated users and role assignments pub struct ResourceBuilder<'a> { @@ -85,7 +79,7 @@ impl<'a> ResourceBuilder<'a> { for role in T::AllowedRoles::iter() { let role_name = role.to_database_string(); let username = format!("{}-{}", resource_name, role_name); - let user_id = make_uuid(); + let user_id = Uuid::new_v4(); println!("creating user: {}", &username); self.users.push((username.clone(), user_id)); From e2c48bc63140522656424280ece8b5870d327fcf Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 10:34:40 -0700 Subject: [PATCH 16/32] more docs, more cleanup --- nexus/src/authz/policy_test/coverage.rs | 40 ++++--- nexus/src/authz/policy_test/mod.rs | 126 ++++++++++++++++------- nexus/src/authz/policy_test/resources.rs | 63 +++++++++--- 3 files changed, 163 insertions(+), 66 deletions(-) diff --git a/nexus/src/authz/policy_test/coverage.rs b/nexus/src/authz/policy_test/coverage.rs index 8208f65902..aa4964212e 100644 --- a/nexus/src/authz/policy_test/coverage.rs +++ b/nexus/src/authz/policy_test/coverage.rs @@ -25,20 +25,33 @@ impl Coverage { let authz = authz::Authz::new(&log); let class_names = authz.into_class_names(); - // Class names should be added to this exemption list when their Polar - // code snippets and authz behavior is identical to another class. This - // is primarily for performance reasons because this test takes a long - // time. But with every exemption comes the risk of a security issue! + // Exemption list for this coverage test // - // PLEASE: instead of adding a class to this list, consider updating - // this test to create an instance of the class and then test it. + // There are two possible reasons for a resource to appear on this list: + // + // (1) because its behavior is identical to that of some other resource + // that we are testing (i.e., same Polar snippet and identical + // configuration for the authz type). There aren't any examples of + // this today, but it might be reasonable to do this for resources + // that are indistinguishable to the authz subsystem (e.g., Disks, + // Instances, Vpcs, and other things nested directly below Project) + // + // (2) because we have not yet gotten around to adding the type to this + // test. We don't want to expand this list if we can avoid it! let exempted = [ - // Non-resources + // Non-resources: authz::Action::get_polar_class(), authz::actor::AnyActor::get_polar_class(), authz::actor::AuthenticatedActor::get_polar_class(), - // XXX-dap TODO-coverage Not yet implemented, but not exempted for a - // good reason. + // Resources whose behavior should be identical to an existing type + // and we don't want to do the test twice for performance reasons: + // none yet. + // + // TODO-coverage Resources that we should test, but for which we + // have not yet added a test. PLEASE: instead of adding something + // to this list, modify `make_resources()` to test it instead. This + // should be pretty straightforward in most cases. Adding a new + // class to this list makes it harder to catch security flaws! authz::IpPoolList::get_polar_class(), authz::GlobalImageList::get_polar_class(), authz::ConsoleSessionList::get_polar_class(), @@ -90,9 +103,12 @@ impl Coverage { match (exempted, covered) { (true, false) => { - // XXX-dap consider checking whether the Polar snippet - // exactly matches that of another class? - debug!(&self.log, "exempt"; "class_name" => class_name); + // TODO-coverage It would be nice if we could verify that + // the Polar snippet and authz_resource! configuration were + // identical to that of an existing class. Then it would be + // safer to exclude types that are truly duplicative of + // some other type. + warn!(&self.log, "exempt"; "class_name" => class_name); } (false, true) => { debug!(&self.log, "covered"; "class_name" => class_name); diff --git a/nexus/src/authz/policy_test/mod.rs b/nexus/src/authz/policy_test/mod.rs index 699b92c76d..e2e3523d09 100644 --- a/nexus/src/authz/policy_test/mod.rs +++ b/nexus/src/authz/policy_test/mod.rs @@ -30,7 +30,7 @@ use nexus_test_utils::db::test_setup_database; use omicron_common::api::external::Error; use omicron_common::api::external::LookupType; use omicron_test_utils::dev; -use resources::Authorizable; +use resources::DynAuthorizedResource; use resources::ResourceBuilder; use resources::Resources; use std::io::Cursor; @@ -173,55 +173,101 @@ async fn test_iam_roles_behavior() { // other branches. We don't need to explicitly create users to test silo2 or // silo1-org2 or silo1-org1-proj2 (for examples) because those cases are // identical. +// +// IF YOU WANT TO ADD A NEW RESOURCE TO THIS TEST: the goal is to have this test +// show exactly what roles grant what permissions on your resource. Generally, +// that means you'll need to create more than one instance of the resource, with +// different levels of access by different users. This is probably easier than +// it sounds! +// +// - If your resource is NOT a collection, you only need to modify the function +// that creates the parent collection to create an instance of your resource. +// That's likely `make_project()`, `make_organization()`, `make_silo()`, etc. +// If your resource is essentially a global singleton (like "Fleet"), you can +// modify `make_resources()` directly. +// +// - If your resource is a collection, then you want to create a new function +// similar to the other functions that make collections (`make_project()`, +// `make_organization()`, etc.) You'll likely need the `first_branch` +// argument that says whether to create users and how many child hierarchies +// to create. +// XXX-dap put this in a separate module +// XXX-dap abstract better? async fn make_resources<'a>( mut builder: ResourceBuilder<'a>, main_silo_id: Uuid, ) -> Resources { builder.new_resource(authz::DATABASE.clone()); - builder.new_resource_with_roles(authz::FLEET.clone()).await; + builder.new_resource_with_users(authz::FLEET.clone()).await; + + make_silo(&mut builder, "silo1", main_silo_id, true).await; + make_silo(&mut builder, "silo2", Uuid::new_v4(), false).await; - // Create the "silo1" hierarchy. + builder.build() +} + +/// Helper for `make_resources()` that constructs a small Silo hierarchy +async fn make_silo( + builder: &mut ResourceBuilder<'_>, + silo_name: &str, + silo_id: Uuid, + first_branch: bool, +) { let silo1 = authz::Silo::new( authz::FLEET, - main_silo_id, - LookupType::ByName(String::from("silo1")), - ); - builder.new_resource_with_roles(silo1.clone()).await; - - let silo1_org1 = authz::Organization::new( - silo1.clone(), - Uuid::new_v4(), - LookupType::ByName(String::from("silo1-org1")), + silo_id, + LookupType::ByName(silo_name.to_string()), ); - builder.new_resource_with_roles(silo1_org1.clone()).await; - - make_project(&mut builder, &silo1_org1, "silo1-org1-proj1", true).await; - make_project(&mut builder, &silo1_org1, "silo1-org1-proj2", false).await; + if first_branch { + builder.new_resource_with_users(silo1.clone()).await; + } else { + builder.new_resource(silo1.clone()); + } - let silo1_org2 = authz::Organization::new( - silo1.clone(), - Uuid::new_v4(), - LookupType::ByName(String::from("silo1-org2")), - ); - builder.new_resource(silo1_org2.clone()); - make_project(&mut builder, &silo1_org2, "silo1-org2-proj1", false).await; + let norganizations = if first_branch { 2 } else { 1 }; + for i in 0..norganizations { + let organization_name = format!("{}-org{}", silo_name, i + 1); + let org_first_branch = first_branch && i == 0; + make_organization( + builder, + &silo1, + &organization_name, + org_first_branch, + ) + .await; + } +} - // Create the "silo2" hierarchy. - let silo2 = authz::Silo::new( - authz::FLEET, - Uuid::new_v4(), - LookupType::ByName(String::from("silo2")), - ); - builder.new_resource(silo2.clone()); - let silo2_org1 = authz::Organization::new( - silo2.clone(), +/// Helper for `make_resources()` that constructs a small Organization hierarchy +async fn make_organization( + builder: &mut ResourceBuilder<'_>, + silo: &authz::Silo, + organization_name: &str, + first_branch: bool, +) { + let organization = authz::Organization::new( + silo.clone(), Uuid::new_v4(), - LookupType::ByName(String::from("silo2-org1")), + LookupType::ByName(organization_name.to_string()), ); - builder.new_resource(silo2_org1.clone()); - make_project(&mut builder, &silo2_org1, "silo2-org1-proj1", false).await; + if first_branch { + builder.new_resource_with_users(organization.clone()).await; + } else { + builder.new_resource(organization.clone()); + } - builder.build() + let nprojects = if first_branch { 2 } else { 1 }; + for i in 0..nprojects { + let project_name = format!("{}-proj{}", organization_name, i + 1); + let create_project_users = first_branch && i == 0; + make_project( + builder, + &organization, + &project_name, + create_project_users, + ) + .await; + } } /// Helper for `make_resources()` that constructs a small Project hierarchy @@ -229,15 +275,15 @@ async fn make_project( builder: &mut ResourceBuilder<'_>, organization: &authz::Organization, project_name: &str, - with_roles: bool, + first_branch: bool, ) { let project = authz::Project::new( organization.clone(), Uuid::new_v4(), LookupType::ByName(project_name.to_string()), ); - if with_roles { - builder.new_resource_with_roles(project.clone()).await; + if first_branch { + builder.new_resource_with_users(project.clone()).await; } else { builder.new_resource(project.clone()); } @@ -313,7 +359,7 @@ async fn authorize_everything( async fn authorize_one_resource( log: slog::Logger, user_contexts: Vec>, - resource: Arc, + resource: Arc, ) -> String { let task = tokio::spawn(async move { let mut buffer = Vec::new(); diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index 12fbfed6cd..69f43ab418 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -26,16 +26,30 @@ use uuid::Uuid; /// Manages the construction of the resource hierarchy used in the test, plus /// associated users and role assignments pub struct ResourceBuilder<'a> { + // Inputs + /// opcontext used for creating users and role assignments opctx: &'a OpContext, - coverage: &'a mut Coverage, + /// datastore used for creating users and role assignments datastore: &'a db::DataStore, - resources: Vec>, + /// used to verify test coverage of all authz resources + coverage: &'a mut Coverage, + /// id of the "main" silo -- this is the one that users are created in main_silo_id: Uuid, + + // Outputs + /// list of resources created so far + resources: Vec>, + /// list of users created so far users: Vec<(String, Uuid)>, } -// XXX-dap TODO-doc impl<'a> ResourceBuilder<'a> { + /// Begin constructing a resource hierarchy and associated users and role + /// assignments + /// + /// The users and role assignments will be created in silo `main_silo_id` + /// using OpContext `opctx` and datastore `datastore`. `coverage` is used + /// to verify test coverage of authz resource types. pub fn new( opctx: &'a OpContext, datastore: &'a db::DataStore, @@ -52,17 +66,25 @@ impl<'a> ResourceBuilder<'a> { } } - pub fn new_resource(&mut self, resource: T) { + /// Register a new resource for later testing, with no associated users or + /// role assignments + pub fn new_resource(&mut self, resource: T) { self.coverage.covered(&resource); self.resources.push(Arc::new(resource)); } - pub async fn new_resource_with_roles(&mut self, resource: T) + /// Register a new resource for later testing and also: for each supported + /// role on this resource, create a user that has that role on this resource + pub async fn new_resource_with_users(&mut self, resource: T) where - T: Authorizable + ApiResourceWithRolesType + AuthorizedResource + Clone, + T: DynAuthorizedResource + + ApiResourceWithRolesType + + AuthorizedResource + + Clone, T::AllowedRoles: IntoEnumIterator, { self.new_resource(resource.clone()); + let resource_name = match resource.lookup_type() { LookupType::ByName(name) => name, LookupType::ById(id) if *id == *FLEET_ID => "fleet", @@ -114,24 +136,29 @@ impl<'a> ResourceBuilder<'a> { } } + /// Returns an immutable view of the resources and users created pub fn build(self) -> Resources { Resources { resources: self.resources, users: self.users } } } -/// Describes the hierarchy of resources used in our RBAC test +/// Describes the hierarchy of resources that were registered and the users that +/// were created with specific roles on those resources pub struct Resources { - resources: Vec>, + resources: Vec>, users: Vec<(String, Uuid)>, } impl Resources { + /// Iterate the resources to be tested pub fn resources( &self, - ) -> impl std::iter::Iterator> + '_ { + ) -> impl std::iter::Iterator> + '_ + { self.resources.iter().cloned() } + /// Iterate the users that were created as `(username, user_id)` pairs pub fn users( &self, ) -> impl std::iter::Iterator + '_ { @@ -139,9 +166,15 @@ impl Resources { } } -pub trait Authorizable: AuthorizedResource + std::fmt::Debug { - fn resource_name(&self) -> String; - +/// Dynamically-dispatched version of `AuthorizedResource` +/// +/// This is needed because calling [`OpContext::authorize()`] requires knowing +/// at compile time exactly which resource you're authorizing. But we want to +/// put many different resource types into a collection and do authz checks on +/// all of them. (We could also change `authorize()` to be dynamically- +/// dispatched. This would be a much more sprawling change. And it's not clear +/// that our use case has much application outside of a test like this.) +pub trait DynAuthorizedResource: AuthorizedResource + std::fmt::Debug { fn do_authorize<'a, 'b>( &'a self, opctx: &'b OpContext, @@ -149,9 +182,11 @@ pub trait Authorizable: AuthorizedResource + std::fmt::Debug { ) -> BoxFuture<'a, Result<(), Error>> where 'b: 'a; + + fn resource_name(&self) -> String; } -impl Authorizable for T +impl DynAuthorizedResource for T where T: ApiResource + AuthorizedResource + oso::PolarClass + Clone, { @@ -179,7 +214,7 @@ where } } -impl Authorizable for authz::oso_generic::Database { +impl DynAuthorizedResource for authz::oso_generic::Database { fn resource_name(&self) -> String { String::from("DATABASE") } From 2decfddb52e7b2d030b8bef7d4d646ad51655978 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 10:49:06 -0700 Subject: [PATCH 17/32] move concrete resources to a separate module --- nexus/src/authz/policy_test/mod.rs | 181 +-------- .../src/authz/policy_test/resource_builder.rs | 232 ++++++++++++ nexus/src/authz/policy_test/resources.rs | 356 ++++++++---------- 3 files changed, 389 insertions(+), 380 deletions(-) create mode 100644 nexus/src/authz/policy_test/resource_builder.rs diff --git a/nexus/src/authz/policy_test/mod.rs b/nexus/src/authz/policy_test/mod.rs index e2e3523d09..8d77cb6f4a 100644 --- a/nexus/src/authz/policy_test/mod.rs +++ b/nexus/src/authz/policy_test/mod.rs @@ -13,11 +13,11 @@ //! XXX-dap TODO: //! - add test for other policies: that the policy reflects the privileges that //! we expect (e.g., ordinary users don't have internal roles) -//! - clean up, document test //! - figure out what other types to add //! - review remaining XXX-dap mod coverage; +mod resource_builder; mod resources; use crate::authn; @@ -28,11 +28,11 @@ use coverage::Coverage; use futures::StreamExt; use nexus_test_utils::db::test_setup_database; use omicron_common::api::external::Error; -use omicron_common::api::external::LookupType; use omicron_test_utils::dev; -use resources::DynAuthorizedResource; -use resources::ResourceBuilder; -use resources::Resources; +use resource_builder::DynAuthorizedResource; +use resource_builder::ResourceBuilder; +use resource_builder::ResourceSet; +use resources::make_resources; use std::io::Cursor; use std::io::Write; use std::sync::Arc; @@ -146,175 +146,6 @@ async fn test_iam_roles_behavior() { logctx.cleanup_successful(); } -/// Assemble the set of resources that we'll test -// The main hierarchy looks like this: -// -// fleet -// fleet/s1 -// fleet/s1/o1 -// fleet/s1/o1/p1 -// fleet/s1/o1/p1/vpc1 -// fleet/s1/o1/p2 -// fleet/s1/o1/p2/vpc1 -// fleet/s1/o2 -// fleet/s1/o2/p1 -// fleet/s1/o2/p1/vpc1 -// fleet/s2 -// fleet/s2/o1 -// fleet/s2/o1/p1 -// fleet/s2/o1/p1/vpc1 -// -// For one branch of the hierarchy, for each resource that supports roles, for -// each supported role, we will create one user with that role on that resource. -// Concretely, we'll create users like fleet-admin, silo1-admin, -// silo1-org1-viewer, silo1-org1-proj1-viewer, etc. This is enough to check -// what privileges are granted by that role (i.e., privileges on that resource) -// as well as verify that those privileges are _not_ granted on resources in the -// other branches. We don't need to explicitly create users to test silo2 or -// silo1-org2 or silo1-org1-proj2 (for examples) because those cases are -// identical. -// -// IF YOU WANT TO ADD A NEW RESOURCE TO THIS TEST: the goal is to have this test -// show exactly what roles grant what permissions on your resource. Generally, -// that means you'll need to create more than one instance of the resource, with -// different levels of access by different users. This is probably easier than -// it sounds! -// -// - If your resource is NOT a collection, you only need to modify the function -// that creates the parent collection to create an instance of your resource. -// That's likely `make_project()`, `make_organization()`, `make_silo()`, etc. -// If your resource is essentially a global singleton (like "Fleet"), you can -// modify `make_resources()` directly. -// -// - If your resource is a collection, then you want to create a new function -// similar to the other functions that make collections (`make_project()`, -// `make_organization()`, etc.) You'll likely need the `first_branch` -// argument that says whether to create users and how many child hierarchies -// to create. -// XXX-dap put this in a separate module -// XXX-dap abstract better? -async fn make_resources<'a>( - mut builder: ResourceBuilder<'a>, - main_silo_id: Uuid, -) -> Resources { - builder.new_resource(authz::DATABASE.clone()); - builder.new_resource_with_users(authz::FLEET.clone()).await; - - make_silo(&mut builder, "silo1", main_silo_id, true).await; - make_silo(&mut builder, "silo2", Uuid::new_v4(), false).await; - - builder.build() -} - -/// Helper for `make_resources()` that constructs a small Silo hierarchy -async fn make_silo( - builder: &mut ResourceBuilder<'_>, - silo_name: &str, - silo_id: Uuid, - first_branch: bool, -) { - let silo1 = authz::Silo::new( - authz::FLEET, - silo_id, - LookupType::ByName(silo_name.to_string()), - ); - if first_branch { - builder.new_resource_with_users(silo1.clone()).await; - } else { - builder.new_resource(silo1.clone()); - } - - let norganizations = if first_branch { 2 } else { 1 }; - for i in 0..norganizations { - let organization_name = format!("{}-org{}", silo_name, i + 1); - let org_first_branch = first_branch && i == 0; - make_organization( - builder, - &silo1, - &organization_name, - org_first_branch, - ) - .await; - } -} - -/// Helper for `make_resources()` that constructs a small Organization hierarchy -async fn make_organization( - builder: &mut ResourceBuilder<'_>, - silo: &authz::Silo, - organization_name: &str, - first_branch: bool, -) { - let organization = authz::Organization::new( - silo.clone(), - Uuid::new_v4(), - LookupType::ByName(organization_name.to_string()), - ); - if first_branch { - builder.new_resource_with_users(organization.clone()).await; - } else { - builder.new_resource(organization.clone()); - } - - let nprojects = if first_branch { 2 } else { 1 }; - for i in 0..nprojects { - let project_name = format!("{}-proj{}", organization_name, i + 1); - let create_project_users = first_branch && i == 0; - make_project( - builder, - &organization, - &project_name, - create_project_users, - ) - .await; - } -} - -/// Helper for `make_resources()` that constructs a small Project hierarchy -async fn make_project( - builder: &mut ResourceBuilder<'_>, - organization: &authz::Organization, - project_name: &str, - first_branch: bool, -) { - let project = authz::Project::new( - organization.clone(), - Uuid::new_v4(), - LookupType::ByName(project_name.to_string()), - ); - if first_branch { - builder.new_resource_with_users(project.clone()).await; - } else { - builder.new_resource(project.clone()); - } - - let vpc1_name = format!("{}-vpc1", project_name); - let vpc1 = authz::Vpc::new( - project.clone(), - Uuid::new_v4(), - LookupType::ByName(vpc1_name.clone()), - ); - - // XXX-dap TODO-coverage add more different kinds of children - builder.new_resource(authz::Disk::new( - project.clone(), - Uuid::new_v4(), - LookupType::ByName(format!("{}-disk1", project_name)), - )); - builder.new_resource(authz::Instance::new( - project.clone(), - Uuid::new_v4(), - LookupType::ByName(format!("{}-instance1", project_name)), - )); - builder.new_resource(vpc1.clone()); - // Test a resource nested two levels below Project - builder.new_resource(authz::VpcSubnet::new( - vpc1, - Uuid::new_v4(), - LookupType::ByName(format!("{}-subnet1", vpc1_name)), - )); -} - /// Now that we've set up the resource hierarchy and users with associated /// roles, exhaustively attempt to authorize every action for every resource by /// every user and write a human-readable summary to `out` @@ -325,7 +156,7 @@ async fn authorize_everything( mut out: W, log: &slog::Logger, user_contexts: &[Arc<(String, OpContext)>], - test_resources: &Resources, + test_resources: &ResourceSet, ) -> std::io::Result<()> { // Run the per-resource tests in parallel. Since the caller will be // checking the overall output against some expected output, it's important diff --git a/nexus/src/authz/policy_test/resource_builder.rs b/nexus/src/authz/policy_test/resource_builder.rs new file mode 100644 index 0000000000..74bccbca2c --- /dev/null +++ b/nexus/src/authz/policy_test/resource_builder.rs @@ -0,0 +1,232 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Structures and functions for creating resources and associated users for the +//! IAM policy test + +use super::coverage::Coverage; +use crate::authz; +use crate::authz::ApiResourceWithRolesType; +use crate::authz::AuthorizedResource; +use crate::context::OpContext; +use crate::db; +use crate::db::fixed_data::FLEET_ID; +use authz::ApiResource; +use futures::future::BoxFuture; +use futures::FutureExt; +use nexus_db_model::DatabaseString; +use nexus_types::external_api::shared; +use omicron_common::api::external::Error; +use omicron_common::api::external::LookupType; +use std::sync::Arc; +use strum::IntoEnumIterator; +use uuid::Uuid; + +/// Manages the construction of the resource hierarchy used in the test, plus +/// associated users and role assignments +pub struct ResourceBuilder<'a> { + // Inputs + /// opcontext used for creating users and role assignments + opctx: &'a OpContext, + /// datastore used for creating users and role assignments + datastore: &'a db::DataStore, + /// used to verify test coverage of all authz resources + coverage: &'a mut Coverage, + /// id of the "main" silo -- this is the one that users are created in + main_silo_id: Uuid, + + // Outputs + /// list of resources created so far + resources: Vec>, + /// list of users created so far + users: Vec<(String, Uuid)>, +} + +impl<'a> ResourceBuilder<'a> { + /// Begin constructing a resource hierarchy and associated users and role + /// assignments + /// + /// The users and role assignments will be created in silo `main_silo_id` + /// using OpContext `opctx` and datastore `datastore`. `coverage` is used + /// to verify test coverage of authz resource types. + pub fn new( + opctx: &'a OpContext, + datastore: &'a db::DataStore, + coverage: &'a mut Coverage, + main_silo_id: Uuid, + ) -> ResourceBuilder<'a> { + ResourceBuilder { + opctx, + coverage, + datastore, + resources: Vec::new(), + main_silo_id, + users: Vec::new(), + } + } + + /// Register a new resource for later testing, with no associated users or + /// role assignments + pub fn new_resource(&mut self, resource: T) { + self.coverage.covered(&resource); + self.resources.push(Arc::new(resource)); + } + + /// Register a new resource for later testing and also: for each supported + /// role on this resource, create a user that has that role on this resource + pub async fn new_resource_with_users(&mut self, resource: T) + where + T: DynAuthorizedResource + + ApiResourceWithRolesType + + AuthorizedResource + + Clone, + T::AllowedRoles: IntoEnumIterator, + { + self.new_resource(resource.clone()); + + let resource_name = match resource.lookup_type() { + LookupType::ByName(name) => name, + LookupType::ById(id) if *id == *FLEET_ID => "fleet", + LookupType::ById(_) + | LookupType::BySessionToken(_) + | LookupType::ByCompositeId(_) => { + panic!("test resources must be given names"); + } + }; + let silo_id = self.main_silo_id; + + let opctx = self.opctx; + let datastore = self.datastore; + for role in T::AllowedRoles::iter() { + let role_name = role.to_database_string(); + let username = format!("{}-{}", resource_name, role_name); + let user_id = Uuid::new_v4(); + println!("creating user: {}", &username); + self.users.push((username.clone(), user_id)); + + let silo_user = + db::model::SiloUser::new(silo_id, user_id, username); + datastore + .silo_user_create(silo_user) + .await + .expect("failed to create silo user"); + + let old_role_assignments = datastore + .role_assignment_fetch_visible(opctx, &resource) + .await + .expect("fetching policy"); + let new_role_assignments = old_role_assignments + .into_iter() + .map(|r| r.try_into().unwrap()) + .chain(std::iter::once(shared::RoleAssignment { + identity_type: shared::IdentityType::SiloUser, + identity_id: user_id, + role_name: role, + })) + .collect::>(); + datastore + .role_assignment_replace_visible( + opctx, + &resource, + &new_role_assignments, + ) + .await + .expect("failed to assign role"); + } + } + + /// Returns an immutable view of the resources and users created + pub fn build(self) -> ResourceSet { + ResourceSet { resources: self.resources, users: self.users } + } +} + +/// Describes the hierarchy of resources that were registered and the users that +/// were created with specific roles on those resources +pub struct ResourceSet { + resources: Vec>, + users: Vec<(String, Uuid)>, +} + +impl ResourceSet { + /// Iterate the resources to be tested + pub fn resources( + &self, + ) -> impl std::iter::Iterator> + '_ + { + self.resources.iter().cloned() + } + + /// Iterate the users that were created as `(username, user_id)` pairs + pub fn users( + &self, + ) -> impl std::iter::Iterator + '_ { + self.users.iter() + } +} + +/// Dynamically-dispatched version of `AuthorizedResource` +/// +/// This is needed because calling [`OpContext::authorize()`] requires knowing +/// at compile time exactly which resource you're authorizing. But we want to +/// put many different resource types into a collection and do authz checks on +/// all of them. (We could also change `authorize()` to be dynamically- +/// dispatched. This would be a much more sprawling change. And it's not clear +/// that our use case has much application outside of a test like this.) +pub trait DynAuthorizedResource: AuthorizedResource + std::fmt::Debug { + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a; + + fn resource_name(&self) -> String; +} + +impl DynAuthorizedResource for T +where + T: ApiResource + AuthorizedResource + oso::PolarClass + Clone, +{ + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a, + { + opctx.authorize(action, self).boxed() + } + + fn resource_name(&self) -> String { + let my_ident = match self.lookup_type() { + LookupType::ByName(name) => format!("{:?}", name), + LookupType::ById(id) => format!("id {:?}", id.to_string()), + LookupType::BySessionToken(_) | LookupType::ByCompositeId(_) => { + unimplemented!() + } + }; + + format!("{:?} {}", self.resource_type(), my_ident) + } +} + +impl DynAuthorizedResource for authz::oso_generic::Database { + fn resource_name(&self) -> String { + String::from("DATABASE") + } + + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a, + { + opctx.authorize(action, self).boxed() + } +} diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index 69f43ab418..0b3bd94582 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -2,231 +2,177 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! Structures and functions related to the resources created for the IAM role -//! policy test +//! Concrete list of resources created for the IAM policy test -use super::coverage::Coverage; +use super::resource_builder::ResourceBuilder; +use super::resource_builder::ResourceSet; use crate::authz; -use crate::authz::ApiResourceWithRolesType; -use crate::authz::AuthorizedResource; -use crate::context::OpContext; -use crate::db; -use crate::db::fixed_data::FLEET_ID; -use authz::ApiResource; -use futures::future::BoxFuture; -use futures::FutureExt; -use nexus_db_model::DatabaseString; -use nexus_types::external_api::shared; -use omicron_common::api::external::Error; use omicron_common::api::external::LookupType; -use std::sync::Arc; -use strum::IntoEnumIterator; use uuid::Uuid; -/// Manages the construction of the resource hierarchy used in the test, plus -/// associated users and role assignments -pub struct ResourceBuilder<'a> { - // Inputs - /// opcontext used for creating users and role assignments - opctx: &'a OpContext, - /// datastore used for creating users and role assignments - datastore: &'a db::DataStore, - /// used to verify test coverage of all authz resources - coverage: &'a mut Coverage, - /// id of the "main" silo -- this is the one that users are created in +/// Assemble the set of resources that we'll test +// The main hierarchy looks like this: +// +// fleet +// fleet/s1 +// fleet/s1/o1 +// fleet/s1/o1/p1 +// fleet/s1/o1/p1/vpc1 +// fleet/s1/o1/p2 +// fleet/s1/o1/p2/vpc1 +// fleet/s1/o2 +// fleet/s1/o2/p1 +// fleet/s1/o2/p1/vpc1 +// fleet/s2 +// fleet/s2/o1 +// fleet/s2/o1/p1 +// fleet/s2/o1/p1/vpc1 +// +// For one branch of the hierarchy, for each resource that supports roles, for +// each supported role, we will create one user with that role on that resource. +// Concretely, we'll create users like fleet-admin, silo1-admin, +// silo1-org1-viewer, silo1-org1-proj1-viewer, etc. This is enough to check +// what privileges are granted by that role (i.e., privileges on that resource) +// as well as verify that those privileges are _not_ granted on resources in the +// other branches. We don't need to explicitly create users to test silo2 or +// silo1-org2 or silo1-org1-proj2 (for examples) because those cases are +// identical. +// +// IF YOU WANT TO ADD A NEW RESOURCE TO THIS TEST: the goal is to have this test +// show exactly what roles grant what permissions on your resource. Generally, +// that means you'll need to create more than one instance of the resource, with +// different levels of access by different users. This is probably easier than +// it sounds! +// +// - If your resource is NOT a collection, you only need to modify the function +// that creates the parent collection to create an instance of your resource. +// That's likely `make_project()`, `make_organization()`, `make_silo()`, etc. +// If your resource is essentially a global singleton (like "Fleet"), you can +// modify `make_resources()` directly. +// +// - If your resource is a collection, then you want to create a new function +// similar to the other functions that make collections (`make_project()`, +// `make_organization()`, etc.) You'll likely need the `first_branch` +// argument that says whether to create users and how many child hierarchies +// to create. +pub async fn make_resources<'a>( + mut builder: ResourceBuilder<'a>, main_silo_id: Uuid, +) -> ResourceSet { + builder.new_resource(authz::DATABASE.clone()); + builder.new_resource_with_users(authz::FLEET.clone()).await; - // Outputs - /// list of resources created so far - resources: Vec>, - /// list of users created so far - users: Vec<(String, Uuid)>, -} - -impl<'a> ResourceBuilder<'a> { - /// Begin constructing a resource hierarchy and associated users and role - /// assignments - /// - /// The users and role assignments will be created in silo `main_silo_id` - /// using OpContext `opctx` and datastore `datastore`. `coverage` is used - /// to verify test coverage of authz resource types. - pub fn new( - opctx: &'a OpContext, - datastore: &'a db::DataStore, - coverage: &'a mut Coverage, - main_silo_id: Uuid, - ) -> ResourceBuilder<'a> { - ResourceBuilder { - opctx, - coverage, - datastore, - resources: Vec::new(), - main_silo_id, - users: Vec::new(), - } - } - - /// Register a new resource for later testing, with no associated users or - /// role assignments - pub fn new_resource(&mut self, resource: T) { - self.coverage.covered(&resource); - self.resources.push(Arc::new(resource)); - } + make_silo(&mut builder, "silo1", main_silo_id, true).await; + make_silo(&mut builder, "silo2", Uuid::new_v4(), false).await; - /// Register a new resource for later testing and also: for each supported - /// role on this resource, create a user that has that role on this resource - pub async fn new_resource_with_users(&mut self, resource: T) - where - T: DynAuthorizedResource - + ApiResourceWithRolesType - + AuthorizedResource - + Clone, - T::AllowedRoles: IntoEnumIterator, - { - self.new_resource(resource.clone()); - - let resource_name = match resource.lookup_type() { - LookupType::ByName(name) => name, - LookupType::ById(id) if *id == *FLEET_ID => "fleet", - LookupType::ById(_) - | LookupType::BySessionToken(_) - | LookupType::ByCompositeId(_) => { - panic!("test resources must be given names"); - } - }; - let silo_id = self.main_silo_id; - - let opctx = self.opctx; - let datastore = self.datastore; - for role in T::AllowedRoles::iter() { - let role_name = role.to_database_string(); - let username = format!("{}-{}", resource_name, role_name); - let user_id = Uuid::new_v4(); - println!("creating user: {}", &username); - self.users.push((username.clone(), user_id)); - - let silo_user = - db::model::SiloUser::new(silo_id, user_id, username); - datastore - .silo_user_create(silo_user) - .await - .expect("failed to create silo user"); - - let old_role_assignments = datastore - .role_assignment_fetch_visible(opctx, &resource) - .await - .expect("fetching policy"); - let new_role_assignments = old_role_assignments - .into_iter() - .map(|r| r.try_into().unwrap()) - .chain(std::iter::once(shared::RoleAssignment { - identity_type: shared::IdentityType::SiloUser, - identity_id: user_id, - role_name: role, - })) - .collect::>(); - datastore - .role_assignment_replace_visible( - opctx, - &resource, - &new_role_assignments, - ) - .await - .expect("failed to assign role"); - } - } - - /// Returns an immutable view of the resources and users created - pub fn build(self) -> Resources { - Resources { resources: self.resources, users: self.users } - } + builder.build() } -/// Describes the hierarchy of resources that were registered and the users that -/// were created with specific roles on those resources -pub struct Resources { - resources: Vec>, - users: Vec<(String, Uuid)>, -} - -impl Resources { - /// Iterate the resources to be tested - pub fn resources( - &self, - ) -> impl std::iter::Iterator> + '_ - { - self.resources.iter().cloned() +/// Helper for `make_resources()` that constructs a small Silo hierarchy +async fn make_silo( + builder: &mut ResourceBuilder<'_>, + silo_name: &str, + silo_id: Uuid, + first_branch: bool, +) { + let silo1 = authz::Silo::new( + authz::FLEET, + silo_id, + LookupType::ByName(silo_name.to_string()), + ); + if first_branch { + builder.new_resource_with_users(silo1.clone()).await; + } else { + builder.new_resource(silo1.clone()); } - /// Iterate the users that were created as `(username, user_id)` pairs - pub fn users( - &self, - ) -> impl std::iter::Iterator + '_ { - self.users.iter() + let norganizations = if first_branch { 2 } else { 1 }; + for i in 0..norganizations { + let organization_name = format!("{}-org{}", silo_name, i + 1); + let org_first_branch = first_branch && i == 0; + make_organization( + builder, + &silo1, + &organization_name, + org_first_branch, + ) + .await; } } -/// Dynamically-dispatched version of `AuthorizedResource` -/// -/// This is needed because calling [`OpContext::authorize()`] requires knowing -/// at compile time exactly which resource you're authorizing. But we want to -/// put many different resource types into a collection and do authz checks on -/// all of them. (We could also change `authorize()` to be dynamically- -/// dispatched. This would be a much more sprawling change. And it's not clear -/// that our use case has much application outside of a test like this.) -pub trait DynAuthorizedResource: AuthorizedResource + std::fmt::Debug { - fn do_authorize<'a, 'b>( - &'a self, - opctx: &'b OpContext, - action: authz::Action, - ) -> BoxFuture<'a, Result<(), Error>> - where - 'b: 'a; - - fn resource_name(&self) -> String; -} - -impl DynAuthorizedResource for T -where - T: ApiResource + AuthorizedResource + oso::PolarClass + Clone, -{ - fn do_authorize<'a, 'b>( - &'a self, - opctx: &'b OpContext, - action: authz::Action, - ) -> BoxFuture<'a, Result<(), Error>> - where - 'b: 'a, - { - opctx.authorize(action, self).boxed() +/// Helper for `make_resources()` that constructs a small Organization hierarchy +async fn make_organization( + builder: &mut ResourceBuilder<'_>, + silo: &authz::Silo, + organization_name: &str, + first_branch: bool, +) { + let organization = authz::Organization::new( + silo.clone(), + Uuid::new_v4(), + LookupType::ByName(organization_name.to_string()), + ); + if first_branch { + builder.new_resource_with_users(organization.clone()).await; + } else { + builder.new_resource(organization.clone()); } - fn resource_name(&self) -> String { - let my_ident = match self.lookup_type() { - LookupType::ByName(name) => format!("{:?}", name), - LookupType::ById(id) => format!("id {:?}", id.to_string()), - LookupType::BySessionToken(_) | LookupType::ByCompositeId(_) => { - unimplemented!() - } - }; - - format!("{:?} {}", self.resource_type(), my_ident) + let nprojects = if first_branch { 2 } else { 1 }; + for i in 0..nprojects { + let project_name = format!("{}-proj{}", organization_name, i + 1); + let create_project_users = first_branch && i == 0; + make_project( + builder, + &organization, + &project_name, + create_project_users, + ) + .await; } } -impl DynAuthorizedResource for authz::oso_generic::Database { - fn resource_name(&self) -> String { - String::from("DATABASE") +/// Helper for `make_resources()` that constructs a small Project hierarchy +async fn make_project( + builder: &mut ResourceBuilder<'_>, + organization: &authz::Organization, + project_name: &str, + first_branch: bool, +) { + let project = authz::Project::new( + organization.clone(), + Uuid::new_v4(), + LookupType::ByName(project_name.to_string()), + ); + if first_branch { + builder.new_resource_with_users(project.clone()).await; + } else { + builder.new_resource(project.clone()); } - fn do_authorize<'a, 'b>( - &'a self, - opctx: &'b OpContext, - action: authz::Action, - ) -> BoxFuture<'a, Result<(), Error>> - where - 'b: 'a, - { - opctx.authorize(action, self).boxed() - } + let vpc1_name = format!("{}-vpc1", project_name); + let vpc1 = authz::Vpc::new( + project.clone(), + Uuid::new_v4(), + LookupType::ByName(vpc1_name.clone()), + ); + + // XXX-dap TODO-coverage add more different kinds of children + builder.new_resource(authz::Disk::new( + project.clone(), + Uuid::new_v4(), + LookupType::ByName(format!("{}-disk1", project_name)), + )); + builder.new_resource(authz::Instance::new( + project.clone(), + Uuid::new_v4(), + LookupType::ByName(format!("{}-instance1", project_name)), + )); + builder.new_resource(vpc1.clone()); + // Test a resource nested two levels below Project + builder.new_resource(authz::VpcSubnet::new( + vpc1, + Uuid::new_v4(), + LookupType::ByName(format!("{}-subnet1", vpc1_name)), + )); } From ab6d3be315463d7b2fd0352bb047446f2604be42 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 11:00:25 -0700 Subject: [PATCH 18/32] move exemption list --- nexus/src/authz/policy_test/coverage.rs | 71 +++--------------------- nexus/src/authz/policy_test/mod.rs | 6 +- nexus/src/authz/policy_test/resources.rs | 64 +++++++++++++++++++++ 3 files changed, 74 insertions(+), 67 deletions(-) diff --git a/nexus/src/authz/policy_test/coverage.rs b/nexus/src/authz/policy_test/coverage.rs index aa4964212e..cb9140c7f4 100644 --- a/nexus/src/authz/policy_test/coverage.rs +++ b/nexus/src/authz/policy_test/coverage.rs @@ -4,7 +4,6 @@ use crate::authz; use crate::authz::AuthorizedResource; -use oso::PolarClass; use std::collections::BTreeSet; /// Helper for identifying authz resources not covered by the IAM role policy @@ -20,78 +19,27 @@ pub struct Coverage { } impl Coverage { - pub fn new(log: &slog::Logger) -> Coverage { + pub fn new(log: &slog::Logger, exempted: BTreeSet) -> Coverage { let log = log.new(o!("component" => "IamTestCoverage")); - let authz = authz::Authz::new(&log); - let class_names = authz.into_class_names(); - - // Exemption list for this coverage test - // - // There are two possible reasons for a resource to appear on this list: - // - // (1) because its behavior is identical to that of some other resource - // that we are testing (i.e., same Polar snippet and identical - // configuration for the authz type). There aren't any examples of - // this today, but it might be reasonable to do this for resources - // that are indistinguishable to the authz subsystem (e.g., Disks, - // Instances, Vpcs, and other things nested directly below Project) - // - // (2) because we have not yet gotten around to adding the type to this - // test. We don't want to expand this list if we can avoid it! - let exempted = [ - // Non-resources: - authz::Action::get_polar_class(), - authz::actor::AnyActor::get_polar_class(), - authz::actor::AuthenticatedActor::get_polar_class(), - // Resources whose behavior should be identical to an existing type - // and we don't want to do the test twice for performance reasons: - // none yet. - // - // TODO-coverage Resources that we should test, but for which we - // have not yet added a test. PLEASE: instead of adding something - // to this list, modify `make_resources()` to test it instead. This - // should be pretty straightforward in most cases. Adding a new - // class to this list makes it harder to catch security flaws! - authz::IpPoolList::get_polar_class(), - authz::GlobalImageList::get_polar_class(), - authz::ConsoleSessionList::get_polar_class(), - authz::DeviceAuthRequestList::get_polar_class(), - authz::IpPool::get_polar_class(), - authz::NetworkInterface::get_polar_class(), - authz::VpcRouter::get_polar_class(), - authz::RouterRoute::get_polar_class(), - authz::ConsoleSession::get_polar_class(), - authz::DeviceAuthRequest::get_polar_class(), - authz::DeviceAccessToken::get_polar_class(), - authz::Rack::get_polar_class(), - authz::RoleBuiltin::get_polar_class(), - authz::SshKey::get_polar_class(), - authz::SiloUser::get_polar_class(), - authz::SiloGroup::get_polar_class(), - authz::IdentityProvider::get_polar_class(), - authz::SamlIdentityProvider::get_polar_class(), - authz::Sled::get_polar_class(), - authz::UpdateAvailableArtifact::get_polar_class(), - authz::UserBuiltin::get_polar_class(), - authz::GlobalImage::get_polar_class(), - ] - .into_iter() - .map(|c| c.name.clone()) - .collect(); - + let class_names = authz::Authz::new(&log).into_class_names(); Coverage { log, class_names, exempted, covered: BTreeSet::new() } } + /// Record that the Polar class associated with `covered` is covered by the + /// test pub fn covered(&mut self, covered: &dyn AuthorizedResource) { self.covered_class(covered.polar_class()) } + /// Record that type `class` is covered by the test pub fn covered_class(&mut self, class: oso::Class) { let class_name = class.name.clone(); debug!(&self.log, "covering"; "class_name" => &class_name); self.covered.insert(class_name); } + /// Checks coverage and panics if any non-exempt types were _not_ covered or + /// if any exempt types _were_ covered pub fn verify(&self) { let mut uncovered = Vec::new(); let mut bad_exemptions = Vec::new(); @@ -103,11 +51,6 @@ impl Coverage { match (exempted, covered) { (true, false) => { - // TODO-coverage It would be nice if we could verify that - // the Polar snippet and authz_resource! configuration were - // identical to that of an existing class. Then it would be - // safer to exclude types that are truly duplicative of - // some other type. warn!(&self.log, "exempt"; "class_name" => class_name); } (false, true) => { diff --git a/nexus/src/authz/policy_test/mod.rs b/nexus/src/authz/policy_test/mod.rs index 8d77cb6f4a..6bdcb4f694 100644 --- a/nexus/src/authz/policy_test/mod.rs +++ b/nexus/src/authz/policy_test/mod.rs @@ -32,7 +32,6 @@ use omicron_test_utils::dev; use resource_builder::DynAuthorizedResource; use resource_builder::ResourceBuilder; use resource_builder::ResourceSet; -use resources::make_resources; use std::io::Cursor; use std::io::Write; use std::sync::Arc; @@ -66,11 +65,12 @@ async fn test_iam_roles_behavior() { // these resources, create the users and role assignments needed for the // exhaustive test. `Coverage` is used to help verify that all resources // are tested or explicitly opted out. - let mut coverage = Coverage::new(&logctx.log); + let exemptions = resources::exempted_authz_classes(); + let mut coverage = Coverage::new(&logctx.log, exemptions); let main_silo_id = Uuid::new_v4(); let builder = ResourceBuilder::new(&opctx, &datastore, &mut coverage, main_silo_id); - let test_resources = make_resources(builder, main_silo_id).await; + let test_resources = resources::make_resources(builder, main_silo_id).await; coverage.verify(); // For each user that was created, create an OpContext that we'll use to diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index 0b3bd94582..0a2fa4eca1 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -8,6 +8,8 @@ use super::resource_builder::ResourceBuilder; use super::resource_builder::ResourceSet; use crate::authz; use omicron_common::api::external::LookupType; +use oso::PolarClass; +use std::collections::BTreeSet; use uuid::Uuid; /// Assemble the set of resources that we'll test @@ -176,3 +178,65 @@ async fn make_project( LookupType::ByName(format!("{}-subnet1", vpc1_name)), )); } + +/// Returns the set of authz classes exempted from the coverage test +pub fn exempted_authz_classes() -> BTreeSet { + // Exemption list for the coverage test + // + // There are two possible reasons for a resource to appear on this list: + // + // (1) because its behavior is identical to that of some other resource + // that we are testing (i.e., same Polar snippet and identical + // configuration for the authz type). There aren't any examples of + // this today, but it might be reasonable to do this for resources + // that are indistinguishable to the authz subsystem (e.g., Disks, + // Instances, Vpcs, and other things nested directly below Project) + // + // TODO-coverage It would be nice if we could verify that the Polar + // snippet and authz_resource! configuration were identical to that of + // an existing class. Then it would be safer to exclude types that are + // truly duplicative of some other type. + // + // (2) because we have not yet gotten around to adding the type to this + // test. We don't want to expand this list if we can avoid it! + [ + // Non-resources: + authz::Action::get_polar_class(), + authz::actor::AnyActor::get_polar_class(), + authz::actor::AuthenticatedActor::get_polar_class(), + // Resources whose behavior should be identical to an existing type + // and we don't want to do the test twice for performance reasons: + // none yet. + // + // TODO-coverage Resources that we should test, but for which we + // have not yet added a test. PLEASE: instead of adding something + // to this list, modify `make_resources()` to test it instead. This + // should be pretty straightforward in most cases. Adding a new + // class to this list makes it harder to catch security flaws! + authz::IpPoolList::get_polar_class(), + authz::GlobalImageList::get_polar_class(), + authz::ConsoleSessionList::get_polar_class(), + authz::DeviceAuthRequestList::get_polar_class(), + authz::IpPool::get_polar_class(), + authz::NetworkInterface::get_polar_class(), + authz::VpcRouter::get_polar_class(), + authz::RouterRoute::get_polar_class(), + authz::ConsoleSession::get_polar_class(), + authz::DeviceAuthRequest::get_polar_class(), + authz::DeviceAccessToken::get_polar_class(), + authz::Rack::get_polar_class(), + authz::RoleBuiltin::get_polar_class(), + authz::SshKey::get_polar_class(), + authz::SiloUser::get_polar_class(), + authz::SiloGroup::get_polar_class(), + authz::IdentityProvider::get_polar_class(), + authz::SamlIdentityProvider::get_polar_class(), + authz::Sled::get_polar_class(), + authz::UpdateAvailableArtifact::get_polar_class(), + authz::UserBuiltin::get_polar_class(), + authz::GlobalImage::get_polar_class(), + ] + .into_iter() + .map(|c| c.name.clone()) + .collect() +} From c20ba0580e9fb510f3c50b7ae85d3005010e9a88 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 11:23:37 -0700 Subject: [PATCH 19/32] this rule is tested --- nexus/src/authz/omicron.polar | 1 - 1 file changed, 1 deletion(-) diff --git a/nexus/src/authz/omicron.polar b/nexus/src/authz/omicron.polar index e8f04a6350..733b918ada 100644 --- a/nexus/src/authz/omicron.polar +++ b/nexus/src/authz/omicron.polar @@ -457,6 +457,5 @@ resource Database { has_permission(_actor: AuthenticatedActor, "query", _resource: Database); # The "db-init" user is the only one with the "modify" permission. -# XXX-dap does this rule change do what I expect? Should we have a test for it? has_permission(USER_DB_INIT: AuthenticatedActor, "modify", _resource: Database); has_permission(USER_DB_INIT: AuthenticatedActor, "create_child", _resource: IpPoolList); From 086bf94e51fc4962efa9cb3964bf0309e4883c7d Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 11:30:42 -0700 Subject: [PATCH 20/32] remaining XXX-dap are covered elsewhere or already done --- nexus/src/authz/policy_test/mod.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nexus/src/authz/policy_test/mod.rs b/nexus/src/authz/policy_test/mod.rs index 6bdcb4f694..e02a07c1ce 100644 --- a/nexus/src/authz/policy_test/mod.rs +++ b/nexus/src/authz/policy_test/mod.rs @@ -9,12 +9,6 @@ //! (including the Oso policy file). They do not verify HTTP endpoint behavior. //! The integration tests verify HTTP endpoint behavior but are not nearly so //! exhaustive in testing the policy itself. -//! -//! XXX-dap TODO: -//! - add test for other policies: that the policy reflects the privileges that -//! we expect (e.g., ordinary users don't have internal roles) -//! - figure out what other types to add -//! - review remaining XXX-dap mod coverage; mod resource_builder; From b580aa15cecc7b0f45b911a6fa0375abd2bb0bb2 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 11:55:25 -0700 Subject: [PATCH 21/32] add some more global resources --- .../src/authz/policy_test/resource_builder.rs | 38 ++++++---- nexus/src/authz/policy_test/resources.rs | 14 ++-- nexus/tests/output/authz-roles.out | 70 ++++++++++++++++++- 3 files changed, 99 insertions(+), 23 deletions(-) diff --git a/nexus/src/authz/policy_test/resource_builder.rs b/nexus/src/authz/policy_test/resource_builder.rs index 74bccbca2c..c33b61d306 100644 --- a/nexus/src/authz/policy_test/resource_builder.rs +++ b/nexus/src/authz/policy_test/resource_builder.rs @@ -214,19 +214,29 @@ where } } -impl DynAuthorizedResource for authz::oso_generic::Database { - fn resource_name(&self) -> String { - String::from("DATABASE") - } +macro_rules! impl_dyn_authorized_resource_for_global { + ($t:ty) => { + impl DynAuthorizedResource for $t { + fn resource_name(&self) -> String { + String::from(stringify!($t)) + } - fn do_authorize<'a, 'b>( - &'a self, - opctx: &'b OpContext, - action: authz::Action, - ) -> BoxFuture<'a, Result<(), Error>> - where - 'b: 'a, - { - opctx.authorize(action, self).boxed() - } + fn do_authorize<'a, 'b>( + &'a self, + opctx: &'b OpContext, + action: authz::Action, + ) -> BoxFuture<'a, Result<(), Error>> + where + 'b: 'a, + { + opctx.authorize(action, self).boxed() + } + } + }; } + +impl_dyn_authorized_resource_for_global!(authz::oso_generic::Database); +impl_dyn_authorized_resource_for_global!(authz::ConsoleSessionList); +impl_dyn_authorized_resource_for_global!(authz::GlobalImageList); +impl_dyn_authorized_resource_for_global!(authz::IpPoolList); +impl_dyn_authorized_resource_for_global!(authz::DeviceAuthRequestList); diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index 0a2fa4eca1..5a9c78baef 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -63,6 +63,10 @@ pub async fn make_resources<'a>( ) -> ResourceSet { builder.new_resource(authz::DATABASE.clone()); builder.new_resource_with_users(authz::FLEET.clone()).await; + builder.new_resource(authz::CONSOLE_SESSION_LIST.clone()); + builder.new_resource(authz::DEVICE_AUTH_REQUEST_LIST.clone()); + builder.new_resource(authz::GLOBAL_IMAGE_LIST.clone()); + builder.new_resource(authz::IP_POOL_LIST.clone()); make_silo(&mut builder, "silo1", main_silo_id, true).await; make_silo(&mut builder, "silo2", Uuid::new_v4(), false).await; @@ -187,7 +191,7 @@ pub fn exempted_authz_classes() -> BTreeSet { // // (1) because its behavior is identical to that of some other resource // that we are testing (i.e., same Polar snippet and identical - // configuration for the authz type). There aren't any examples of + // configuration for the authz type). There aren't many examples of // this today, but it might be reasonable to do this for resources // that are indistinguishable to the authz subsystem (e.g., Disks, // Instances, Vpcs, and other things nested directly below Project) @@ -206,19 +210,13 @@ pub fn exempted_authz_classes() -> BTreeSet { authz::actor::AuthenticatedActor::get_polar_class(), // Resources whose behavior should be identical to an existing type // and we don't want to do the test twice for performance reasons: - // none yet. - // + authz::NetworkInterface::get_polar_class(), // TODO-coverage Resources that we should test, but for which we // have not yet added a test. PLEASE: instead of adding something // to this list, modify `make_resources()` to test it instead. This // should be pretty straightforward in most cases. Adding a new // class to this list makes it harder to catch security flaws! - authz::IpPoolList::get_polar_class(), - authz::GlobalImageList::get_polar_class(), - authz::ConsoleSessionList::get_polar_class(), - authz::DeviceAuthRequestList::get_polar_class(), authz::IpPool::get_polar_class(), - authz::NetworkInterface::get_polar_class(), authz::VpcRouter::get_polar_class(), authz::RouterRoute::get_polar_class(), authz::ConsoleSession::get_polar_class(), diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out index c045392dfb..546a7a5c7d 100644 --- a/nexus/tests/output/authz-roles.out +++ b/nexus/tests/output/authz-roles.out @@ -1,4 +1,4 @@ -resource: DATABASE +resource: authz::oso_generic::Database USER Q R LC RP M MP CC D LP fleet-admin ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -32,6 +32,74 @@ resource: Fleet id "001de000-1334-4000-8000-000000000000" silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! +resource: authz::ConsoleSessionList + + USER Q R LC RP M MP CC D LP + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! ! + +resource: authz::DeviceAuthRequestList + + USER Q R LC RP M MP CC D LP + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! ! + +resource: authz::GlobalImageList + + USER Q R LC RP M MP CC D LP + fleet-admin ✘ ✘ ✔ ✘ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! ! + +resource: authz::IpPoolList + + USER Q R LC RP M MP CC D LP + fleet-admin ✘ ✘ ✔ ✘ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! ! + resource: Silo "silo1" USER Q R LC RP M MP CC D LP From f4fc02ce80f376a710d5c656cbd56db9c898be29 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 12:01:20 -0700 Subject: [PATCH 22/32] remove last XXX --- nexus/src/authz/policy_test/resources.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index 5a9c78baef..80cb6c5114 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -163,7 +163,6 @@ async fn make_project( LookupType::ByName(vpc1_name.clone()), ); - // XXX-dap TODO-coverage add more different kinds of children builder.new_resource(authz::Disk::new( project.clone(), Uuid::new_v4(), From 795048990cdc1023fe3243b677876fce5b3aa856 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 13:31:25 -0700 Subject: [PATCH 23/32] add racks, sleds, NICs --- .../src/authz/policy_test/resource_builder.rs | 15 +-- nexus/src/authz/policy_test/resources.rs | 35 ++++-- nexus/tests/output/authz-roles.out | 102 ++++++++++++++++++ 3 files changed, 139 insertions(+), 13 deletions(-) diff --git a/nexus/src/authz/policy_test/resource_builder.rs b/nexus/src/authz/policy_test/resource_builder.rs index c33b61d306..e9fb26bbf6 100644 --- a/nexus/src/authz/policy_test/resource_builder.rs +++ b/nexus/src/authz/policy_test/resource_builder.rs @@ -11,7 +11,6 @@ use crate::authz::ApiResourceWithRolesType; use crate::authz::AuthorizedResource; use crate::context::OpContext; use crate::db; -use crate::db::fixed_data::FLEET_ID; use authz::ApiResource; use futures::future::BoxFuture; use futures::FutureExt; @@ -86,16 +85,18 @@ impl<'a> ResourceBuilder<'a> { self.new_resource(resource.clone()); let resource_name = match resource.lookup_type() { - LookupType::ByName(name) => name, - LookupType::ById(id) if *id == *FLEET_ID => "fleet", - LookupType::ById(_) - | LookupType::BySessionToken(_) - | LookupType::ByCompositeId(_) => { + LookupType::ByName(name) => name.clone(), + LookupType::ById(_) => { + // For resources identified only by id, we only have one of them + // in our test suite and it's more convenient to omit the id + // (e.g., "fleet"). + resource.resource_type().to_string().to_lowercase() + } + LookupType::BySessionToken(_) | LookupType::ByCompositeId(_) => { panic!("test resources must be given names"); } }; let silo_id = self.main_silo_id; - let opctx = self.opctx; let datastore = self.datastore; for role in T::AllowedRoles::iter() { diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index 80cb6c5114..eeefe6620a 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -61,6 +61,7 @@ pub async fn make_resources<'a>( mut builder: ResourceBuilder<'a>, main_silo_id: Uuid, ) -> ResourceSet { + // Global resources builder.new_resource(authz::DATABASE.clone()); builder.new_resource_with_users(authz::FLEET.clone()).await; builder.new_resource(authz::CONSOLE_SESSION_LIST.clone()); @@ -68,9 +69,25 @@ pub async fn make_resources<'a>( builder.new_resource(authz::GLOBAL_IMAGE_LIST.clone()); builder.new_resource(authz::IP_POOL_LIST.clone()); + // Silo/organization/project hierarchy make_silo(&mut builder, "silo1", main_silo_id, true).await; make_silo(&mut builder, "silo2", Uuid::new_v4(), false).await; + // Various other resources + let rack_id = "c037e882-8b6d-c8b5-bef4-97e848eb0a50".parse().unwrap(); + builder.new_resource(authz::Rack::new( + authz::FLEET.clone(), + Uuid::new_v4(), + LookupType::ById(rack_id), + )); + + let sled_id = "8a785566-adaf-c8d8-e886-bee7f9b73ca7".parse().unwrap(); + builder.new_resource(authz::Sled::new( + authz::FLEET.clone(), + Uuid::new_v4(), + LookupType::ById(sled_id), + )); + builder.build() } @@ -163,15 +180,22 @@ async fn make_project( LookupType::ByName(vpc1_name.clone()), ); + let instance_name = format!("{}-instance1", project_name); + let instance = authz::Instance::new( + project.clone(), + Uuid::new_v4(), + LookupType::ByName(instance_name.clone()), + ); builder.new_resource(authz::Disk::new( project.clone(), Uuid::new_v4(), LookupType::ByName(format!("{}-disk1", project_name)), )); - builder.new_resource(authz::Instance::new( - project.clone(), + builder.new_resource(instance.clone()); + builder.new_resource(authz::NetworkInterface::new( + instance, Uuid::new_v4(), - LookupType::ByName(format!("{}-instance1", project_name)), + LookupType::ByName(format!("{}-nic1", instance_name)), )); builder.new_resource(vpc1.clone()); // Test a resource nested two levels below Project @@ -209,7 +233,8 @@ pub fn exempted_authz_classes() -> BTreeSet { authz::actor::AuthenticatedActor::get_polar_class(), // Resources whose behavior should be identical to an existing type // and we don't want to do the test twice for performance reasons: - authz::NetworkInterface::get_polar_class(), + // none yet. + // // TODO-coverage Resources that we should test, but for which we // have not yet added a test. PLEASE: instead of adding something // to this list, modify `make_resources()` to test it instead. This @@ -221,14 +246,12 @@ pub fn exempted_authz_classes() -> BTreeSet { authz::ConsoleSession::get_polar_class(), authz::DeviceAuthRequest::get_polar_class(), authz::DeviceAccessToken::get_polar_class(), - authz::Rack::get_polar_class(), authz::RoleBuiltin::get_polar_class(), authz::SshKey::get_polar_class(), authz::SiloUser::get_polar_class(), authz::SiloGroup::get_polar_class(), authz::IdentityProvider::get_polar_class(), authz::SamlIdentityProvider::get_polar_class(), - authz::Sled::get_polar_class(), authz::UpdateAvailableArtifact::get_polar_class(), authz::UserBuiltin::get_polar_class(), authz::GlobalImage::get_polar_class(), diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out index 546a7a5c7d..d1c584c755 100644 --- a/nexus/tests/output/authz-roles.out +++ b/nexus/tests/output/authz-roles.out @@ -185,6 +185,23 @@ resource: Instance "silo1-org1-proj1-instance1" silo1-org1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! +resource: NetworkInterface "silo1-org1-proj1-instance1-nic1" + + USER Q R LC RP M MP CC D LP + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-proj1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! ! + resource: Vpc "silo1-org1-proj1-vpc1" USER Q R LC RP M MP CC D LP @@ -270,6 +287,23 @@ resource: Instance "silo1-org1-proj2-instance1" silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! +resource: NetworkInterface "silo1-org1-proj2-instance1-nic1" + + USER Q R LC RP M MP CC D LP + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-org1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! ! + resource: Vpc "silo1-org1-proj2-vpc1" USER Q R LC RP M MP CC D LP @@ -372,6 +406,23 @@ resource: Instance "silo1-org2-proj1-instance1" silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! +resource: NetworkInterface "silo1-org2-proj1-instance1-nic1" + + USER Q R LC RP M MP CC D LP + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! ! + resource: Vpc "silo1-org2-proj1-vpc1" USER Q R LC RP M MP CC D LP @@ -491,6 +542,23 @@ resource: Instance "silo2-org1-proj1-instance1" silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! +resource: NetworkInterface "silo2-org1-proj1-instance1-nic1" + + USER Q R LC RP M MP CC D LP + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! ! + resource: Vpc "silo2-org1-proj1-vpc1" USER Q R LC RP M MP CC D LP @@ -525,6 +593,40 @@ resource: VpcSubnet "silo2-org1-proj1-vpc1-subnet1" silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ unauthenticated ! ! ! ! ! ! ! ! ! +resource: Rack id "c037e882-8b6d-c8b5-bef4-97e848eb0a50" + + USER Q R LC RP M MP CC D LP + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! ! + +resource: Sled id "8a785566-adaf-c8d8-e886-bee7f9b73ca7" + + USER Q R LC RP M MP CC D LP + fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ + fleet-collaborator ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + silo1-org1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + unauthenticated ! ! ! ! ! ! ! ! ! + ACTIONS: Q = Query From 3209cf2fe8ac2b6ee8786349dcf59b29b1c23d49 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 14:34:28 -0700 Subject: [PATCH 24/32] Fleet privileges should not cascade into Silos --- nexus/src/authz/omicron.polar | 14 +- nexus/src/authz/policy_test/mod.rs | 28 +++- nexus/src/authz/policy_test/resources.rs | 8 +- nexus/src/db/fixed_data/role_assignment.rs | 3 +- nexus/src/db/fixed_data/role_builtin.rs | 11 +- nexus/src/db/fixed_data/silo_user.rs | 10 +- nexus/tests/integration_tests/saml.rs | 79 +++++++++- nexus/tests/integration_tests/silos.rs | 48 ++++++ nexus/tests/output/authz-roles.out | 174 ++++++++++----------- 9 files changed, 271 insertions(+), 104 deletions(-) diff --git a/nexus/src/authz/omicron.polar b/nexus/src/authz/omicron.polar index 733b918ada..6e284077f3 100644 --- a/nexus/src/authz/omicron.polar +++ b/nexus/src/authz/omicron.polar @@ -61,7 +61,7 @@ has_role(actor: AuthenticatedActor, role: String, resource: Resource) # # - fleet.admin (superuser for the whole system) # - fleet.collaborator (can manage Silos) -# - fleet.viewer (can read most resources in the system) +# - fleet.viewer (can read most non-siloed resources in the system) # - silo.admin (superuser for the silo) # - silo.collaborator (can create and own Organizations) # - silo.viewer (can read most resources within the Silo) @@ -132,10 +132,16 @@ resource Silo { "create_child" if "collaborator"; "modify" if "admin"; - # Roles implied by roles on this resource's parent (Fleet) + # Permissions implied by roles on this resource's parent (Fleet). Fleet + # privileges allow a user to see and potentially administer the Silo, + # but they do not give anyone permission to look at anything inside the + # Silo. To achieve this, we use permission rules here. (If we granted + # Fleet administrators _roles_ on the Silo, then those would cascade + # into the Silo as well.) relations = { parent_fleet: Fleet }; - "admin" if "collaborator" on "parent_fleet"; - "viewer" if "viewer" on "parent_fleet"; + "read" if "viewer" on "parent_fleet"; + "list_identity_providers" if "viewer" on "parent_fleet"; + "modify" if "collaborator" on "parent_fleet"; # external authenticator has to create silo users "list_children" if "external-authenticator" on "parent_fleet"; diff --git a/nexus/src/authz/policy_test/mod.rs b/nexus/src/authz/policy_test/mod.rs index e02a07c1ce..04d9b8b747 100644 --- a/nexus/src/authz/policy_test/mod.rs +++ b/nexus/src/authz/policy_test/mod.rs @@ -14,14 +14,19 @@ mod coverage; mod resource_builder; mod resources; +use super::SiloRole; use crate::authn; use crate::authz; use crate::context::OpContext; use crate::db; +use authn::USER_TEST_PRIVILEGED; use coverage::Coverage; use futures::StreamExt; use nexus_test_utils::db::test_setup_database; +use nexus_types::external_api::shared; +use nexus_types::identity::Asset; use omicron_common::api::external::Error; +use omicron_common::api::external::LookupType; use omicron_test_utils::dev; use resource_builder::DynAuthorizedResource; use resource_builder::ResourceBuilder; @@ -55,13 +60,34 @@ async fn test_iam_roles_behavior() { let mut db = test_setup_database(&logctx.log).await; let (opctx, datastore) = db::datastore::datastore_test(&logctx, &db).await; + // Before we can create the resources, users, and role assignments that we + // need, we must grant the "test-privileged" user privileges to fetch and + // modify policies inside the "main" Silo (the one we create users in). + let main_silo_id = Uuid::new_v4(); + let main_silo = authz::Silo::new( + authz::FLEET, + main_silo_id, + LookupType::ById(main_silo_id), + ); + datastore + .role_assignment_replace_visible( + &opctx, + &main_silo, + &[shared::RoleAssignment { + identity_type: shared::IdentityType::SiloUser, + identity_id: USER_TEST_PRIVILEGED.id(), + role_name: SiloRole::Admin, + }], + ) + .await + .unwrap(); + // Assemble the list of resources that we'll use for testing. As we create // these resources, create the users and role assignments needed for the // exhaustive test. `Coverage` is used to help verify that all resources // are tested or explicitly opted out. let exemptions = resources::exempted_authz_classes(); let mut coverage = Coverage::new(&logctx.log, exemptions); - let main_silo_id = Uuid::new_v4(); let builder = ResourceBuilder::new(&opctx, &datastore, &mut coverage, main_silo_id); let test_resources = resources::make_resources(builder, main_silo_id).await; diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index eeefe6620a..1dd9bc8f94 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -98,15 +98,15 @@ async fn make_silo( silo_id: Uuid, first_branch: bool, ) { - let silo1 = authz::Silo::new( + let silo = authz::Silo::new( authz::FLEET, silo_id, LookupType::ByName(silo_name.to_string()), ); if first_branch { - builder.new_resource_with_users(silo1.clone()).await; + builder.new_resource_with_users(silo.clone()).await; } else { - builder.new_resource(silo1.clone()); + builder.new_resource(silo.clone()); } let norganizations = if first_branch { 2 } else { 1 }; @@ -115,7 +115,7 @@ async fn make_silo( let org_first_branch = first_branch && i == 0; make_organization( builder, - &silo1, + &silo, &organization_name, org_first_branch, ) diff --git a/nexus/src/db/fixed_data/role_assignment.rs b/nexus/src/db/fixed_data/role_assignment.rs index f6bbb951b6..7ca398b811 100644 --- a/nexus/src/db/fixed_data/role_assignment.rs +++ b/nexus/src/db/fixed_data/role_assignment.rs @@ -14,7 +14,7 @@ lazy_static! { pub static ref BUILTIN_ROLE_ASSIGNMENTS: Vec = vec![ // The "internal-api" user gets the "admin" role on the sole Fleet. - // This will grant them (nearly) all permissions on all resources. + // This is a pretty elevated privilege. // TODO-security We should scope this down (or, really, figure out a // better internal authn/authz story). RoleAssignment::new( @@ -24,6 +24,7 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), + // XXX-dap remove this one? RoleAssignment::new( IdentityType::UserBuiltin, user_builtin::USER_SERVICE_BALANCER.id, diff --git a/nexus/src/db/fixed_data/role_builtin.rs b/nexus/src/db/fixed_data/role_builtin.rs index eaa43d162b..07dcbd0d06 100644 --- a/nexus/src/db/fixed_data/role_builtin.rs +++ b/nexus/src/db/fixed_data/role_builtin.rs @@ -41,6 +41,11 @@ lazy_static! { role_name: "collaborator", description: "Organization Collaborator", }; + pub static ref SILO_ADMIN: RoleBuiltinConfig = RoleBuiltinConfig { + resource_type: api::external::ResourceType::Silo, + role_name: "admin", + description: "Silo Administrator", + }; pub static ref BUILTIN_ROLES: Vec = vec![ FLEET_ADMIN.clone(), FLEET_AUTHENTICATOR.clone(), @@ -50,11 +55,7 @@ lazy_static! { role_name: "collaborator", description: "Fleet Collaborator", }, - RoleBuiltinConfig { - resource_type: api::external::ResourceType::Silo, - role_name: "admin", - description: "Silo Administrator", - }, + SILO_ADMIN.clone(), RoleBuiltinConfig { resource_type: api::external::ResourceType::Silo, role_name: "collaborator", diff --git a/nexus/src/db/fixed_data/silo_user.rs b/nexus/src/db/fixed_data/silo_user.rs index d985b7a414..cd3f01f350 100644 --- a/nexus/src/db/fixed_data/silo_user.rs +++ b/nexus/src/db/fixed_data/silo_user.rs @@ -26,7 +26,7 @@ lazy_static! { pub static ref ROLE_ASSIGNMENTS_PRIVILEGED: Vec = vec![ // The "test-privileged" user gets the "admin" role on the sole - // Fleet. This will grant them all permissions on all resources. + // Fleet as well as the default Silo. db::model::RoleAssignment::new( db::model::IdentityType::SiloUser, USER_TEST_PRIVILEGED.id(), @@ -34,6 +34,14 @@ lazy_static! { *db::fixed_data::FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), + + db::model::RoleAssignment::new( + db::model::IdentityType::SiloUser, + USER_TEST_PRIVILEGED.id(), + role_builtin::SILO_ADMIN.resource_type, + *db::fixed_data::silo::SILO_ID, + role_builtin::SILO_ADMIN.role_name, + ), ]; /// Test user that's granted no privileges, used for automated testing diff --git a/nexus/tests/integration_tests/saml.rs b/nexus/tests/integration_tests/saml.rs index af6bc7e48c..c9a189e872 100644 --- a/nexus/tests/integration_tests/saml.rs +++ b/nexus/tests/integration_tests/saml.rs @@ -14,12 +14,17 @@ use omicron_nexus::TestInterfaces; use http::method::Method; use http::StatusCode; -use nexus_test_utils::resource_helpers::{create_silo, object_create}; +use nexus_test_utils::resource_helpers::{ + create_silo, grant_iam, object_create, +}; use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use httptest::{matchers::*, responders::*, Expectation, Server}; +use omicron_nexus::authn::USER_TEST_PRIVILEGED; +use omicron_nexus::authz::SiloRole; +use omicron_nexus::db::identity::Asset; // Valid SAML IdP entity descriptor from https://en.wikipedia.org/wiki/SAML_metadata#Identity_provider_metadata // note: no signing keys @@ -34,6 +39,14 @@ async fn test_create_a_saml_idp(cptestctx: &ControlPlaneTestContext) { const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let silo: Silo = NexusRequest::object_get(&client, &format!("/silos/{}", SILO_NAME,)) @@ -152,6 +165,14 @@ async fn test_create_a_saml_idp_invalid_descriptor_truncated( const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let saml_idp_descriptor = { let mut saml_idp_descriptor = SAML_IDP_DESCRIPTOR.to_string(); @@ -212,6 +233,14 @@ async fn test_create_a_saml_idp_invalid_descriptor_no_redirect_binding( const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let saml_idp_descriptor = { let saml_idp_descriptor = SAML_IDP_DESCRIPTOR.to_string(); @@ -282,6 +311,14 @@ async fn test_create_a_hidden_silo_saml_idp( create_silo(&client, "hidden", false, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + "/silos/hidden", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; // Valid IdP descriptor let saml_idp_descriptor = SAML_IDP_DESCRIPTOR.to_string(); @@ -351,6 +388,14 @@ async fn test_saml_idp_metadata_url_404(cptestctx: &ControlPlaneTestContext) { const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let server = Server::run(); server.expect( @@ -405,6 +450,14 @@ async fn test_saml_idp_metadata_url_invalid( const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; NexusRequest::new( RequestBuilder::new( @@ -561,6 +614,14 @@ async fn test_saml_idp_rsa_keypair_ok(cptestctx: &ControlPlaneTestContext) { const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; NexusRequest::new( RequestBuilder::new( @@ -932,6 +993,14 @@ async fn test_post_saml_response(cptestctx: &ControlPlaneTestContext) { const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Jit).await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let _silo_saml_idp: views::SamlIdentityProvider = object_create( client, @@ -1025,6 +1094,14 @@ async fn test_post_saml_response_with_relay_state( const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Jit).await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let _silo_saml_idp: views::SamlIdentityProvider = object_create( client, diff --git a/nexus/tests/integration_tests/silos.rs b/nexus/tests/integration_tests/silos.rs index 1e94970f08..96f80d5016 100644 --- a/nexus/tests/integration_tests/silos.rs +++ b/nexus/tests/integration_tests/silos.rs @@ -231,6 +231,14 @@ async fn test_silo_admin_group(cptestctx: &ControlPlaneTestContext) { }, ) .await; + grant_iam( + &client, + "/silos/silo-name", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let authn_opctx = nexus.opctx_external_authn(); @@ -507,6 +515,14 @@ async fn test_saml_idp_metadata_data_valid( create_silo(&client, "blahblah", true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + "/silos/blahblah", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let silo_saml_idp: SamlIdentityProvider = object_create( client, @@ -568,6 +584,14 @@ async fn test_saml_idp_metadata_data_truncated( create_silo(&client, "blahblah", true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + "/silos/blahblah", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; NexusRequest::new( RequestBuilder::new( @@ -621,6 +645,14 @@ async fn test_saml_idp_metadata_data_invalid( const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; NexusRequest::new( RequestBuilder::new( @@ -767,6 +799,14 @@ async fn test_silo_user_fetch_by_external_id( shared::UserProvisionType::Fixed, ) .await; + grant_iam( + &client, + "/silos/test-silo", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let opctx = OpContext::for_tests( cptestctx.logctx.log.new(o!()), @@ -1304,6 +1344,14 @@ async fn test_silo_delete_clean_up_groups(cptestctx: &ControlPlaneTestContext) { let silo = create_silo(&client, "test-silo", true, shared::UserProvisionType::Jit) .await; + grant_iam( + &client, + "/silos/test-silo", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let opctx = OpContext::for_tests( cptestctx.logctx.log.new(o!()), diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out index d1c584c755..d45f392f78 100644 --- a/nexus/tests/output/authz-roles.out +++ b/nexus/tests/output/authz-roles.out @@ -103,9 +103,9 @@ resource: authz::IpPoolList resource: Silo "silo1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✔ + fleet-admin ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ ✔ + fleet-collaborator ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ ✔ + fleet-viewer ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ ✔ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔ silo1-collaborator ✘ ✔ ✔ ✔ ✘ ✘ ✔ ✘ ✔ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✔ @@ -120,9 +120,9 @@ resource: Silo "silo1" resource: Organization "silo1-org1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -137,9 +137,9 @@ resource: Organization "silo1-org1" resource: Project "silo1-org1-proj1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -154,9 +154,9 @@ resource: Project "silo1-org1-proj1" resource: Disk "silo1-org1-proj1-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -171,9 +171,9 @@ resource: Disk "silo1-org1-proj1-disk1" resource: Instance "silo1-org1-proj1-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -188,9 +188,9 @@ resource: Instance "silo1-org1-proj1-instance1" resource: NetworkInterface "silo1-org1-proj1-instance1-nic1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -205,9 +205,9 @@ resource: NetworkInterface "silo1-org1-proj1-instance1-nic1" resource: Vpc "silo1-org1-proj1-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -222,9 +222,9 @@ resource: Vpc "silo1-org1-proj1-vpc1" resource: VpcSubnet "silo1-org1-proj1-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -239,9 +239,9 @@ resource: VpcSubnet "silo1-org1-proj1-vpc1-subnet1" resource: Project "silo1-org1-proj2" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -256,9 +256,9 @@ resource: Project "silo1-org1-proj2" resource: Disk "silo1-org1-proj2-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -273,9 +273,9 @@ resource: Disk "silo1-org1-proj2-disk1" resource: Instance "silo1-org1-proj2-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -290,9 +290,9 @@ resource: Instance "silo1-org1-proj2-instance1" resource: NetworkInterface "silo1-org1-proj2-instance1-nic1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -307,9 +307,9 @@ resource: NetworkInterface "silo1-org1-proj2-instance1-nic1" resource: Vpc "silo1-org1-proj2-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -324,9 +324,9 @@ resource: Vpc "silo1-org1-proj2-vpc1" resource: VpcSubnet "silo1-org1-proj2-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -341,9 +341,9 @@ resource: VpcSubnet "silo1-org1-proj2-vpc1-subnet1" resource: Organization "silo1-org2" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -358,9 +358,9 @@ resource: Organization "silo1-org2" resource: Project "silo1-org2-proj1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -375,9 +375,9 @@ resource: Project "silo1-org2-proj1" resource: Disk "silo1-org2-proj1-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -392,9 +392,9 @@ resource: Disk "silo1-org2-proj1-disk1" resource: Instance "silo1-org2-proj1-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -409,9 +409,9 @@ resource: Instance "silo1-org2-proj1-instance1" resource: NetworkInterface "silo1-org2-proj1-instance1-nic1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -426,9 +426,9 @@ resource: NetworkInterface "silo1-org2-proj1-instance1-nic1" resource: Vpc "silo1-org2-proj1-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -443,9 +443,9 @@ resource: Vpc "silo1-org2-proj1-vpc1" resource: VpcSubnet "silo1-org2-proj1-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -460,9 +460,9 @@ resource: VpcSubnet "silo1-org2-proj1-vpc1-subnet1" resource: Silo "silo2" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ ✔ + fleet-collaborator ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ ✔ + fleet-viewer ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ ✔ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -477,9 +477,9 @@ resource: Silo "silo2" resource: Organization "silo2-org1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -494,9 +494,9 @@ resource: Organization "silo2-org1" resource: Project "silo2-org1-proj1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -511,9 +511,9 @@ resource: Project "silo2-org1-proj1" resource: Disk "silo2-org1-proj1-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -528,9 +528,9 @@ resource: Disk "silo2-org1-proj1-disk1" resource: Instance "silo2-org1-proj1-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -545,9 +545,9 @@ resource: Instance "silo2-org1-proj1-instance1" resource: NetworkInterface "silo2-org1-proj1-instance1-nic1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -562,9 +562,9 @@ resource: NetworkInterface "silo2-org1-proj1-instance1-nic1" resource: Vpc "silo2-org1-proj1-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -579,9 +579,9 @@ resource: Vpc "silo2-org1-proj1-vpc1" resource: VpcSubnet "silo2-org1-proj1-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ From f3a0a807f2fc2e2800410adee446df0b4c8320c0 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 15:21:53 -0700 Subject: [PATCH 25/32] fix style --- nexus/src/authz/policy_test/resources.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index 1dd9bc8f94..aab79b057a 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -113,13 +113,8 @@ async fn make_silo( for i in 0..norganizations { let organization_name = format!("{}-org{}", silo_name, i + 1); let org_first_branch = first_branch && i == 0; - make_organization( - builder, - &silo, - &organization_name, - org_first_branch, - ) - .await; + make_organization(builder, &silo, &organization_name, org_first_branch) + .await; } } From 7d99ea31f1e81813d85059c0b02235397aef959a Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 15:23:41 -0700 Subject: [PATCH 26/32] remove unused service balancer role --- nexus/src/db/fixed_data/role_assignment.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/nexus/src/db/fixed_data/role_assignment.rs b/nexus/src/db/fixed_data/role_assignment.rs index 7ca398b811..43635518c9 100644 --- a/nexus/src/db/fixed_data/role_assignment.rs +++ b/nexus/src/db/fixed_data/role_assignment.rs @@ -24,14 +24,6 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), - // XXX-dap remove this one? - RoleAssignment::new( - IdentityType::UserBuiltin, - user_builtin::USER_SERVICE_BALANCER.id, - role_builtin::FLEET_ADMIN.resource_type, - *FLEET_ID, - role_builtin::FLEET_ADMIN.role_name, - ), // The "internal-read" user gets the "viewer" role on the sole // Fleet. This will grant them the ability to read various control From 3377e50c2c017a6e18c85c321856c10afa1bfa48 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 15:39:08 -0700 Subject: [PATCH 27/32] fix test --- nexus/src/app/silo.rs | 9 ++++++++- nexus/src/db/datastore/silo.rs | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index ddf08dfc7e..49f66c02f3 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -31,7 +31,14 @@ impl super::Nexus { opctx: &OpContext, new_silo_params: params::SiloCreate, ) -> CreateResult { - self.datastore().silo_create(&opctx, new_silo_params).await + // Silo group creation happens as Nexus's "external authn" context, + // not the user's context here. The user may not have permission to + // create arbitrary groups in the Silo, but we allow them to create + // this one in this case. + let external_authn_opctx = self.opctx_external_authn(); + self.datastore() + .silo_create(&opctx, &external_authn_opctx, new_silo_params) + .await } pub async fn silos_list_by_name( diff --git a/nexus/src/db/datastore/silo.rs b/nexus/src/db/datastore/silo.rs index 491e9c4a44..96324b8350 100644 --- a/nexus/src/db/datastore/silo.rs +++ b/nexus/src/db/datastore/silo.rs @@ -71,6 +71,7 @@ impl DataStore { pub async fn silo_create( &self, opctx: &OpContext, + group_opctx: &OpContext, new_silo_params: params::SiloCreate, ) -> CreateResult { let silo_id = Uuid::new_v4(); @@ -90,7 +91,7 @@ impl DataStore { { let silo_admin_group_ensure_query = DataStore::silo_group_ensure_query( - opctx, + &group_opctx, &authz_silo, db::model::SiloGroup::new( silo_group_id, From 5771c3c6e01110b67ab7257139eb0eb0ad10e538 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Fri, 12 Aug 2022 10:29:45 -0700 Subject: [PATCH 28/32] update to 0.26.2 for fix for oso#1592 --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29c9b29d6b..b6c3803539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3241,9 +3241,9 @@ checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" [[package]] name = "oso" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f60e93371698d27da6df716b4523ed72eaf3cf85e7f36fa96a04d1e72ae29c" +checksum = "736242f751b0f25d9361042fd856e1a54cc3fc37b033245cc4e9d69f751f87e6" dependencies = [ "impl-trait-for-tuples", "lazy_static", @@ -3257,9 +3257,9 @@ dependencies = [ [[package]] name = "oso-derive" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca6cc08d0dda47f82240caafbc46b2701c4fe1a4692394d4292c4b2f2dd00a2" +checksum = "47da8980dacacbc0cbbdd88ff252bacef5aafa368261d6703f35f8933bc45aca" dependencies = [ "quote", "syn", @@ -3683,9 +3683,9 @@ dependencies = [ [[package]] name = "polar-core" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d532b44ae9b5baa9561472c100a6b99b14d305cfe59422d04f550c9933c1d5" +checksum = "a942921f9a8bd9753db813ebb5ea4ad25d0cad5de338ea987a32b2631a4f397a" dependencies = [ "indoc", "js-sys", From d2766c6cea9224fa689b388e4d556af8c8f47b57 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 14:34:28 -0700 Subject: [PATCH 29/32] Fleet privileges should not cascade into Silos --- nexus/src/authz/omicron.polar | 14 +- nexus/src/authz/policy_test/mod.rs | 28 +++- nexus/src/authz/policy_test/resources.rs | 8 +- nexus/src/db/fixed_data/role_assignment.rs | 3 +- nexus/src/db/fixed_data/role_builtin.rs | 11 +- nexus/src/db/fixed_data/silo_user.rs | 10 +- nexus/tests/integration_tests/saml.rs | 79 +++++++++- nexus/tests/integration_tests/silos.rs | 48 ++++++ nexus/tests/output/authz-roles.out | 174 ++++++++++----------- 9 files changed, 271 insertions(+), 104 deletions(-) diff --git a/nexus/src/authz/omicron.polar b/nexus/src/authz/omicron.polar index 733b918ada..6e284077f3 100644 --- a/nexus/src/authz/omicron.polar +++ b/nexus/src/authz/omicron.polar @@ -61,7 +61,7 @@ has_role(actor: AuthenticatedActor, role: String, resource: Resource) # # - fleet.admin (superuser for the whole system) # - fleet.collaborator (can manage Silos) -# - fleet.viewer (can read most resources in the system) +# - fleet.viewer (can read most non-siloed resources in the system) # - silo.admin (superuser for the silo) # - silo.collaborator (can create and own Organizations) # - silo.viewer (can read most resources within the Silo) @@ -132,10 +132,16 @@ resource Silo { "create_child" if "collaborator"; "modify" if "admin"; - # Roles implied by roles on this resource's parent (Fleet) + # Permissions implied by roles on this resource's parent (Fleet). Fleet + # privileges allow a user to see and potentially administer the Silo, + # but they do not give anyone permission to look at anything inside the + # Silo. To achieve this, we use permission rules here. (If we granted + # Fleet administrators _roles_ on the Silo, then those would cascade + # into the Silo as well.) relations = { parent_fleet: Fleet }; - "admin" if "collaborator" on "parent_fleet"; - "viewer" if "viewer" on "parent_fleet"; + "read" if "viewer" on "parent_fleet"; + "list_identity_providers" if "viewer" on "parent_fleet"; + "modify" if "collaborator" on "parent_fleet"; # external authenticator has to create silo users "list_children" if "external-authenticator" on "parent_fleet"; diff --git a/nexus/src/authz/policy_test/mod.rs b/nexus/src/authz/policy_test/mod.rs index e02a07c1ce..04d9b8b747 100644 --- a/nexus/src/authz/policy_test/mod.rs +++ b/nexus/src/authz/policy_test/mod.rs @@ -14,14 +14,19 @@ mod coverage; mod resource_builder; mod resources; +use super::SiloRole; use crate::authn; use crate::authz; use crate::context::OpContext; use crate::db; +use authn::USER_TEST_PRIVILEGED; use coverage::Coverage; use futures::StreamExt; use nexus_test_utils::db::test_setup_database; +use nexus_types::external_api::shared; +use nexus_types::identity::Asset; use omicron_common::api::external::Error; +use omicron_common::api::external::LookupType; use omicron_test_utils::dev; use resource_builder::DynAuthorizedResource; use resource_builder::ResourceBuilder; @@ -55,13 +60,34 @@ async fn test_iam_roles_behavior() { let mut db = test_setup_database(&logctx.log).await; let (opctx, datastore) = db::datastore::datastore_test(&logctx, &db).await; + // Before we can create the resources, users, and role assignments that we + // need, we must grant the "test-privileged" user privileges to fetch and + // modify policies inside the "main" Silo (the one we create users in). + let main_silo_id = Uuid::new_v4(); + let main_silo = authz::Silo::new( + authz::FLEET, + main_silo_id, + LookupType::ById(main_silo_id), + ); + datastore + .role_assignment_replace_visible( + &opctx, + &main_silo, + &[shared::RoleAssignment { + identity_type: shared::IdentityType::SiloUser, + identity_id: USER_TEST_PRIVILEGED.id(), + role_name: SiloRole::Admin, + }], + ) + .await + .unwrap(); + // Assemble the list of resources that we'll use for testing. As we create // these resources, create the users and role assignments needed for the // exhaustive test. `Coverage` is used to help verify that all resources // are tested or explicitly opted out. let exemptions = resources::exempted_authz_classes(); let mut coverage = Coverage::new(&logctx.log, exemptions); - let main_silo_id = Uuid::new_v4(); let builder = ResourceBuilder::new(&opctx, &datastore, &mut coverage, main_silo_id); let test_resources = resources::make_resources(builder, main_silo_id).await; diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index eeefe6620a..1dd9bc8f94 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -98,15 +98,15 @@ async fn make_silo( silo_id: Uuid, first_branch: bool, ) { - let silo1 = authz::Silo::new( + let silo = authz::Silo::new( authz::FLEET, silo_id, LookupType::ByName(silo_name.to_string()), ); if first_branch { - builder.new_resource_with_users(silo1.clone()).await; + builder.new_resource_with_users(silo.clone()).await; } else { - builder.new_resource(silo1.clone()); + builder.new_resource(silo.clone()); } let norganizations = if first_branch { 2 } else { 1 }; @@ -115,7 +115,7 @@ async fn make_silo( let org_first_branch = first_branch && i == 0; make_organization( builder, - &silo1, + &silo, &organization_name, org_first_branch, ) diff --git a/nexus/src/db/fixed_data/role_assignment.rs b/nexus/src/db/fixed_data/role_assignment.rs index f6bbb951b6..7ca398b811 100644 --- a/nexus/src/db/fixed_data/role_assignment.rs +++ b/nexus/src/db/fixed_data/role_assignment.rs @@ -14,7 +14,7 @@ lazy_static! { pub static ref BUILTIN_ROLE_ASSIGNMENTS: Vec = vec![ // The "internal-api" user gets the "admin" role on the sole Fleet. - // This will grant them (nearly) all permissions on all resources. + // This is a pretty elevated privilege. // TODO-security We should scope this down (or, really, figure out a // better internal authn/authz story). RoleAssignment::new( @@ -24,6 +24,7 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), + // XXX-dap remove this one? RoleAssignment::new( IdentityType::UserBuiltin, user_builtin::USER_SERVICE_BALANCER.id, diff --git a/nexus/src/db/fixed_data/role_builtin.rs b/nexus/src/db/fixed_data/role_builtin.rs index eaa43d162b..07dcbd0d06 100644 --- a/nexus/src/db/fixed_data/role_builtin.rs +++ b/nexus/src/db/fixed_data/role_builtin.rs @@ -41,6 +41,11 @@ lazy_static! { role_name: "collaborator", description: "Organization Collaborator", }; + pub static ref SILO_ADMIN: RoleBuiltinConfig = RoleBuiltinConfig { + resource_type: api::external::ResourceType::Silo, + role_name: "admin", + description: "Silo Administrator", + }; pub static ref BUILTIN_ROLES: Vec = vec![ FLEET_ADMIN.clone(), FLEET_AUTHENTICATOR.clone(), @@ -50,11 +55,7 @@ lazy_static! { role_name: "collaborator", description: "Fleet Collaborator", }, - RoleBuiltinConfig { - resource_type: api::external::ResourceType::Silo, - role_name: "admin", - description: "Silo Administrator", - }, + SILO_ADMIN.clone(), RoleBuiltinConfig { resource_type: api::external::ResourceType::Silo, role_name: "collaborator", diff --git a/nexus/src/db/fixed_data/silo_user.rs b/nexus/src/db/fixed_data/silo_user.rs index d985b7a414..cd3f01f350 100644 --- a/nexus/src/db/fixed_data/silo_user.rs +++ b/nexus/src/db/fixed_data/silo_user.rs @@ -26,7 +26,7 @@ lazy_static! { pub static ref ROLE_ASSIGNMENTS_PRIVILEGED: Vec = vec![ // The "test-privileged" user gets the "admin" role on the sole - // Fleet. This will grant them all permissions on all resources. + // Fleet as well as the default Silo. db::model::RoleAssignment::new( db::model::IdentityType::SiloUser, USER_TEST_PRIVILEGED.id(), @@ -34,6 +34,14 @@ lazy_static! { *db::fixed_data::FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), + + db::model::RoleAssignment::new( + db::model::IdentityType::SiloUser, + USER_TEST_PRIVILEGED.id(), + role_builtin::SILO_ADMIN.resource_type, + *db::fixed_data::silo::SILO_ID, + role_builtin::SILO_ADMIN.role_name, + ), ]; /// Test user that's granted no privileges, used for automated testing diff --git a/nexus/tests/integration_tests/saml.rs b/nexus/tests/integration_tests/saml.rs index af6bc7e48c..c9a189e872 100644 --- a/nexus/tests/integration_tests/saml.rs +++ b/nexus/tests/integration_tests/saml.rs @@ -14,12 +14,17 @@ use omicron_nexus::TestInterfaces; use http::method::Method; use http::StatusCode; -use nexus_test_utils::resource_helpers::{create_silo, object_create}; +use nexus_test_utils::resource_helpers::{ + create_silo, grant_iam, object_create, +}; use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use httptest::{matchers::*, responders::*, Expectation, Server}; +use omicron_nexus::authn::USER_TEST_PRIVILEGED; +use omicron_nexus::authz::SiloRole; +use omicron_nexus::db::identity::Asset; // Valid SAML IdP entity descriptor from https://en.wikipedia.org/wiki/SAML_metadata#Identity_provider_metadata // note: no signing keys @@ -34,6 +39,14 @@ async fn test_create_a_saml_idp(cptestctx: &ControlPlaneTestContext) { const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let silo: Silo = NexusRequest::object_get(&client, &format!("/silos/{}", SILO_NAME,)) @@ -152,6 +165,14 @@ async fn test_create_a_saml_idp_invalid_descriptor_truncated( const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let saml_idp_descriptor = { let mut saml_idp_descriptor = SAML_IDP_DESCRIPTOR.to_string(); @@ -212,6 +233,14 @@ async fn test_create_a_saml_idp_invalid_descriptor_no_redirect_binding( const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let saml_idp_descriptor = { let saml_idp_descriptor = SAML_IDP_DESCRIPTOR.to_string(); @@ -282,6 +311,14 @@ async fn test_create_a_hidden_silo_saml_idp( create_silo(&client, "hidden", false, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + "/silos/hidden", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; // Valid IdP descriptor let saml_idp_descriptor = SAML_IDP_DESCRIPTOR.to_string(); @@ -351,6 +388,14 @@ async fn test_saml_idp_metadata_url_404(cptestctx: &ControlPlaneTestContext) { const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let server = Server::run(); server.expect( @@ -405,6 +450,14 @@ async fn test_saml_idp_metadata_url_invalid( const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; NexusRequest::new( RequestBuilder::new( @@ -561,6 +614,14 @@ async fn test_saml_idp_rsa_keypair_ok(cptestctx: &ControlPlaneTestContext) { const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; NexusRequest::new( RequestBuilder::new( @@ -932,6 +993,14 @@ async fn test_post_saml_response(cptestctx: &ControlPlaneTestContext) { const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Jit).await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let _silo_saml_idp: views::SamlIdentityProvider = object_create( client, @@ -1025,6 +1094,14 @@ async fn test_post_saml_response_with_relay_state( const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Jit).await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let _silo_saml_idp: views::SamlIdentityProvider = object_create( client, diff --git a/nexus/tests/integration_tests/silos.rs b/nexus/tests/integration_tests/silos.rs index 1e94970f08..96f80d5016 100644 --- a/nexus/tests/integration_tests/silos.rs +++ b/nexus/tests/integration_tests/silos.rs @@ -231,6 +231,14 @@ async fn test_silo_admin_group(cptestctx: &ControlPlaneTestContext) { }, ) .await; + grant_iam( + &client, + "/silos/silo-name", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let authn_opctx = nexus.opctx_external_authn(); @@ -507,6 +515,14 @@ async fn test_saml_idp_metadata_data_valid( create_silo(&client, "blahblah", true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + "/silos/blahblah", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let silo_saml_idp: SamlIdentityProvider = object_create( client, @@ -568,6 +584,14 @@ async fn test_saml_idp_metadata_data_truncated( create_silo(&client, "blahblah", true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + "/silos/blahblah", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; NexusRequest::new( RequestBuilder::new( @@ -621,6 +645,14 @@ async fn test_saml_idp_metadata_data_invalid( const SILO_NAME: &str = "saml-silo"; create_silo(&client, SILO_NAME, true, shared::UserProvisionType::Fixed) .await; + grant_iam( + &client, + &format!("/silos/{}", SILO_NAME), + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; NexusRequest::new( RequestBuilder::new( @@ -767,6 +799,14 @@ async fn test_silo_user_fetch_by_external_id( shared::UserProvisionType::Fixed, ) .await; + grant_iam( + &client, + "/silos/test-silo", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let opctx = OpContext::for_tests( cptestctx.logctx.log.new(o!()), @@ -1304,6 +1344,14 @@ async fn test_silo_delete_clean_up_groups(cptestctx: &ControlPlaneTestContext) { let silo = create_silo(&client, "test-silo", true, shared::UserProvisionType::Jit) .await; + grant_iam( + &client, + "/silos/test-silo", + SiloRole::Admin, + USER_TEST_PRIVILEGED.id(), + AuthnMode::PrivilegedUser, + ) + .await; let opctx = OpContext::for_tests( cptestctx.logctx.log.new(o!()), diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out index d1c584c755..d45f392f78 100644 --- a/nexus/tests/output/authz-roles.out +++ b/nexus/tests/output/authz-roles.out @@ -103,9 +103,9 @@ resource: authz::IpPoolList resource: Silo "silo1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✔ + fleet-admin ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ ✔ + fleet-collaborator ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ ✔ + fleet-viewer ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ ✔ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔ silo1-collaborator ✘ ✔ ✔ ✔ ✘ ✘ ✔ ✘ ✔ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✔ @@ -120,9 +120,9 @@ resource: Silo "silo1" resource: Organization "silo1-org1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -137,9 +137,9 @@ resource: Organization "silo1-org1" resource: Project "silo1-org1-proj1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -154,9 +154,9 @@ resource: Project "silo1-org1-proj1" resource: Disk "silo1-org1-proj1-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -171,9 +171,9 @@ resource: Disk "silo1-org1-proj1-disk1" resource: Instance "silo1-org1-proj1-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -188,9 +188,9 @@ resource: Instance "silo1-org1-proj1-instance1" resource: NetworkInterface "silo1-org1-proj1-instance1-nic1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -205,9 +205,9 @@ resource: NetworkInterface "silo1-org1-proj1-instance1-nic1" resource: Vpc "silo1-org1-proj1-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -222,9 +222,9 @@ resource: Vpc "silo1-org1-proj1-vpc1" resource: VpcSubnet "silo1-org1-proj1-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -239,9 +239,9 @@ resource: VpcSubnet "silo1-org1-proj1-vpc1-subnet1" resource: Project "silo1-org1-proj2" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -256,9 +256,9 @@ resource: Project "silo1-org1-proj2" resource: Disk "silo1-org1-proj2-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -273,9 +273,9 @@ resource: Disk "silo1-org1-proj2-disk1" resource: Instance "silo1-org1-proj2-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -290,9 +290,9 @@ resource: Instance "silo1-org1-proj2-instance1" resource: NetworkInterface "silo1-org1-proj2-instance1-nic1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -307,9 +307,9 @@ resource: NetworkInterface "silo1-org1-proj2-instance1-nic1" resource: Vpc "silo1-org1-proj2-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -324,9 +324,9 @@ resource: Vpc "silo1-org1-proj2-vpc1" resource: VpcSubnet "silo1-org1-proj2-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -341,9 +341,9 @@ resource: VpcSubnet "silo1-org1-proj2-vpc1-subnet1" resource: Organization "silo1-org2" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -358,9 +358,9 @@ resource: Organization "silo1-org2" resource: Project "silo1-org2-proj1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -375,9 +375,9 @@ resource: Project "silo1-org2-proj1" resource: Disk "silo1-org2-proj1-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -392,9 +392,9 @@ resource: Disk "silo1-org2-proj1-disk1" resource: Instance "silo1-org2-proj1-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -409,9 +409,9 @@ resource: Instance "silo1-org2-proj1-instance1" resource: NetworkInterface "silo1-org2-proj1-instance1-nic1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -426,9 +426,9 @@ resource: NetworkInterface "silo1-org2-proj1-instance1-nic1" resource: Vpc "silo1-org2-proj1-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -443,9 +443,9 @@ resource: Vpc "silo1-org2-proj1-vpc1" resource: VpcSubnet "silo1-org2-proj1-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ @@ -460,9 +460,9 @@ resource: VpcSubnet "silo1-org2-proj1-vpc1-subnet1" resource: Silo "silo2" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ ✔ + fleet-collaborator ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ ✔ + fleet-viewer ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ ✔ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -477,9 +477,9 @@ resource: Silo "silo2" resource: Organization "silo2-org1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -494,9 +494,9 @@ resource: Organization "silo2-org1" resource: Project "silo2-org1-proj1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -511,9 +511,9 @@ resource: Project "silo2-org1-proj1" resource: Disk "silo2-org1-proj1-disk1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -528,9 +528,9 @@ resource: Disk "silo2-org1-proj1-disk1" resource: Instance "silo2-org1-proj1-instance1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -545,9 +545,9 @@ resource: Instance "silo2-org1-proj1-instance1" resource: NetworkInterface "silo2-org1-proj1-instance1-nic1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -562,9 +562,9 @@ resource: NetworkInterface "silo2-org1-proj1-instance1-nic1" resource: Vpc "silo2-org1-proj1-vpc1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ @@ -579,9 +579,9 @@ resource: Vpc "silo2-org1-proj1-vpc1" resource: VpcSubnet "silo2-org1-proj1-vpc1-subnet1" USER Q R LC RP M MP CC D LP - fleet-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✘ - fleet-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ From c0958cbcc81e6ce5c126221cd7862c6fb1bbaf1c Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 15:21:53 -0700 Subject: [PATCH 30/32] fix style --- nexus/src/authz/policy_test/resources.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/nexus/src/authz/policy_test/resources.rs b/nexus/src/authz/policy_test/resources.rs index 1dd9bc8f94..aab79b057a 100644 --- a/nexus/src/authz/policy_test/resources.rs +++ b/nexus/src/authz/policy_test/resources.rs @@ -113,13 +113,8 @@ async fn make_silo( for i in 0..norganizations { let organization_name = format!("{}-org{}", silo_name, i + 1); let org_first_branch = first_branch && i == 0; - make_organization( - builder, - &silo, - &organization_name, - org_first_branch, - ) - .await; + make_organization(builder, &silo, &organization_name, org_first_branch) + .await; } } From fb3a0bd0e76c56b3712bab9e1a48fe4d57c75fd1 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 15:23:41 -0700 Subject: [PATCH 31/32] remove unused service balancer role --- nexus/src/db/fixed_data/role_assignment.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/nexus/src/db/fixed_data/role_assignment.rs b/nexus/src/db/fixed_data/role_assignment.rs index 7ca398b811..43635518c9 100644 --- a/nexus/src/db/fixed_data/role_assignment.rs +++ b/nexus/src/db/fixed_data/role_assignment.rs @@ -24,14 +24,6 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), - // XXX-dap remove this one? - RoleAssignment::new( - IdentityType::UserBuiltin, - user_builtin::USER_SERVICE_BALANCER.id, - role_builtin::FLEET_ADMIN.resource_type, - *FLEET_ID, - role_builtin::FLEET_ADMIN.role_name, - ), // The "internal-read" user gets the "viewer" role on the sole // Fleet. This will grant them the ability to read various control From 06dbe494dab1aa79d63336e7cc155ea24347227d Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 11 Aug 2022 15:39:08 -0700 Subject: [PATCH 32/32] fix test --- nexus/src/app/silo.rs | 9 ++++++++- nexus/src/db/datastore/silo.rs | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index 52fe72ec8b..dceb83711b 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -31,7 +31,14 @@ impl super::Nexus { opctx: &OpContext, new_silo_params: params::SiloCreate, ) -> CreateResult { - self.datastore().silo_create(&opctx, new_silo_params).await + // Silo group creation happens as Nexus's "external authn" context, + // not the user's context here. The user may not have permission to + // create arbitrary groups in the Silo, but we allow them to create + // this one in this case. + let external_authn_opctx = self.opctx_external_authn(); + self.datastore() + .silo_create(&opctx, &external_authn_opctx, new_silo_params) + .await } pub async fn silos_list_by_name( diff --git a/nexus/src/db/datastore/silo.rs b/nexus/src/db/datastore/silo.rs index 491e9c4a44..96324b8350 100644 --- a/nexus/src/db/datastore/silo.rs +++ b/nexus/src/db/datastore/silo.rs @@ -71,6 +71,7 @@ impl DataStore { pub async fn silo_create( &self, opctx: &OpContext, + group_opctx: &OpContext, new_silo_params: params::SiloCreate, ) -> CreateResult { let silo_id = Uuid::new_v4(); @@ -90,7 +91,7 @@ impl DataStore { { let silo_admin_group_ensure_query = DataStore::silo_group_ensure_query( - opctx, + &group_opctx, &authz_silo, db::model::SiloGroup::new( silo_group_id,