Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
41 changes: 34 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,8 @@ newtype-uuid = { version = "1.3.1", default-features = false }
newtype-uuid-macros = "0.1.0"
omicron-uuid-kinds = { path = "uuid-kinds", features = ["serde", "schemars08", "uuid-v4"] }

scim2-rs = { git = "https://github.com/oxidecomputer/scim2-rs" }
scim2-rs = { git = "https://github.com/oxidecomputer/scim2-rs", rev = "c78005db837a71f94c3b8efac9a64cfbdb2d527f" }
scim2-test-client = { git = "https://github.com/oxidecomputer/scim2-rs", rev = "c78005db837a71f94c3b8efac9a64cfbdb2d527f" }

# NOTE: The test profile inherits from the dev profile, so settings under
# profile.dev get inherited. AVOID setting anything under profile.test: that
Expand Down
1 change: 1 addition & 0 deletions nexus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ pretty_assertions.workspace = true
rcgen.workspace = true
regex.workspace = true
rustls.workspace = true
scim2-test-client.workspace = true
similar-asserts.workspace = true
sp-sim.workspace = true
strum.workspace = true
Expand Down
12 changes: 12 additions & 0 deletions nexus/auth/src/authz/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ impl oso::PolarClass for AuthenticatedActor {
authn::Actor::Scim { .. } => false,
}
})
// Like the "is_user" guard above but reversed, this guard is used
// in the Polar file to grant permissions to a SCIM IdP actor
// without the need for a role.
.add_attribute_getter("is_scim_idp", |a: &AuthenticatedActor| {
match a.actor {
authn::Actor::SiloUser { .. } => false,

authn::Actor::UserBuiltin { .. } => false,

authn::Actor::Scim { .. } => true,
}
})
.add_attribute_getter("silo", |a: &AuthenticatedActor| {
match a.actor {
authn::Actor::SiloUser { silo_id, .. }
Expand Down
49 changes: 49 additions & 0 deletions nexus/auth/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,55 @@ impl AuthorizedResource for SiloUserList {
}
}

/// Synthetic resource describing the list of Groups in a Silo
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SiloGroupList(Silo);

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

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

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

impl AuthorizedResource for SiloGroupList {
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
51 changes: 50 additions & 1 deletion nexus/auth/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,11 @@ resource SshKey {
relations = { silo_user: SiloUser };

"read" if "read" on "silo_user";
"modify" if "modify" on "silo_user";
}
# We want to allow the user to modify the ssh key but disallow a SCIM IdP token
# from doing the same.
has_permission(actor: AuthenticatedActor, "modify", ssh_key: SshKey)
if actor.is_user and has_permission(actor, "modify", ssh_key.silo_user);
has_relation(user: SiloUser, "silo_user", ssh_key: SshKey)
if ssh_key.silo_user = user;

Expand Down Expand Up @@ -630,6 +633,52 @@ has_relation(silo: Silo, "parent_silo", collection: SiloUserList)
has_relation(fleet: Fleet, "parent_fleet", collection: SiloUserList)
if collection.silo.fleet = fleet;

# Grant SCIM IdP actors the permissions they need on users.
has_permission(actor: AuthenticatedActor, "read", silo_user: SiloUser)
if actor.is_scim_idp and silo_user.silo in actor.silo;
has_permission(actor: AuthenticatedActor, "create_child", silo_user_list: SiloUserList)
if actor.is_scim_idp and silo_user_list.silo in actor.silo;
has_permission(actor: AuthenticatedActor, "modify", silo_user: SiloUser)
if actor.is_scim_idp and silo_user.silo in actor.silo;
has_permission(actor: AuthenticatedActor, "list_children", silo_user_list: SiloUserList)
if actor.is_scim_idp and silo_user_list.silo in actor.silo;

# Describes the policy for creating and managing Silo groups (mostly intended
# for API-managed groups)
resource SiloGroupList {
permissions = [ "list_children", "create_child" ];

relations = { parent_silo: Silo, parent_fleet: Fleet };

# Everyone who can read the Silo (which includes all the groups in the
# Silo) can see the groups in it.
"list_children" if "read" on "parent_silo";

# Fleet and Silo administrators can manage the Silo's groups. This is
# one of the only areas of Silo configuration that Fleet Administrators
# have permissions on. This is also one of the few cases (so far) where
# we need to look two levels up the hierarchy to see if somebody has the
# right permission. For most other things, permissions cascade down the
# hierarchy so we only need to look at the parent.
"create_child" if "admin" on "parent_silo";
"list_children" if "admin" on "parent_fleet";
"create_child" if "admin" on "parent_fleet";
}
has_relation(silo: Silo, "parent_silo", collection: SiloGroupList)
if collection.silo = silo;
has_relation(fleet: Fleet, "parent_fleet", collection: SiloGroupList)
if collection.silo.fleet = fleet;

# Grant SCIM IdP actors the permissions they need on groups.
has_permission(actor: AuthenticatedActor, "read", silo_group: SiloGroup)
if actor.is_scim_idp and silo_group.silo in actor.silo;
has_permission(actor: AuthenticatedActor, "create_child", silo_group_list: SiloGroupList)
if actor.is_scim_idp and silo_group_list.silo in actor.silo;
has_permission(actor: AuthenticatedActor, "modify", silo_group: SiloGroup)
if actor.is_scim_idp and silo_group.silo in actor.silo;
has_permission(actor: AuthenticatedActor, "list_children", silo_group_list: SiloGroupList)
if actor.is_scim_idp and silo_group_list.silo in actor.silo;

# These rules grants the external authenticator role the permissions it needs to
# read silo users and modify their sessions. This is necessary for login to
# work.
Expand Down
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 @@ -114,6 +114,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<OsoInit, anyhow::Error> {
DeviceAuthRequestList::get_polar_class(),
QuiesceState::get_polar_class(),
SiloCertificateList::get_polar_class(),
SiloGroupList::get_polar_class(),
SiloIdentityProviderList::get_polar_class(),
SiloUserList::get_polar_class(),
SiloUserSessionList::get_polar_class(),
Expand Down
3 changes: 2 additions & 1 deletion nexus/db-model/src/schema_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
///
/// This must be updated when you change the database schema. Refer to
/// schema/crdb/README.adoc in the root of this repository for details.
pub const SCHEMA_VERSION: Version = Version::new(201, 0, 0);
pub const SCHEMA_VERSION: Version = Version::new(202, 0, 0);

/// List of all past database schema versions, in *reverse* order
///
Expand All @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
// | leaving the first copy as an example for the next person.
// v
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
KnownVersion::new(202, "scim-actor-audit-log"),
KnownVersion::new(201, "scim-client-bearer-token"),
KnownVersion::new(200, "dual-stack-network-interfaces"),
KnownVersion::new(199, "multicast-pool-support"),
Expand Down
Loading
Loading