Skip to content
Open
7 changes: 4 additions & 3 deletions nexus/db-fixed-data/src/silo_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
//! Built-in Silo Users

use nexus_db_model as model;
use nexus_types::{identity::Asset, silo::DEFAULT_SILO_ID};
use nexus_types::identity::Asset;
use nexus_types::silo::DEFAULT_SILO_ID;
use omicron_common::api::external::ResourceType;
use std::sync::LazyLock;

Expand All @@ -15,7 +16,7 @@ use std::sync::LazyLock;
// not automatically at Nexus startup. See omicron#2305.
pub static USER_TEST_PRIVILEGED: LazyLock<model::SiloUser> =
LazyLock::new(|| {
model::SiloUser::new(
model::SiloUser::new_api_only_user(
DEFAULT_SILO_ID,
// "4007" looks a bit like "root".
"001de000-05e4-4000-8000-000000004007".parse().unwrap(),
Expand Down Expand Up @@ -50,7 +51,7 @@ pub static ROLE_ASSIGNMENTS_PRIVILEGED: LazyLock<Vec<model::RoleAssignment>> =
// not automatically at Nexus startup. See omicron#2305.
pub static USER_TEST_UNPRIVILEGED: LazyLock<model::SiloUser> =
LazyLock::new(|| {
model::SiloUser::new(
model::SiloUser::new_api_only_user(
DEFAULT_SILO_ID,
// 60001 is the decimal uid for "nobody" on Helios.
"001de000-05e4-4000-8000-000000060001".parse().unwrap(),
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(195, 0, 0);
pub const SCHEMA_VERSION: Version = Version::new(196, 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(196, "user-provision-type-for-silo-user-and-group"),
KnownVersion::new(195, "tuf-pruned-index"),
KnownVersion::new(194, "tuf-pruned"),
KnownVersion::new(193, "nexus-lockstep-port"),
Expand Down
57 changes: 39 additions & 18 deletions nexus/db-model/src/silo_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
// 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 super::UserProvisionType;
use crate::DbTypedUuid;
use crate::to_db_typed_uuid;
use db_macros::Asset;
use nexus_db_schema::schema::{silo_group, silo_group_membership};
use nexus_types::external_api::views;
use nexus_types::identity::Asset;
use omicron_uuid_kinds::SiloGroupKind;
use omicron_uuid_kinds::SiloGroupUuid;
use omicron_uuid_kinds::SiloUserKind;
Expand All @@ -20,17 +19,50 @@ use uuid::Uuid;
#[asset(uuid_kind = SiloGroupKind)]
pub struct SiloGroup {
#[diesel(embed)]
identity: SiloGroupIdentity,
pub identity: SiloGroupIdentity,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do these need to be fully pub or can we make them pub(crate)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, these need to be fully pub as the DataStore structs need to access those fields.

pub time_deleted: Option<chrono::DateTime<chrono::Utc>>,

pub silo_id: Uuid,

/// The identity provider's name for this group.
pub external_id: String,
/// If the user provision type is ApiOnly or JIT, then the external id is
/// the identity provider's ID for this group. There is a database
/// constraint (`lookup_silo_group_by_silo`) that ensures this field must be
/// non-null for those provision types.
///
/// For SCIM, this may be null, which would trigger the uniqueness
/// constraint if that wasn't limited to specific provision types.
pub external_id: Option<String>,

pub user_provision_type: UserProvisionType,
}

impl SiloGroup {
pub fn new(id: SiloGroupUuid, silo_id: Uuid, external_id: String) -> Self {
Self { identity: SiloGroupIdentity::new(id), silo_id, external_id }
pub fn new_api_only_group(
id: SiloGroupUuid,
silo_id: Uuid,
external_id: String,
) -> Self {
Self {
identity: SiloGroupIdentity::new(id),
time_deleted: None,
silo_id,
user_provision_type: UserProvisionType::ApiOnly,
external_id: Some(external_id),
}
}

pub fn new_jit_group(
id: SiloGroupUuid,
silo_id: Uuid,
external_id: String,
) -> Self {
Comment on lines +40 to +58
Copy link
Member

Choose a reason for hiding this comment

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

why two separate constructors instead of taking UserProvisionType as an argument? is that because we're eventually going to have a SCIM UserProvisionType that will be constructed differently?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right yeah, and specifically the new_scim_group method will require different fields to be non-null (and additional fields that could be null)

Self {
identity: SiloGroupIdentity::new(id),
time_deleted: None,
silo_id,
user_provision_type: UserProvisionType::Jit,
external_id: Some(external_id),
}
}
}

Expand All @@ -53,14 +85,3 @@ impl SiloGroupMembership {
}
}
}

impl From<SiloGroup> for views::Group {
fn from(group: SiloGroup) -> Self {
Self {
id: group.id(),
// TODO the use of external_id as display_name is temporary
display_name: group.external_id,
silo_id: group.silo_id,
}
}
}
39 changes: 25 additions & 14 deletions nexus/db-model/src/silo_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
// 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 super::UserProvisionType;
use db_macros::Asset;
use nexus_db_schema::schema::silo_user;
use nexus_types::external_api::views;
use nexus_types::identity::Asset;
use omicron_uuid_kinds::SiloUserUuid;
use uuid::Uuid;

Expand All @@ -15,17 +14,25 @@ use uuid::Uuid;
#[asset(uuid_kind = SiloUserKind)]
pub struct SiloUser {
#[diesel(embed)]
identity: SiloUserIdentity,
pub identity: SiloUserIdentity,

pub time_deleted: Option<chrono::DateTime<chrono::Utc>>,
pub silo_id: Uuid,

/// The identity provider's ID for this user.
pub external_id: String,
/// If the user provision type is ApiOnly or JIT, then the external id is
/// the identity provider's ID for this user. There is a database constraint
/// (`lookup_silo_user_by_silo`) that ensures this field must be non-null
/// for those provision types.
///
/// For SCIM, this may be null, which would trigger the uniqueness
/// constraint if that wasn't limited to specific provision types.
pub external_id: Option<String>,

pub user_provision_type: UserProvisionType,
}

impl SiloUser {
pub fn new(
pub fn new_api_only_user(
silo_id: Uuid,
user_id: SiloUserUuid,
external_id: String,
Expand All @@ -34,18 +41,22 @@ impl SiloUser {
identity: SiloUserIdentity::new(user_id),
time_deleted: None,
silo_id,
external_id,
external_id: Some(external_id),
user_provision_type: UserProvisionType::ApiOnly,
}
}
}

impl From<SiloUser> for views::User {
fn from(user: SiloUser) -> Self {
pub fn new_jit_user(
silo_id: Uuid,
user_id: SiloUserUuid,
external_id: String,
) -> Self {
Self {
id: user.id(),
// TODO the use of external_id as display_name is temporary
display_name: user.external_id,
silo_id: user.silo_id,
identity: SiloUserIdentity::new(user_id),
time_deleted: None,
silo_id,
external_id: Some(external_id),
user_provision_type: UserProvisionType::Jit,
}
}
}
20 changes: 15 additions & 5 deletions nexus/db-queries/src/db/datastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ pub use region::RegionAllocationParameters;
pub use region_snapshot_replacement::NewRegionVolumeId;
pub use region_snapshot_replacement::OldSnapshotVolumeId;
pub use silo::Discoverability;
pub use silo_group::SiloGroup;
pub use silo_group::SiloGroupApiOnly;
pub use silo_group::SiloGroupJit;
pub use silo_group::SiloGroupLookup;
pub use silo_user::SiloUser;
pub use silo_user::SiloUserApiOnly;
pub use silo_user::SiloUserJit;
pub use silo_user::SiloUserLookup;
pub use sled::SledTransition;
pub use sled::TransitionError;
pub use support_bundle::SupportBundleExpungementReport;
Expand Down Expand Up @@ -577,7 +585,7 @@ mod test {
use crate::db::model::{
BlockSize, ConsoleSession, CrucibleDataset, ExternalIp, PhysicalDisk,
PhysicalDiskKind, PhysicalDiskPolicy, PhysicalDiskState, Project, Rack,
Region, SiloUser, SshKey, Zpool,
Region, SshKey, Zpool,
};
use crate::db::pub_test_utils::TestDatabase;
use crate::db::pub_test_utils::helpers::SledUpdateBuilder;
Expand Down Expand Up @@ -716,11 +724,12 @@ mod test {
datastore
.silo_user_create(
&authz_silo,
SiloUser::new(
SiloUserApiOnly::new(
authz_silo.id(),
silo_user_id,
"external_id".into(),
),
)
.into(),
)
.await
.unwrap();
Expand Down Expand Up @@ -1856,11 +1865,12 @@ mod test {
datastore
.silo_user_create(
&authz_silo,
SiloUser::new(
SiloUserApiOnly::new(
authz_silo.id(),
silo_user_id,
"external@id".into(),
),
)
.into(),
)
.await
.unwrap();
Expand Down
30 changes: 24 additions & 6 deletions nexus/db-queries/src/db/datastore/rack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::db::model::CrucibleDataset;
use crate::db::model::IncompleteExternalIp;
use crate::db::model::PhysicalDisk;
use crate::db::model::Rack;
use crate::db::model::UserProvisionType;
use crate::db::model::Zpool;
use crate::db::pagination::paginated;
use async_bb8_diesel::AsyncRunQueryDsl;
Expand Down Expand Up @@ -442,6 +443,15 @@ impl DataStore {
recovery_user_password_hash: omicron_passwords::PasswordHashString,
dns_update: DnsVersionUpdateBuilder,
) -> Result<(), RackInitError> {
if !matches!(
&recovery_silo.identity_mode,
shared::SiloIdentityMode::LocalOnly
) {
return Err(RackInitError::Silo(Error::invalid_request(
"recovery silo should only use identity mode LocalOnly",
)));
}

let db_silo = self
.silo_create_conn(
conn,
Expand All @@ -460,11 +470,19 @@ impl DataStore {

// Create the first user in the initial Recovery Silo
let silo_user_id = SiloUserUuid::new_v4();
let silo_user = SiloUser::new(
db_silo.id(),
silo_user_id,
recovery_user_id.as_ref().to_owned(),
);

let silo_user = match &db_silo.user_provision_type {
UserProvisionType::ApiOnly => SiloUser::new_api_only_user(
db_silo.id(),
silo_user_id,
recovery_user_id.as_ref().to_owned(),
),

UserProvisionType::Jit => {
unreachable!("match at start of function should prevent this");
}
};

{
use nexus_db_schema::schema::silo_user::dsl;
diesel::insert_into(dsl::silo_user)
Expand Down Expand Up @@ -1250,7 +1268,7 @@ mod test {
.expect("failed to list users");
assert_eq!(silo_users.len(), 1);
assert_eq!(
silo_users[0].external_id,
silo_users[0].external_id(),
rack_init.recovery_user_id.as_ref()
);
let authz_silo_user = authz::SiloUser::new(
Expand Down
41 changes: 36 additions & 5 deletions nexus/db-queries/src/db/datastore/silo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,30 @@ impl DataStore {
let silo_admin_group_ensure_query = if let Some(ref admin_group_name) =
new_silo_params.admin_group_name
{
let silo_admin_group =
match new_silo_params.identity_mode.user_provision_type() {
shared::UserProvisionType::ApiOnly => {
db::model::SiloGroup::new_api_only_group(
silo_group_id,
silo_id,
admin_group_name.clone(),
)
}

shared::UserProvisionType::Jit => {
db::model::SiloGroup::new_jit_group(
silo_group_id,
silo_id,
admin_group_name.clone(),
)
}
};
Comment on lines +195 to +212
Copy link
Member

Choose a reason for hiding this comment

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

this sure seems like it would be a lot simpler if we just had one constructor that you pass the user_provision_type into, but i'm guessing a subsequent change will make it make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's additional behaviour changes required in this function (and others) based on the user provision type. Hopefully the additional "add the scim user provision type" PR will make this make more sense, I just wanted to break out the changes in the smallest parts that made sense (maybe this one was too small!).


let silo_admin_group_ensure_query =
DataStore::silo_group_ensure_query(
&nexus_opctx,
&authz_silo,
db::model::SiloGroup::new(
silo_group_id,
silo_id,
admin_group_name.clone(),
),
silo_admin_group,
)
.await?;

Expand Down Expand Up @@ -505,6 +520,10 @@ impl DataStore {
silo_user::dsl::silo_user
.filter(silo_user::dsl::silo_id.eq(id))
.filter(silo_user::dsl::time_deleted.is_null())
.filter(
silo_user::dsl::user_provision_type
.eq(db_silo.user_provision_type),
)
.select(silo_user::dsl::id),
),
)
Expand All @@ -520,6 +539,10 @@ impl DataStore {
let updated_rows = diesel::update(silo_user::dsl::silo_user)
.filter(silo_user::dsl::silo_id.eq(id))
.filter(silo_user::dsl::time_deleted.is_null())
.filter(
silo_user::dsl::user_provision_type
.eq(db_silo.user_provision_type),
)
.set(silo_user::dsl::time_deleted.eq(now))
.execute_async(&*conn)
.await
Expand All @@ -538,6 +561,10 @@ impl DataStore {
silo_group::dsl::silo_group
.filter(silo_group::dsl::silo_id.eq(id))
.filter(silo_group::dsl::time_deleted.is_null())
.filter(
silo_group::dsl::user_provision_type
.eq(db_silo.user_provision_type),
)
.select(silo_group::dsl::id),
),
)
Expand All @@ -556,6 +583,10 @@ impl DataStore {
let updated_rows = diesel::update(silo_group::dsl::silo_group)
.filter(silo_group::dsl::silo_id.eq(id))
.filter(silo_group::dsl::time_deleted.is_null())
.filter(
silo_group::dsl::user_provision_type
.eq(db_silo.user_provision_type),
)
.set(silo_group::dsl::time_deleted.eq(now))
.execute_async(&*conn)
.await
Expand Down
Loading
Loading