Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions nexus/auth/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,61 @@ impl AuthorizedResource for SiloGroupList {
}
}

/// Synthetic resource describing the list of Silo Images associated with a Silo
///
/// This synthetic resource is used to control who can promote project images to
/// silo images. By using a synthetic resource, we can grant limited-collaborators
/// the ability to create silo images (via promotion) without giving them the
/// broader create_child permission on Silo (which would allow creating projects,
/// users, groups, etc.).
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SiloImageList(Silo);

impl SiloImageList {
pub fn new(silo: Silo) -> Self {
SiloImageList(silo)
}

pub fn silo(&self) -> &Silo {
&self.0
}
}

impl oso::PolarClass for SiloImageList {
fn get_polar_class_builder() -> oso::ClassBuilder<Self> {
oso::Class::builder()
.with_equality_check()
.add_attribute_getter("silo", |list: &SiloImageList| list.0.clone())
}
}

impl AuthorizedResource for SiloImageList {
fn load_roles<'fut>(
&'fut self,
opctx: &'fut OpContext,
authn: &'fut authn::Context,
roleset: &'fut mut RoleSet,
) -> futures::future::BoxFuture<'fut, Result<(), Error>> {
// There are no roles on this resource, but we still need to load the
// Silo-related roles.
self.silo().load_roles(opctx, authn, roleset)
}

fn on_unauthorized(
&self,
_: &Authz,
error: Error,
_: AnyActor,
_: Action,
) -> Error {
error
}

fn polar_class(&self) -> oso::Class {
Self::get_polar_class()
}
}

// Note the session list and the token list have exactly the same behavior

/// Synthetic resource for managing a user's sessions
Expand Down
29 changes: 29 additions & 0 deletions nexus/auth/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -825,3 +825,32 @@ resource VpcList {
}
has_relation(project: Project, "containing_project", collection: VpcList)
if collection.project = project;

# SiloImageList is a synthetic resource for controlling silo image creation.
# Unlike other silo resources, silo image creation (promotion from project images)
# should be allowed for limited-collaborators, since they need full image management
# capabilities while being restricted from VPC operations.
# This allows organizations to give users full control over images (create, promote,
# demote) while restricting network configuration.
resource SiloImageList {
permissions = [ "list_children", "create_child" ];

relations = { containing_silo: Silo };

"list_children" if "viewer" on "containing_silo";
"create_child" if "limited-collaborator" on "containing_silo";
}
has_relation(silo: Silo, "containing_silo", collection: SiloImageList)
if collection.silo = silo;

# SiloImage modifications for limited-collaborator
# By default, SiloImage uses the InSilo pattern where only "collaborator" can
# modify. We extend this to also allow "limited-collaborator" to modify silo
# images (specifically for demotion). Limited-collaborator is restricted from
# VPC operations but should have full image management capabilities.
#
# Note: If more silo-level resources need limited-collaborator access in the
# future, consider creating InSiloLimited and InSiloFull macro patterns,
# similar to InProjectLimited and InProjectFull.
has_permission(actor: Actor, "modify", silo_image: SiloImage) if
has_role(actor, "limited-collaborator", silo_image.silo);
1 change: 1 addition & 0 deletions nexus/auth/src/authz/oso_generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<OsoInit, anyhow::Error> {
QuiesceState::get_polar_class(),
SiloCertificateList::get_polar_class(),
SiloGroupList::get_polar_class(),
SiloImageList::get_polar_class(),
SiloIdentityProviderList::get_polar_class(),
SiloUserList::get_polar_class(),
SiloUserSessionList::get_polar_class(),
Expand Down
9 changes: 8 additions & 1 deletion nexus/db-queries/src/db/datastore/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,14 @@ impl DataStore {
authz_project_image: &authz::ProjectImage,
project_image: &ProjectImage,
) -> UpdateResult<Image> {
opctx.authorize(authz::Action::CreateChild, authz_silo).await?;
// Check if the user can create silo images (promote from project images).
// We use SiloImageList to allow limited-collaborators to promote images
// without granting them the broader create_child permission on Silo.
let authz_silo_image_list =
authz::SiloImageList::new(authz_silo.clone());
opctx
.authorize(authz::Action::CreateChild, &authz_silo_image_list)
.await?;
opctx.authorize(authz::Action::Modify, authz_project_image).await?;

use nexus_db_schema::schema::image::dsl;
Expand Down
17 changes: 17 additions & 0 deletions nexus/db-queries/src/policy_test/resource_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,3 +435,20 @@ impl DynAuthorizedResource for authz::VpcList {
format!("{}: vpc list", self.project().resource_name())
}
}

impl DynAuthorizedResource for authz::SiloImageList {
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 {
format!("{}: silo image list", self.silo().resource_name())
}
}
2 changes: 2 additions & 0 deletions nexus/db-queries/src/policy_test/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ async fn make_silo(
LookupType::ByName(format!("{}-certificate", silo_name)),
));

