diff --git a/nexus/src/authz/api_resources.rs b/nexus/src/authz/api_resources.rs index 915b98b19b5..2f86a7425a1 100644 --- a/nexus/src/authz/api_resources.rs +++ b/nexus/src/authz/api_resources.rs @@ -391,6 +391,7 @@ impl ApiResourceWithRolesType for Organization { pub enum OrganizationRoles { Admin, Collaborator, + Viewer, } impl db::model::DatabaseString for OrganizationRoles { @@ -400,6 +401,7 @@ impl db::model::DatabaseString for OrganizationRoles { match self { OrganizationRoles::Admin => "admin", OrganizationRoles::Collaborator => "collaborator", + OrganizationRoles::Viewer => "viewer", } } @@ -407,6 +409,7 @@ impl db::model::DatabaseString for OrganizationRoles { match s { "admin" => Ok(OrganizationRoles::Admin), "collaborator" => Ok(OrganizationRoles::Collaborator), + "viewer" => Ok(OrganizationRoles::Viewer), _ => Err(anyhow!( "unsupported Organization role from database: {:?}", s diff --git a/nexus/src/authz/omicron.polar b/nexus/src/authz/omicron.polar index 35829817295..98afd891bdf 100644 --- a/nexus/src/authz/omicron.polar +++ b/nexus/src/authz/omicron.polar @@ -67,6 +67,7 @@ has_role(actor: AuthenticatedActor, role: String, resource: Resource) # - silo.viewer (can read most resources within the Silo) # - organization.admin (complete control over an organization) # - organization.collaborator (can manage Projects) +# - organization.viewer (can read most resources within the Organization) # - project.admin (complete control over a Project) # - project.collaborator (can manage all resources within the Project) # - project.viewer (can read most resources within the Project) @@ -164,22 +165,22 @@ resource Organization { "read", "create_child", ]; - roles = [ "admin", "collaborator" ]; + roles = [ "admin", "collaborator", "viewer" ]; # Roles implied by other roles on this resource + "viewer" if "collaborator"; "collaborator" if "admin"; # Permissions granted directly by roles on this resource - "list_children" if "collaborator"; - "read" if "collaborator"; + "list_children" if "viewer"; + "read" if "viewer"; "create_child" if "collaborator"; "modify" if "admin"; # Roles implied by roles on this resource's parent (Silo) relations = { parent_silo: Silo }; "admin" if "collaborator" on "parent_silo"; - "read" if "viewer" on "parent_silo"; - "list_children" if "viewer" on "parent_silo"; + "viewer" if "viewer" on "parent_silo"; } has_relation(silo: Silo, "parent_silo", organization: Organization) if organization.silo = silo; @@ -206,7 +207,7 @@ resource Project { # Roles implied by roles on this resource's parent (Organization) relations = { parent_organization: Organization }; "admin" if "collaborator" on "parent_organization"; - "viewer" if "list_children" on "parent_organization"; + "viewer" if "viewer" on "parent_organization"; } has_relation(organization: Organization, "parent_organization", project: Project) if project.organization = organization; @@ -253,7 +254,7 @@ has_relation(user: SiloUser, "silo_user", ssh_key: SshKey) # of the API path (e.g., "/images") or as an implementation detail of the system # (in the case of console sessions and "Database"). The policies are # either statically-defined in this file or driven by role assignments on the -# Fleet. +# Fleet. None of these resources defines their own roles. # # Describes the policy for accessing "/images" (in the API) @@ -320,25 +321,11 @@ resource Database { # other general functions. "modify" ]; - roles = [ - # All authenticated users get the "user" role, which grants the - # "query" permission. See above. - "user", - - # The special "db-init" user gets the "init" role, which grants the - # additional "modify" permission. - "init" - ]; - - # See above. - "query" if "user"; - - "user" if "init"; - "modify" if "init"; } -# All authenticated users have the "user" role on the database. -has_role(_actor: AuthenticatedActor, "user", _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_role(actor: AuthenticatedActor, "init", _resource: Database) +has_permission(actor: AuthenticatedActor, "modify", _resource: Database) if actor = USER_DB_INIT; diff --git a/nexus/src/db/fixed_data/role_builtin.rs b/nexus/src/db/fixed_data/role_builtin.rs index 8522bafd658..33c64ea7a1d 100644 --- a/nexus/src/db/fixed_data/role_builtin.rs +++ b/nexus/src/db/fixed_data/role_builtin.rs @@ -6,7 +6,7 @@ use lazy_static::lazy_static; use omicron_common::api; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct RoleBuiltinConfig { pub resource_type: api::external::ResourceType, pub role_name: &'static str, @@ -67,6 +67,11 @@ lazy_static! { }, ORGANIZATION_ADMINISTRATOR.clone(), ORGANIZATION_COLLABORATOR.clone(), + RoleBuiltinConfig { + resource_type: api::external::ResourceType::Organization, + role_name: "viewer", + description: "Organization Viewer", + }, RoleBuiltinConfig { resource_type: api::external::ResourceType::Project, role_name: "admin", @@ -84,3 +89,52 @@ lazy_static! { }, ]; } + +#[cfg(test)] +mod test { + use super::BUILTIN_ROLES; + use crate::authz; + use crate::db::model::DatabaseString; + use omicron_common::api::external::ResourceType; + use strum::IntoEnumIterator; + + #[test] + fn test_fixed_role_data() { + // Every role that's defined in the public API as assignable on a + // resource must have a corresponding entry in BUILTIN_ROLES above. + // The reverse is not necessarily true because we have some internal + // roles that are not exposed to end users. + check_public_roles::(ResourceType::Fleet); + check_public_roles::(ResourceType::Silo); + check_public_roles::( + ResourceType::Organization, + ); + check_public_roles::(ResourceType::Project); + } + + fn check_public_roles(resource_type: ResourceType) + where + T: std::fmt::Debug + DatabaseString + IntoEnumIterator, + { + for variant in T::iter() { + let role_name = variant.to_database_string(); + + let found = BUILTIN_ROLES.iter().find(|role_config| { + role_config.resource_type == resource_type + && role_config.role_name == role_name + }); + if let Some(found_config) = found { + println!( + "variant: {:?} found fixed data {:?}", + variant, found_config + ); + } else { + panic!( + "found public role {:?} on {:?} with no corresponding \ + built-in role", + role_name, resource_type + ); + } + } + } +} diff --git a/nexus/tests/integration_tests/roles_builtin.rs b/nexus/tests/integration_tests/roles_builtin.rs index 3e29f3211c5..2f6ce82827c 100644 --- a/nexus/tests/integration_tests/roles_builtin.rs +++ b/nexus/tests/integration_tests/roles_builtin.rs @@ -34,6 +34,7 @@ async fn test_roles_builtin(cptestctx: &ControlPlaneTestContext) { ("fleet.viewer", "Fleet Viewer"), ("organization.admin", "Organization Administrator"), ("organization.collaborator", "Organization Collaborator"), + ("organization.viewer", "Organization Viewer"), ("project.admin", "Project Administrator"), ("project.collaborator", "Project Collaborator"), ("project.viewer", "Project Viewer"), diff --git a/nexus/tests/output/authz-roles-organization.txt b/nexus/tests/output/authz-roles-organization.txt index 3ce6f774599..2d9076f78be 100644 --- a/nexus/tests/output/authz-roles-organization.txt +++ b/nexus/tests/output/authz-roles-organization.txt @@ -1,2 +1,3 @@ variant Admin: serialized form = admin variant Collaborator: serialized form = collaborator +variant Viewer: serialized form = viewer diff --git a/openapi/nexus.json b/openapi/nexus.json index 066c0aa399e..2f0cbb242c2 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -6643,7 +6643,8 @@ "type": "string", "enum": [ "admin", - "collaborator" + "collaborator", + "viewer" ] }, "OrganizationRolesPolicy": {