Skip to content

Commit c787f64

Browse files
jmpesppapertigers
andauthored
[SCIM 4/4]: Draw the rest of the owl (#9276)
The majority of this PR fills out the CrdbScimProviderStore implementation that the scim2-rs crate's Provider object will use to implement SCIM where CRDB is the durable store for users and groups, and adds a boat load of integration tests. The notion of a silo user being "active" or not has also been added to support users being deactivated by the SCIM client. Non-SCIM silo users should not be affected. --------- Co-authored-by: Mike Zeller <[email protected]>
1 parent 49305d8 commit c787f64

File tree

26 files changed

+3573
-91
lines changed

26 files changed

+3573
-91
lines changed

Cargo.lock

Lines changed: 33 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,8 @@ newtype-uuid = { version = "1.3.1", default-features = false }
803803
newtype-uuid-macros = "0.1.0"
804804
omicron-uuid-kinds = { path = "uuid-kinds", features = ["serde", "schemars08", "uuid-v4"] }
805805

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

808809
# NOTE: The test profile inherits from the dev profile, so settings under
809810
# profile.dev get inherited. AVOID setting anything under profile.test: that

nexus/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ pretty_assertions.workspace = true
177177
rcgen.workspace = true
178178
regex.workspace = true
179179
rustls.workspace = true
180+
scim2-test-client.workspace = true
180181
similar-asserts.workspace = true
181182
sp-sim.workspace = true
182183
strum.workspace = true

nexus/auth/src/authz/actor.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ impl oso::PolarClass for AuthenticatedActor {
134134
authn::Actor::Scim { .. } => false,
135135
}
136136
})
137+
// Like the "is_user" guard above but reversed, this guard is used
138+
// in the Polar file to grant permissions to a SCIM IdP actor
139+
// without the need for a role.
140+
.add_attribute_getter("is_scim_idp", |a: &AuthenticatedActor| {
141+
match a.actor {
142+
authn::Actor::SiloUser { .. } => false,
143+
144+
authn::Actor::UserBuiltin { .. } => false,
145+
146+
authn::Actor::Scim { .. } => true,
147+
}
148+
})
137149
.add_attribute_getter("silo", |a: &AuthenticatedActor| {
138150
match a.actor {
139151
authn::Actor::SiloUser { silo_id, .. }

nexus/auth/src/authz/api_resources.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,55 @@ impl AuthorizedResource for SiloUserList {
770770
}
771771
}
772772

773+
/// Synthetic resource describing the list of Groups in a Silo
774+
#[derive(Clone, Debug, Eq, PartialEq)]
775+
pub struct SiloGroupList(Silo);
776+
777+
impl SiloGroupList {
778+
pub fn new(silo: Silo) -> Self {
779+
SiloGroupList(silo)
780+
}
781+
782+
pub fn silo(&self) -> &Silo {
783+
&self.0
784+
}
785+
}
786+
787+
impl oso::PolarClass for SiloGroupList {
788+
fn get_polar_class_builder() -> oso::ClassBuilder<Self> {
789+
oso::Class::builder()
790+
.with_equality_check()
791+
.add_attribute_getter("silo", |list: &SiloGroupList| list.0.clone())
792+
}
793+
}
794+
795+
impl AuthorizedResource for SiloGroupList {
796+
fn load_roles<'fut>(
797+
&'fut self,
798+
opctx: &'fut OpContext,
799+
authn: &'fut authn::Context,
800+
roleset: &'fut mut RoleSet,
801+
) -> futures::future::BoxFuture<'fut, Result<(), Error>> {
802+
// There are no roles on this resource, but we still need to load the
803+
// Silo-related roles.
804+
self.silo().load_roles(opctx, authn, roleset)
805+
}
806+
807+
fn on_unauthorized(
808+
&self,
809+
_: &Authz,
810+
error: Error,
811+
_: AnyActor,
812+
_: Action,
813+
) -> Error {
814+
error
815+
}
816+
817+
fn polar_class(&self) -> oso::Class {
818+
Self::get_polar_class()
819+
}
820+
}
821+
773822
// Note the session list and the token list have exactly the same behavior
774823

775824
/// Synthetic resource for managing a user's sessions

nexus/auth/src/authz/omicron.polar

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,11 @@ resource SshKey {
281281
relations = { silo_user: SiloUser };
282282

283283
"read" if "read" on "silo_user";
284-
"modify" if "modify" on "silo_user";
285284
}
285+
# We want to allow the user to modify the ssh key but disallow a SCIM IdP token
286+
# from doing the same.
287+
has_permission(actor: AuthenticatedActor, "modify", ssh_key: SshKey)
288+
if actor.is_user and has_permission(actor, "modify", ssh_key.silo_user);
286289
has_relation(user: SiloUser, "silo_user", ssh_key: SshKey)
287290
if ssh_key.silo_user = user;
288291

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

