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

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

8 changes: 8 additions & 0 deletions nexus/auth/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1429,3 +1429,11 @@ authz_resource! {
roles_allowed = false,
polar_snippet = Custom,
}

authz_resource! {
name = "ScimClientBearerToken",
parent = "Silo",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = Custom,
}
38 changes: 37 additions & 1 deletion nexus/auth/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ resource Fleet {
"viewer",

# Internal-only roles
"external-authenticator"
"external-authenticator",
"external-scim"
];

# Roles implied by other roles on this resource
Expand Down Expand Up @@ -149,6 +150,9 @@ resource Silo {
# external authenticator has to create silo users
"list_children" if "external-authenticator" on "parent_fleet";
"create_child" if "external-authenticator" on "parent_fleet";

# external scim has to be able to read SCIM tokens
"list_children" if "external-scim" on "parent_fleet";
}

has_relation(fleet: Fleet, "parent_fleet", silo: Silo)
Expand Down Expand Up @@ -703,3 +707,35 @@ resource AlertClassList {

has_relation(fleet: Fleet, "parent_fleet", collection: AlertClassList)
if collection.fleet = fleet;

# These rules grant the external scim authenticator role the permission
# required to create the SCIM provider implementation for a Silo

has_permission(actor: AuthenticatedActor, "read", silo: Silo)
if has_role(actor, "external-scim", silo.fleet);

resource ScimClientBearerToken {
permissions = [
"read",
"modify",
"create_child",
"list_children",
];
relations = { parent_silo: Silo, parent_fleet: Fleet };

# Silo-level roles grant privileges for SCIM client tokens.
"read" if "admin" on "parent_silo";
"list_children" if "admin" on "parent_silo";
"modify" if "admin" on "parent_silo";
"create_child" if "admin" on "parent_silo";

# Fleet-level roles also grant privileges for SCIM client tokens.
"read" if "admin" on "parent_fleet";
"list_children" if "admin" on "parent_fleet";
"modify" if "admin" on "parent_fleet";
"create_child" if "admin" on "parent_fleet";
}
has_relation(silo: Silo, "parent_silo", scim_client_bearer_token: ScimClientBearerToken)
if scim_client_bearer_token.silo = silo;
has_relation(fleet: Fleet, "parent_fleet", collection: ScimClientBearerToken)
if collection.silo.fleet = fleet;
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 @@ -175,6 +175,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<OsoInit, anyhow::Error> {
Zpool::init(),
Service::init(),
UserBuiltin::init(),
ScimClientBearerToken::init(),
];

for init in generated_inits {
Expand Down
2 changes: 2 additions & 0 deletions nexus/db-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ mod producer_endpoint;
mod project;
mod reconfigurator_config;
mod rendezvous_debug_dataset;
mod scim_client_bearer_token;
mod semver_version;
mod serde_time_delta;
mod silo_auth_settings;
Expand Down Expand Up @@ -223,6 +224,7 @@ pub use rendezvous_debug_dataset::*;
pub use role_assignment::*;
pub use saga_types::*;
pub use schema_versions::*;
pub use scim_client_bearer_token::*;
pub use semver_version::*;
pub use service_kind::*;
pub use silo::*;
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(197, 0, 0);
pub const SCHEMA_VERSION: Version = Version::new(198, 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(198, "scim-client-bearer-token"),
KnownVersion::new(197, "scim-users-and-groups"),
KnownVersion::new(196, "user-provision-type-for-silo-user-and-group"),
KnownVersion::new(195, "tuf-pruned-index"),
Expand Down
47 changes: 47 additions & 0 deletions nexus/db-model/src/scim_client_bearer_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// 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 chrono::DateTime;
use chrono::Utc;
use nexus_db_schema::schema::scim_client_bearer_token;
use nexus_types::external_api::views;
use uuid::Uuid;

/// A SCIM client sends requests to a SCIM provider (in this case, Nexus) using
/// some sort of authentication. Nexus currently only supports Bearer token auth
/// from SCIM clients, and these tokens are stored here.
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
#[diesel(table_name = scim_client_bearer_token)]
pub struct ScimClientBearerToken {
pub id: Uuid,

pub time_created: DateTime<Utc>,
pub time_deleted: Option<DateTime<Utc>>,
pub time_expires: Option<DateTime<Utc>>,

pub silo_id: Uuid,

pub bearer_token: String,
}

impl From<ScimClientBearerToken> for views::ScimClientBearerToken {
fn from(t: ScimClientBearerToken) -> views::ScimClientBearerToken {
views::ScimClientBearerToken {
id: t.id,
time_created: t.time_created,
time_expires: t.time_expires,
}
}
}

impl From<ScimClientBearerToken> for views::ScimClientBearerTokenValue {
fn from(t: ScimClientBearerToken) -> views::ScimClientBearerTokenValue {
views::ScimClientBearerTokenValue {
id: t.id,
time_created: t.time_created,
time_expires: t.time_expires,
bearer_token: t.bearer_token,
}
}
}
2 changes: 2 additions & 0 deletions nexus/db-queries/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ diesel.workspace = true
diesel-dtrace.workspace = true
dropshot.workspace = true
futures.workspace = true
hex.workspace = true
id-map.workspace = true
iddqd.workspace = true
internal-dns-resolver.workspace = true
Expand All @@ -39,6 +40,7 @@ rand.workspace = true
ref-cast.workspace = true
regex.workspace = true
schemars.workspace = true
scim2-rs.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
Expand Down
3 changes: 3 additions & 0 deletions nexus/db-queries/src/db/datastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ pub mod region_snapshot_replacement;
mod rendezvous_debug_dataset;
mod role;
mod saga;
mod scim;
mod scim_provider_store;
mod silo;
mod silo_auth_settings;
mod silo_group;
Expand Down Expand Up @@ -143,6 +145,7 @@ pub use region::RegionAllocationFor;
pub use region::RegionAllocationParameters;
pub use region_snapshot_replacement::NewRegionVolumeId;
pub use region_snapshot_replacement::OldSnapshotVolumeId;
pub use scim_provider_store::CrdbScimProviderStore;
pub use silo::Discoverability;
pub use silo_group::SiloGroup;
pub use silo_group::SiloGroupApiOnly;
Expand Down
151 changes: 151 additions & 0 deletions nexus/db-queries/src/db/datastore/scim.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// 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/.

//! [`DataStore`] methods related to SCIM

use super::DataStore;
use crate::authz;
use crate::context::OpContext;
use crate::db::model::ScimClientBearerToken;
use async_bb8_diesel::AsyncRunQueryDsl;
use chrono::Utc;
use diesel::prelude::*;
use nexus_db_errors::ErrorHandler;
use nexus_db_errors::public_error_from_diesel;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::DeleteResult;
use omicron_common::api::external::ListResultVec;
use omicron_common::api::external::LookupResult;
use rand::{RngCore, SeedableRng, rngs::StdRng};
use uuid::Uuid;

// XXX this is the same as generate_session_token!
fn generate_scim_client_bearer_token() -> String {
let mut rng = StdRng::from_os_rng();
let mut random_bytes: [u8; 20] = [0; 20];
rng.fill_bytes(&mut random_bytes);
hex::encode(random_bytes)
}

impl DataStore {
// SCIM tokens

pub async fn scim_idp_get_tokens(
&self,
opctx: &OpContext,
authz_silo: &authz::Silo,
) -> ListResultVec<ScimClientBearerToken> {
opctx.authorize(authz::Action::ListChildren, authz_silo).await?;

let conn = self.pool_connection_authorized(opctx).await?;

use nexus_db_schema::schema::scim_client_bearer_token::dsl;
let tokens = dsl::scim_client_bearer_token
.filter(dsl::silo_id.eq(authz_silo.id()))
.filter(dsl::time_deleted.is_null())
.select(ScimClientBearerToken::as_select())
.load_async::<ScimClientBearerToken>(&*conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;

Ok(tokens)
}

pub async fn scim_idp_create_token(
&self,
opctx: &OpContext,
authz_silo: &authz::Silo,
) -> CreateResult<ScimClientBearerToken> {
opctx.authorize(authz::Action::CreateChild, authz_silo).await?;

let conn = self.pool_connection_authorized(opctx).await?;

let new_token = ScimClientBearerToken {
id: Uuid::new_v4(),
time_created: Utc::now(),
time_deleted: None,
// TODO: allow setting an expiry? have a silo default?
time_expires: None,
silo_id: authz_silo.id(),
bearer_token: generate_scim_client_bearer_token(),
};

use nexus_db_schema::schema::scim_client_bearer_token::dsl;
diesel::insert_into(dsl::scim_client_bearer_token)
.values(new_token.clone())
.execute_async(&*conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;

Ok(new_token)
}

pub async fn scim_idp_get_token_by_id(
&self,
opctx: &OpContext,
authz_silo: &authz::Silo,
id: Uuid,
) -> LookupResult<Option<ScimClientBearerToken>> {
opctx.authorize(authz::Action::ListChildren, authz_silo).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm thinking you should check the read permission on a value of type authz::ScimClientBearerToken instead, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agreed, my bad - done in ed37a89


let conn = self.pool_connection_authorized(opctx).await?;

use nexus_db_schema::schema::scim_client_bearer_token::dsl;
let token = dsl::scim_client_bearer_token
.filter(dsl::silo_id.eq(authz_silo.id()))
.filter(dsl::id.eq(id))
.filter(dsl::time_deleted.is_null())
.select(ScimClientBearerToken::as_select())
.first_async::<ScimClientBearerToken>(&*conn)
.await
.optional()
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;

Ok(token)
}

pub async fn scim_idp_delete_token_by_id(
&self,
opctx: &OpContext,
authz_silo: &authz::Silo,
id: Uuid,
) -> DeleteResult {
opctx.authorize(authz::Action::Modify, authz_silo).await?;

let conn = self.pool_connection_authorized(opctx).await?;

use nexus_db_schema::schema::scim_client_bearer_token::dsl;
diesel::update(dsl::scim_client_bearer_token)
.filter(dsl::silo_id.eq(authz_silo.id()))
.filter(dsl::id.eq(id))
.filter(dsl::time_deleted.is_null())
.set(dsl::time_deleted.eq(Utc::now()))
.execute_async(&*conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;

Ok(())
}

/// SCIM clients should _not_ authenticate to an Actor in the traditional
/// sense: they shouldn't have permission on any resources under a Silo,
/// only enough to CRUD Silo users and groups.
pub async fn scim_idp_lookup_token_by_bearer(
&self,
bearer_token: String,
) -> LookupResult<Option<ScimClientBearerToken>> {
let conn = self.pool_connection_unauthorized().await?;

use nexus_db_schema::schema::scim_client_bearer_token::dsl;
let maybe_token = dsl::scim_client_bearer_token
.filter(dsl::bearer_token.eq(bearer_token))
.filter(dsl::time_deleted.is_null())
.first_async(&*conn)
.await
.optional()
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;

Ok(maybe_token)
}
}
Loading
Loading