builder.new_resource(authz::SiloImageList::new(silo.clone()));

builder.new_resource(authz::SiloIdentityProviderList::new(silo.clone()));
let idp_id = Uuid::new_v4();
builder.new_resource(authz::IdentityProvider::new(
Expand Down
36 changes: 35 additions & 1 deletion nexus/db-queries/tests/output/authz-roles.out
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,23 @@ resource: Certificate "silo1-certificate"
unauthenticated ! ! ! ! ! ! ! !
scim ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘

resource: Silo "silo1": silo image list

USER Q R LC RP M MP CC D
fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-admin ✘ ✘ ✔ ✘ ✘ ✘ ✔ ✘
silo1-collaborator ✘ ✘ ✔ ✘ ✘ ✘ ✔ ✘
silo1-limited-collaborator ✘ ✘ ✔ ✘ ✘ ✘ ✔ ✘
silo1-viewer ✘ ✘ ✔ ✘ ✘ ✘ ✘ ✘
silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-proj1-limited-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
unauthenticated ! ! ! ! ! ! ! !
scim ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘

resource: Silo "silo1": identity provider list

USER Q R LC RP M MP CC D
Expand Down Expand Up @@ -414,7 +431,7 @@ resource: SiloImage "silo1-silo-image"
fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔
silo1-collaborator ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔
silo1-limited-collaborator ✘ ✔ ✔ ✔ ✘ ✘
silo1-limited-collaborator ✘ ✔ ✔ ✔ ✔ ✔
silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘
silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
Expand Down Expand Up @@ -1069,6 +1086,23 @@ resource: Certificate "silo2-certificate"
unauthenticated ! ! ! ! ! ! ! !
scim ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘

resource: Silo "silo2": silo image list

USER Q R LC RP M MP CC D
fleet-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
fleet-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
fleet-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-limited-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-proj1-admin ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-proj1-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-proj1-limited-collaborator ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
silo1-proj1-viewer ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘
unauthenticated ! ! ! ! ! ! ! !
scim ✘ ✘ ✘ ✘ ✘ ✘ ✘ ✘

resource: Silo "silo2": identity provider list

USER Q R LC RP M MP CC D
Expand Down
11 changes: 6 additions & 5 deletions nexus/src/app/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,7 @@ impl super::Nexus {
ImageLookup::ProjectImage(lookup) => {
let (authz_silo, _, authz_project_image, project_image) =
lookup.fetch_for(authz::Action::Modify).await?;
opctx
.authorize(authz::Action::CreateChild, &authz_silo)
.await?;

self.db_datastore
.project_image_promote(
opctx,
Expand All @@ -217,8 +215,11 @@ impl super::Nexus {
ImageLookup::SiloImage(lookup) => {
let (_, authz_silo_image, silo_image) =
lookup.fetch_for(authz::Action::Modify).await?;
let (_, authz_project) =
project_lookup.lookup_for(authz::Action::Modify).await?;
// Check CreateChild on the project since we're creating a ProjectImage.
// This allows limited-collaborators to demote images.
let (_, authz_project) = project_lookup
.lookup_for(authz::Action::CreateChild)
.await?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Guess this was wrong before.

self.db_datastore
.silo_image_demote(
opctx,
Expand Down
Loading
Loading