Skip to content
Merged
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
8 changes: 7 additions & 1 deletion common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,13 @@ CREATE INDEX on omicron.public.volume (
* Silos
*/

CREATE TYPE omicron.public.authentication_mode AS ENUM (
'local',
'saml'
);

CREATE TYPE omicron.public.user_provision_type AS ENUM (
'fixed',
'api_only',
'jit'
);

Expand All @@ -320,6 +325,7 @@ CREATE TABLE omicron.public.silo (
time_deleted TIMESTAMPTZ,

discoverable BOOL NOT NULL,
authentication_mode omicron.public.authentication_mode NOT NULL,
user_provision_type omicron.public.user_provision_type NOT NULL,

/* child resource generation number, per RFD 192 */
Expand Down
22 changes: 16 additions & 6 deletions nexus/db-macros/src/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ pub struct Input {
primary_key_columns: Vec<PrimaryKeyColumn>,
/// This resources supports soft-deletes
soft_deletes: bool,
/// This resource appears under the `Silo` hierarchy, but nevertheless
/// should be visible to users in other Silos
///
/// This is "false" by default. If you don't specify this,
/// `lookup_resource!` determines whether something should be visible based
/// on whether it's nested under a `Silo`.
#[serde(default)]
visible_outside_silo: bool,
}

#[derive(serde::Deserialize)]
Expand All @@ -67,8 +75,9 @@ pub struct Config {
/// This resources supports soft-deletes
soft_deletes: bool,

/// This resource is nested under the Silo hierarchy
siloed: bool,
/// This resource is inside a Silo and only visible to users in the same
/// Silo
silo_restricted: bool,

// The path to the resource
/// list of type names for this resource and its parents
Expand Down Expand Up @@ -115,11 +124,12 @@ impl Config {

let child_resources = input.children;
let parent = input.ancestors.last().map(|s| Resource::for_name(&s));
let siloed = input.ancestors.iter().any(|s| s == "Silo");
let silo_restricted = !input.visible_outside_silo
&& input.ancestors.iter().any(|s| s == "Silo");

Config {
resource,
siloed,
silo_restricted,
path_types,
path_authz_names,
parent,
Expand Down Expand Up @@ -289,7 +299,7 @@ fn generate_misc_helpers(config: &Config) -> TokenStream {
};

let resource_authz_name = &config.resource.authz_name;
let silo_check_fn = if config.siloed {
let silo_check_fn = if config.silo_restricted {
quote! {
/// For a "siloed" resource (i.e., one that's nested under "Silo" in
/// the resource hierarchy), check whether a given resource's Silo
Expand Down Expand Up @@ -477,7 +487,7 @@ fn generate_lookup_methods(config: &Config) -> TokenStream {
// If this resource is "Siloed", then tack on an extra check that the
// resource's Silo matches the Silo of the actor doing the fetch/lookup.
// See the generation of `silo_check()` for details.
let (silo_check_lookup, silo_check_fetch) = if config.siloed {
let (silo_check_lookup, silo_check_fetch) = if config.silo_restricted {
(
quote! {
.and_then(|input| {
Expand Down
1 change: 1 addition & 0 deletions nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ table! {
time_deleted -> Nullable<Timestamptz>,

discoverable -> Bool,
authentication_mode -> crate::AuthenticationModeEnum,
user_provision_type -> crate::UserProvisionTypeEnum,

rcgen -> Int8,
Expand Down
81 changes: 72 additions & 9 deletions nexus/db-model/src/silo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,45 @@ use crate::collection::DatastoreCollectionConfig;
use crate::impl_enum_type;
use crate::schema::{organization, silo};
use db_macros::Resource;
use nexus_types::external_api::shared::SiloIdentityMode;
use nexus_types::external_api::views;
use nexus_types::external_api::{params, shared};
use nexus_types::identity::Resource;
use omicron_common::api::external::Error;
use uuid::Uuid;

impl_enum_type!(
#[derive(SqlType, Debug, QueryId)]
#[diesel(postgres_type(name = "authentication_mode"))]
pub struct AuthenticationModeEnum;

#[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq)]
#[diesel(sql_type = AuthenticationModeEnum)]
pub enum AuthenticationMode;

// Enum values
Local => b"local"
Saml => b"saml"
);

impl From<shared::AuthenticationMode> for AuthenticationMode {
fn from(params: shared::AuthenticationMode) -> Self {
match params {
shared::AuthenticationMode::Local => AuthenticationMode::Local,
shared::AuthenticationMode::Saml => AuthenticationMode::Saml,
}
}
}

impl From<AuthenticationMode> for shared::AuthenticationMode {
fn from(model: AuthenticationMode) -> Self {
match model {
AuthenticationMode::Local => Self::Local,
AuthenticationMode::Saml => Self::Saml,
}
}
}

impl_enum_type!(
#[derive(SqlType, Debug, QueryId)]
#[diesel(postgres_type(name = "user_provision_type"))]
Expand All @@ -22,14 +56,14 @@ impl_enum_type!(
pub enum UserProvisionType;

// Enum values
Fixed => b"fixed"
ApiOnly => b"api_only"
Jit => b"jit"
);

impl From<shared::UserProvisionType> for UserProvisionType {
fn from(params: shared::UserProvisionType) -> Self {
match params {
shared::UserProvisionType::Fixed => UserProvisionType::Fixed,
shared::UserProvisionType::ApiOnly => UserProvisionType::ApiOnly,
shared::UserProvisionType::Jit => UserProvisionType::Jit,
}
}
Expand All @@ -38,7 +72,7 @@ impl From<shared::UserProvisionType> for UserProvisionType {
impl From<UserProvisionType> for shared::UserProvisionType {
fn from(model: UserProvisionType) -> Self {
match model {
UserProvisionType::Fixed => Self::Fixed,
UserProvisionType::ApiOnly => Self::ApiOnly,
UserProvisionType::Jit => Self::Jit,
}
}
Expand All @@ -53,6 +87,7 @@ pub struct Silo {

pub discoverable: bool,

pub authentication_mode: AuthenticationMode,
pub user_provision_type: UserProvisionType,

/// child resource generation number, per RFD 192
Expand All @@ -69,19 +104,47 @@ impl Silo {
Self {
identity: SiloIdentity::new(id, params.identity),
discoverable: params.discoverable,
user_provision_type: params.user_provision_type.into(),
authentication_mode: params
.identity_mode
.authentication_mode()
.into(),
user_provision_type: params
.identity_mode
.user_provision_type()
.into(),
rcgen: Generation::new(),
}
}
}

impl From<Silo> for views::Silo {
fn from(silo: Silo) -> Self {
Self {
impl TryFrom<Silo> for views::Silo {
type Error = Error;
fn try_from(silo: Silo) -> Result<Self, Self::Error> {
let authn_mode = &silo.authentication_mode;
let user_type = &silo.user_provision_type;
let identity_mode = match (authn_mode, user_type) {
(AuthenticationMode::Saml, UserProvisionType::Jit) => {
Some(SiloIdentityMode::SamlJit)
}
(AuthenticationMode::Saml, UserProvisionType::ApiOnly) => None,
(AuthenticationMode::Local, UserProvisionType::ApiOnly) => {
Some(SiloIdentityMode::LocalOnly)
}
(AuthenticationMode::Local, UserProvisionType::Jit) => None,
}
.ok_or_else(|| {
Error::internal_error(&format!(
"unsupported combination of authentication mode ({:?}) and \
user provision type ({:?})",
authn_mode, user_type
))
})?;

Ok(Self {
identity: silo.identity(),
discoverable: silo.discoverable,
user_provision_type: silo.user_provision_type.into(),
}
identity_mode,
})
}
}

Expand Down
21 changes: 17 additions & 4 deletions nexus/src/app/silo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ impl super::Nexus {
// external id. The next action depends on the silo's user
// provision type.
match db_silo.user_provision_type {
// If the user provision type is fixed, do not a new user if
// one does not exist.
db::model::UserProvisionType::Fixed => {
// If the user provision type is ApiOnly, do not create a
// new user if one does not exist.
db::model::UserProvisionType::ApiOnly => {
return Ok(None);
}

Expand Down Expand Up @@ -212,7 +212,7 @@ impl super::Nexus {

for group in &authenticated_subject.groups {
let silo_group = match db_silo.user_provision_type {
db::model::UserProvisionType::Fixed => {
db::model::UserProvisionType::ApiOnly => {
self.db_datastore
.silo_group_optional_lookup(
opctx,
Expand Down Expand Up @@ -384,6 +384,19 @@ impl super::Nexus {
// an external source.
opctx.authorize(authz::Action::CreateChild, &authz_idp_list).await?;

// The authentication mode is immutable so it's safe to check this here
// and bail out.
if db_silo.authentication_mode
!= nexus_db_model::AuthenticationMode::Saml
{
return Err(Error::invalid_request(&format!(
"cannot create SAML identity provider for this Silo type \
(expected authentication mode {:?}, found {:?})",
nexus_db_model::AuthenticationMode::Saml,
&db_silo.authentication_mode,
)));
}

let idp_metadata_document_string = match &params.idp_metadata_source {
params::IdpMetadataSource::Url { url } => {
// Download the SAML IdP descriptor, and write it into the DB.
Expand Down
30 changes: 22 additions & 8 deletions nexus/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -286,17 +286,24 @@ resource IdentityProvider {
"create_child",
"list_children",
];
relations = { parent_silo: Silo };
relations = { parent_silo: Silo, parent_fleet: Fleet };

# Silo-level roles grant privileges on identity providers.
"read" if "viewer" on "parent_silo";
"list_children" if "viewer" on "parent_silo";

# Only silo admins can create silo identity providers
"modify" if "admin" on "parent_silo";
"create_child" if "admin" on "parent_silo";

# Fleet-level roles also grant privileges on identity providers.
"read" if "viewer" on "parent_fleet";
"list_children" if "viewer" on "parent_fleet";
"modify" if "admin" on "parent_fleet";
"create_child" if "admin" on "parent_fleet";
}
has_relation(silo: Silo, "parent_silo", identity_provider: IdentityProvider)
if identity_provider.silo = silo;
has_relation(fleet: Fleet, "parent_fleet", collection: IdentityProvider)
if collection.silo.fleet = fleet;

resource SamlIdentityProvider {
permissions = [
Expand All @@ -305,17 +312,24 @@ resource SamlIdentityProvider {
"create_child",
"list_children",
];
relations = { parent_silo: Silo };

# Only silo admins have permissions for specific identity provider details
"read" if "admin" on "parent_silo";
"list_children" if "admin" on "parent_silo";
relations = { parent_silo: Silo, parent_fleet: Fleet };

# Silo-level roles grant privileges on identity providers.
"read" if "viewer" on "parent_silo";
"list_children" if "viewer" on "parent_silo";
"modify" if "admin" on "parent_silo";
"create_child" if "admin" on "parent_silo";

# Fleet-level roles also grant privileges on identity providers.
"read" if "viewer" on "parent_fleet";
"list_children" if "viewer" on "parent_fleet";
"modify" if "admin" on "parent_fleet";
"create_child" if "admin" on "parent_fleet";
}
has_relation(silo: Silo, "parent_silo", saml_identity_provider: SamlIdentityProvider)
if saml_identity_provider.silo = silo;
has_relation(fleet: Fleet, "parent_fleet", collection: SamlIdentityProvider)
if collection.silo.fleet = fleet;

#
# SYNTHETIC RESOURCES OUTSIDE THE SILO HIERARCHY
Expand Down
2 changes: 1 addition & 1 deletion nexus/src/db/fixed_data/silo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ lazy_static! {
description: "default silo".to_string(),
},
discoverable: false,
user_provision_type: shared::UserProvisionType::Fixed,
identity_mode: shared::SiloIdentityMode::LocalOnly,
admin_group_name: None,
}
);
Expand Down
3 changes: 2 additions & 1 deletion nexus/src/db/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,8 @@ lookup_resource! {
soft_deletes = true,
primary_key_columns = [
{ column_name = "id", rust_type = Uuid },
]
],
visible_outside_silo = true
}

lookup_resource! {
Expand Down
4 changes: 2 additions & 2 deletions nexus/src/external_api/console_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ impl RelayState {
/// to their identity provider.
#[endpoint {
method = GET,
path = "/login/{silo_name}/{provider_name}",
path = "/login/{silo_name}/saml/{provider_name}",
tags = ["login"],
}]
pub async fn login_saml_begin(
Expand Down Expand Up @@ -326,7 +326,7 @@ pub async fn login_saml_begin(
/// data (like a SAMLResponse). Use these to set the user's session cookie.
#[endpoint {
method = POST,
path = "/login/{silo_name}/{provider_name}",
path = "/login/{silo_name}/saml/{provider_name}",
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the URL that will have to change in the Keycloak configuration, it's the path the IdP POSTs to.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's also maybe update the remote access docs to use a different name for the provider instead of saml, lest we end up with the repetitive /login/mercury/saml/saml 😅

tags = ["login"],
}]
pub async fn login_saml(
Expand Down
Loading