636+
# Grant SCIM IdP actors the permissions they need on users.
637+
has_permission(actor: AuthenticatedActor, "read", silo_user: SiloUser)
638+
if actor.is_scim_idp and silo_user.silo in actor.silo;
639+
has_permission(actor: AuthenticatedActor, "create_child", silo_user_list: SiloUserList)
640+
if actor.is_scim_idp and silo_user_list.silo in actor.silo;
641+
has_permission(actor: AuthenticatedActor, "modify", silo_user: SiloUser)
642+
if actor.is_scim_idp and silo_user.silo in actor.silo;
643+
has_permission(actor: AuthenticatedActor, "list_children", silo_user_list: SiloUserList)
644+
if actor.is_scim_idp and silo_user_list.silo in actor.silo;
645+
646+
# Describes the policy for creating and managing Silo groups (mostly intended
647+
# for API-managed groups)
648+
resource SiloGroupList {
649+
permissions = [ "list_children", "create_child" ];
650+
651+
relations = { parent_silo: Silo, parent_fleet: Fleet };
652+
653+
# Everyone who can read the Silo (which includes all the groups in the
654+
# Silo) can see the groups in it.
655+
"list_children" if "read" on "parent_silo";
656+
657+
# Fleet and Silo administrators can manage the Silo's groups. This is
658+
# one of the only areas of Silo configuration that Fleet Administrators
659+
# have permissions on. This is also one of the few cases (so far) where
660+
# we need to look two levels up the hierarchy to see if somebody has the
661+
# right permission. For most other things, permissions cascade down the
662+
# hierarchy so we only need to look at the parent.
663+
"create_child" if "admin" on "parent_silo";
664+
"list_children" if "admin" on "parent_fleet";
665+
"create_child" if "admin" on "parent_fleet";
666+
}
667+
has_relation(silo: Silo, "parent_silo", collection: SiloGroupList)
668+
if collection.silo = silo;
669+
has_relation(fleet: Fleet, "parent_fleet", collection: SiloGroupList)
670+
if collection.silo.fleet = fleet;
671+
672+
# Grant SCIM IdP actors the permissions they need on groups.
673+
has_permission(actor: AuthenticatedActor, "read", silo_group: SiloGroup)
674+
if actor.is_scim_idp and silo_group.silo in actor.silo;
675+
has_permission(actor: AuthenticatedActor, "create_child", silo_group_list: SiloGroupList)
676+
if actor.is_scim_idp and silo_group_list.silo in actor.silo;
677+
has_permission(actor: AuthenticatedActor, "modify", silo_group: SiloGroup)
678+
if actor.is_scim_idp and silo_group.silo in actor.silo;
679+
has_permission(actor: AuthenticatedActor, "list_children", silo_group_list: SiloGroupList)
680+
if actor.is_scim_idp and silo_group_list.silo in actor.silo;
681+
633682
# These rules grants the external authenticator role the permissions it needs to
634683
# read silo users and modify their sessions. This is necessary for login to
635684
# work.

nexus/auth/src/authz/oso_generic.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<OsoInit, anyhow::Error> {
114114
DeviceAuthRequestList::get_polar_class(),
115115
QuiesceState::get_polar_class(),
116116
SiloCertificateList::get_polar_class(),
117+
SiloGroupList::get_polar_class(),
117118
SiloIdentityProviderList::get_polar_class(),
118119
SiloUserList::get_polar_class(),
119120
SiloUserSessionList::get_polar_class(),

nexus/db-model/src/schema_versions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
1616
///
1717
/// This must be updated when you change the database schema. Refer to
1818
/// schema/crdb/README.adoc in the root of this repository for details.
19-
pub const SCHEMA_VERSION: Version = Version::new(202, 0, 0);
19+
pub const SCHEMA_VERSION: Version = Version::new(203, 0, 0);
2020

2121
/// List of all past database schema versions, in *reverse* order
2222
///
@@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
2828
// | leaving the first copy as an example for the next person.
2929
// v
3030
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
31+
KnownVersion::new(203, "scim-actor-audit-log"),
3132
KnownVersion::new(202, "add-ip-to-external-ip-index"),
3233
KnownVersion::new(201, "scim-client-bearer-token"),
3334
KnownVersion::new(200, "dual-stack-network-interfaces"),

0 commit comments

Comments
 (0)