diff --git a/Cargo.lock b/Cargo.lock index 29c9b29d6bb..b6c3803539d 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", diff --git a/nexus/src/authn/mod.rs b/nexus/src/authn/mod.rs index 0a2712725c9..4a6428e88c3 100644 --- a/nexus/src/authn/mod.rs +++ b/nexus/src/authn/mod.rs @@ -204,15 +204,15 @@ impl Context { /// (for testing only) #[cfg(test)] pub fn unprivileged_test_user() -> Context { - Self::test_silo_user( - USER_TEST_UNPRIVILEGED.silo_id, + Context::for_test_user( USER_TEST_UNPRIVILEGED.id(), + USER_TEST_UNPRIVILEGED.silo_id, ) } - /// Returns an authenticated context for a given silo user + /// Returns an authenticated context for the specific Silo user. #[cfg(test)] - pub fn test_silo_user(silo_id: Uuid, silo_user_id: Uuid) -> Context { + pub fn for_test_user(silo_user_id: Uuid, silo_id: Uuid) -> Context { Context { kind: Kind::Authenticated(Details { actor: Actor::SiloUser { silo_user_id, silo_id }, diff --git a/nexus/src/authz/api_resources.rs b/nexus/src/authz/api_resources.rs index d0428b69b03..2ed038a128c 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 bc024dc4f87..e95c2cbb017 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 @@ -81,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 @@ -97,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: {:?}", @@ -181,23 +186,17 @@ 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)] mod test { - // These are essentially unit tests for the policy itself. - // 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; @@ -212,102 +211,6 @@ mod test { 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(); - } - #[tokio::test] async fn test_unregistered_resource() { let logctx = dev::test_setup_log("test_unregistered_resource"); @@ -353,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/mod.rs b/nexus/src/authz/mod.rs index 3697b324df7..8fb7211ae62 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/omicron.polar b/nexus/src/authz/omicron.polar index 11dbd368ec8..733b918ada1 100644 --- a/nexus/src/authz/omicron.polar +++ b/nexus/src/authz/omicron.polar @@ -456,8 +456,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; -has_permission(actor: AuthenticatedActor, "create_child", _resource: IpPoolList) - if actor = USER_DB_INIT; +# The "db-init" user is the only one with the "modify" permission. +has_permission(USER_DB_INIT: AuthenticatedActor, "modify", _resource: Database); +has_permission(USER_DB_INIT: AuthenticatedActor, "create_child", _resource: IpPoolList); diff --git a/nexus/src/authz/oso_generic.rs b/nexus/src/authz/oso_generic.rs index b91715401d6..79dd23a5e46 100644 --- a/nexus/src/authz/oso_generic.rs +++ b/nexus/src/authz/oso_generic.rs @@ -156,15 +156,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, ListIdentityProviders, // only used during [`Nexus::identity_provider_list`] } @@ -288,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/coverage.rs b/nexus/src/authz/policy_test/coverage.rs new file mode 100644 index 00000000000..cb9140c7f46 --- /dev/null +++ b/nexus/src/authz/policy_test/coverage.rs @@ -0,0 +1,94 @@ +// 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 crate::authz::AuthorizedResource; +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, exempted: BTreeSet) -> Coverage { + let log = log.new(o!("component" => "IamTestCoverage")); + 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(); + + 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) => { + warn!(&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 00000000000..e02a07c1ce3 --- /dev/null +++ b/nexus/src/authz/policy_test/mod.rs @@ -0,0 +1,276 @@ +// 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 +//! +//! 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. + +mod coverage; +mod resource_builder; +mod resources; + +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_test_utils::dev; +use resource_builder::DynAuthorizedResource; +use resource_builder::ResourceBuilder; +use resource_builder::ResourceSet; +use std::io::Cursor; +use std::io::Write; +use std::sync::Arc; +use strum::IntoEnumIterator; +use uuid::Uuid; + +/// 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 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; + coverage.verify(); + + // 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() + .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, main_silo_id), + Arc::clone(&datastore), + ); + + Arc::new((username.clone(), opctx)) + }) + .collect(); + + // Create and test an unauthenticated OpContext as well. + // + // 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. + 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), + ), + ))); + + // 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); + authorize_everything( + &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(); +} + +/// 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: &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 + // to emit the results in a consistent order. + let mut futures = futures::stream::FuturesOrdered::new(); + for resource in test_resources.resources() { + let log = log.new(o!("resource" => format!("{:?}", resource))); + futures.push(authorize_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(()) +} + +/// 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, +) -> 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") +} + +/// Return the column header used for each action +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/resource_builder.rs b/nexus/src/authz/policy_test/resource_builder.rs new file mode 100644 index 00000000000..e9fb26bbf6f --- /dev/null +++ b/nexus/src/authz/policy_test/resource_builder.rs @@ -0,0 +1,243 @@ +// 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 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.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() { + 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) + } +} + +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() + } + } + }; +} + +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 new file mode 100644 index 00000000000..eeefe6620a5 --- /dev/null +++ b/nexus/src/authz/policy_test/resources.rs @@ -0,0 +1,262 @@ +// 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/. + +//! Concrete list of resources created for the IAM policy test + +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 +// 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 { + // 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()); + 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()); + + // 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() +} + +/// 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()), + ); + + 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(instance.clone()); + builder.new_resource(authz::NetworkInterface::new( + instance, + Uuid::new_v4(), + LookupType::ByName(format!("{}-nic1", instance_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)), + )); +} + +/// 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 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) + // + // 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::IpPool::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::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::UpdateAvailableArtifact::get_polar_class(), + authz::UserBuiltin::get_polar_class(), + authz::GlobalImage::get_polar_class(), + ] + .into_iter() + .map(|c| c.name.clone()) + .collect() +} diff --git a/nexus/src/context.rs b/nexus/src/context.rs index d4a26798938..231f235a2ff 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/mod.rs b/nexus/src/db/datastore/mod.rs index eed3b123e5d..22d72c6a0e8 100644 --- a/nexus/src/db/datastore/mod.rs +++ b/nexus/src/db/datastore/mod.rs @@ -401,7 +401,7 @@ mod test { let silo_user_opctx = OpContext::for_background( logctx.log.new(o!()), Arc::new(authz::Authz::new(&logctx.log)), - authn::Context::test_silo_user(*SILO_ID, silo_user_id), + authn::Context::for_test_user(silo_user_id, *SILO_ID), Arc::clone(&datastore), ); let delete = datastore diff --git a/nexus/src/db/datastore/role.rs b/nexus/src/db/datastore/role.rs index f3b4fd99821..03ce9ecb25e 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?; diff --git a/nexus/tests/output/authz-roles.out b/nexus/tests/output/authz-roles.out new file mode 100644 index 00000000000..d1c584c7550 --- /dev/null +++ b/nexus/tests/output/authz-roles.out @@ -0,0 +1,641 @@ +resource: authz::oso_generic::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 ✔ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + 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 + 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 ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 + 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 ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘ + 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 + R = Read + LC = ListChildren + RP = ReadPolicy + M = Modify + MP = ModifyPolicy + CC = CreateChild + D = Delete + LP = ListIdentityProviders +