Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
121b56d
Add support for Silo groups
jmpesp Jul 5, 2022
b963c5d
Merge remote-tracking branch 'upstream/main' into silo_groups
jmpesp Jul 6, 2022
4a51058
add IdentityType::SiloGroup to match patterns
jmpesp Jul 6, 2022
5c85f1b
add optional fetch and lookup
jmpesp Jul 6, 2022
9d2b3ff
WIP add broken silo create saga yay
jmpesp Jul 7, 2022
8f8729c
silo creation is now a saga
jmpesp Jul 8, 2022
d300cff
delete silo groups and group memberships
jmpesp Jul 8, 2022
2456ed1
external authenticator permissions on silo groups
jmpesp Jul 8, 2022
34dffd8
Merge remote-tracking branch 'upstream/main' into silo_groups
jmpesp Jul 18, 2022
2544362
do not use Resource::name for silo group names
jmpesp Jul 19, 2022
b11a146
correct index specification
jmpesp Jul 19, 2022
5eba13a
update user group memberships as one transaction
jmpesp Jul 20, 2022
29950ba
trim whitespace and skip empty groups
jmpesp Jul 20, 2022
04b0af5
add back missing delete code
jmpesp Jul 20, 2022
d82c9f6
get role assignments from group membership during transaction
jmpesp Jul 20, 2022
0067fb0
delete silo_group_membership_for_user_no_authz
jmpesp Jul 20, 2022
0714fa3
Don't delete groups that still have memberships
jmpesp Jul 20, 2022
1d6ef52
fmt
jmpesp Jul 20, 2022
0aa7669
silo creation is now a transaction
jmpesp Jul 27, 2022
9f8a3ec
drop silo_delete_by_id
jmpesp Jul 27, 2022
e1ef703
silo and silo group don't need to serialize
jmpesp Jul 27, 2022
566878d
fmt
jmpesp Jul 27, 2022
99b752b
fmt
jmpesp Jul 27, 2022
c031eca
only one authz check for user instead of a for loop for group
jmpesp Jul 27, 2022
bb9f567
use ErrorHandler::Server, do not error if nothing is deleted
jmpesp Jul 27, 2022
6e91105
only one info log message in during silo delete
jmpesp Jul 27, 2022
96fc1f3
remove duplicate index
jmpesp Jul 27, 2022
615b56e
fmt
jmpesp Jul 27, 2022
34f49bc
use public_error_from_diesel_pool
jmpesp Jul 27, 2022
b841c85
remove actor_type, replace with impl From and silo_user_id fn
jmpesp Jul 27, 2022
54e246e
do not store admin_group_name
jmpesp Jul 27, 2022
f3cccc3
Merge remote-tracking branch 'upstream/main' into silo_groups
jmpesp Jul 28, 2022
35962ce
remove holdover from when this was a saga
jmpesp Aug 2, 2022
7837edc
expand public docs
jmpesp Aug 2, 2022
a2507a6
only checking if any group membership exists
jmpesp Aug 2, 2022
9ee416f
ensure (not exclusively create) silo groups
jmpesp Aug 2, 2022
6e79986
change silo_group index to "time_deleted IS NULL", add test for delet…
jmpesp Aug 3, 2022
df37612
use one unique partial index for silo_group
jmpesp Aug 4, 2022
643aadb
diesel does support ON CONFLICT ( ... ) WHERE ...
jmpesp Aug 4, 2022
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: 1 addition & 1 deletion Cargo.lock

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

1 change: 1 addition & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ pub enum ResourceType {
Fleet,
Silo,
SiloUser,
SiloGroup,
IdentityProvider,
SamlIdentityProvider,
SshKey,
Expand Down
48 changes: 44 additions & 4 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,50 @@ CREATE UNIQUE INDEX ON omicron.public.silo_user (
) WHERE
time_deleted IS NULL;

CREATE TYPE omicron.public.provider_type AS ENUM (
'saml'
/*
* Silo groups
*/

CREATE TABLE omicron.public.silo_group (
id UUID PRIMARY KEY,
time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,
time_deleted TIMESTAMPTZ,

Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems to me that there's no great time to clean up these groups when they're no longer used. We could clean up a group if there are no role assignments referencing it and no users in it and it's not the admin group. But this might be undesirable if it does still exist in the IdP and someone will want to use it again.

So here's a thought: how about we store in this row a time_unreferenced column that we update whenever we remove a role assignment referencing it or remove a member from it. That's pretty little work now, but it will allow us to determine later how much of a problem it is to have unreferenced groups. And it gives us a lot of options for cleaning them up. We could clean up groups that haven't been used for a long time, or we could provide the operator a way to do that, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think it'd be a good user experience for the groups to automatically disappear, and even if an operator was to explicitly delete groups that could still cause a lot of confusion if that was done without the knowledge or approval of the appropriate silo user (an SRE or Infra team member or something like that). I would expect groups to remain if I allocated them or if they were JIT allocated during login, and if it were a problem I would write a tool that would remove groups that aren't being used.

silo_id UUID NOT NULL,
external_id TEXT NOT NULL
);

CREATE UNIQUE INDEX ON omicron.public.silo_group (
silo_id,
external_id
) WHERE
time_deleted IS NULL;

/*
* Silo group membership
*/

CREATE TABLE omicron.public.silo_group_membership (
silo_group_id UUID NOT NULL,
silo_user_id UUID NOT NULL,

PRIMARY KEY (silo_group_id, silo_user_id)
);

CREATE INDEX ON omicron.public.silo_group_membership (
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you need (silo_user_id, silo_group_id) here. It's a little weird to have two indexes whose columns are the same except for the order, but I don't see a way around it. We want to paginate the users in a group as well as the groups that a user is in and I think we need two indexes to do that.

silo_user_id,
silo_group_id
);

/*
* Silo identity provider list
*/

CREATE TYPE omicron.public.provider_type AS ENUM (
'saml'
);

CREATE TABLE omicron.public.identity_provider (
/* Identity metadata */
id UUID PRIMARY KEY,
Expand Down Expand Up @@ -329,7 +366,9 @@ CREATE TABLE omicron.public.saml_identity_provider (
technical_contact_email TEXT NOT NULL,

public_cert TEXT,
private_key TEXT
private_key TEXT,

group_attribute_name TEXT
);

CREATE INDEX ON omicron.public.saml_identity_provider (
Expand Down Expand Up @@ -1422,7 +1461,8 @@ CREATE TABLE omicron.public.role_builtin (

CREATE TYPE omicron.public.identity_type AS ENUM (
'user_builtin',
'silo_user'
'silo_user',
'silo_group'
);

CREATE TABLE omicron.public.role_assignment (
Expand Down
2 changes: 1 addition & 1 deletion nexus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ path = "../rpaths"

[dependencies]
anyhow = "1.0"
async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", rev = "ab1f49e0b3f95557aa96bf593282199fafeef4bd" }
async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", rev = "51de79fe02b334899be5d5fd8b469f9d140ea887" }
async-trait = "0.1.56"
base64 = "0.13.0"
bb8 = "0.8.0"
Expand Down
47 changes: 46 additions & 1 deletion nexus/db-macros/src/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub struct Input {
/// Name of the resource
///
/// This is taken as the name of the database model type in
/// `omicron_nexus::db::model`, the name of the authz type in
/// `omicron_nexus::db_model`, the name of the authz type in
/// `omicron_nexus::authz`, and will be the name of the new type created by
/// this macro. The snake case version of the name is taken as the name of
/// the Diesel table interface in `db::schema`.
Expand Down Expand Up @@ -537,6 +537,13 @@ fn generate_lookup_methods(config: &Config) -> TokenStream {
self.fetch_for(authz::Action::Read).await
}

/// Turn the Result<T, E> of [`fetch`] into a Result<Option<T>, E>.
pub async fn optional_fetch(
&self,
) -> LookupResult<Option<(#(authz::#path_types,)* nexus_db_model::#resource_name)>> {
self.optional_fetch_for(authz::Action::Read).await
}

/// Fetch the record corresponding to the selected resource and
/// check whether the caller is allowed to do the specified `action`
///
Expand Down Expand Up @@ -571,6 +578,25 @@ fn generate_lookup_methods(config: &Config) -> TokenStream {
#silo_check_fetch
}

/// Turn the Result<T, E> of [`fetch_for`] into a Result<Option<T>, E>.
pub async fn optional_fetch_for(
&self,
action: authz::Action,
) -> LookupResult<Option<(#(authz::#path_types,)* nexus_db_model::#resource_name)>> {
let result = self.fetch_for(action).await;

match result {
Err(Error::ObjectNotFound {
type_name: _,
lookup_type: _,
}) => Ok(None),

_ => {
Ok(Some(result?))
}
}
}

/// Fetch an `authz` object for the selected resource and check
/// whether the caller is allowed to do the specified `action`
///
Expand All @@ -592,6 +618,25 @@ fn generate_lookup_methods(config: &Config) -> TokenStream {
#silo_check_lookup
}

/// Turn the Result<T, E> of [`lookup_for`] into a Result<Option<T>, E>.
pub async fn optional_lookup_for(
&self,
action: authz::Action,
) -> LookupResult<Option<(#(authz::#path_types,)*)>> {
let result = self.lookup_for(action).await;

match result {
Err(Error::ObjectNotFound {
type_name: _,
lookup_type: _,
}) => Ok(None),

_ => {
Ok(Some(result?))
}
}
}

/// Fetch the "authz" objects for the selected resource and all its
/// parents
///
Expand Down
16 changes: 16 additions & 0 deletions nexus/db-model/src/identity_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,31 @@ pub struct SamlIdentityProvider {

pub silo_id: Uuid,

/// idp descriptor
pub idp_metadata_document_string: String,

/// idp's entity id
pub idp_entity_id: String,

/// sp's client id
pub sp_client_id: String,

/// service provider endpoint where the response will be sent
pub acs_url: String,

/// service provider endpoint where the idp should send log out requests
pub slo_url: String,

/// customer's technical contact for saml configuration
pub technical_contact_email: String,

/// base64 encoded DER corresponding to X509 pair
pub public_cert: Option<String>,
pub private_key: Option<String>,

/// if set, attributes with this name will be considered to denote a user's
/// group membership, where the values will be the group names.
pub group_attribute_name: Option<String>,
}

impl From<SamlIdentityProvider> for views::SamlIdentityProvider {
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 @@ -48,6 +48,7 @@ pub mod schema;
mod service;
mod service_kind;
mod silo;
mod silo_group;
mod silo_user;
mod sled;
mod snapshot;
Expand Down Expand Up @@ -110,6 +111,7 @@ pub use role_builtin::*;
pub use service::*;
pub use service_kind::*;
pub use silo::*;
pub use silo_group::*;
pub use silo_user::*;
pub use sled::*;
pub use snapshot::*;
Expand Down
3 changes: 3 additions & 0 deletions nexus/db-model/src/role_assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ impl_enum_type!(
// Enum values
UserBuiltin => b"user_builtin"
SiloUser => b"silo_user"
SiloGroup => b"silo_group"
);

impl From<shared::IdentityType> for IdentityType {
fn from(other: shared::IdentityType) -> Self {
match other {
shared::IdentityType::SiloUser => IdentityType::SiloUser,
shared::IdentityType::SiloGroup => IdentityType::SiloGroup,
}
}
}
Expand All @@ -49,6 +51,7 @@ impl TryFrom<IdentityType> for shared::IdentityType {
Err(anyhow!("unsupported db identity type: {:?}", other))
}
IdentityType::SiloUser => Ok(shared::IdentityType::SiloUser),
IdentityType::SiloGroup => Ok(shared::IdentityType::SiloGroup),
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ table! {

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

rcgen -> Int8,
}
}
Expand All @@ -208,6 +209,28 @@ table! {
}
}

table! {
silo_group (id) {
id -> Uuid,
time_created -> Timestamptz,
time_modified -> Timestamptz,
time_deleted -> Nullable<Timestamptz>,

silo_id -> Uuid,
external_id -> Text,
}
}

table! {
silo_group_membership (silo_group_id, silo_user_id) {
silo_group_id -> Uuid,
silo_user_id -> Uuid,
}
}

allow_tables_to_appear_in_same_query!(silo_group, silo_group_membership);
allow_tables_to_appear_in_same_query!(role_assignment, silo_group_membership);

table! {
identity_provider (silo_id, id) {
id -> Uuid,
Expand Down Expand Up @@ -242,6 +265,7 @@ table! {
technical_contact_email -> Text,
public_cert -> Nullable<Text>,
private_key -> Nullable<Text>,
group_attribute_name -> Nullable<Text>,
}
}

Expand Down
40 changes: 40 additions & 0 deletions nexus/db-model/src/silo_group.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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 crate::schema::{silo_group, silo_group_membership};
use db_macros::Asset;
use uuid::Uuid;

/// Describes a silo group within the database.
#[derive(Asset, Queryable, Insertable, Debug, Selectable)]
#[diesel(table_name = silo_group)]
pub struct SiloGroup {
#[diesel(embed)]
identity: SiloGroupIdentity,

pub silo_id: Uuid,

/// The identity provider's name for this group.
pub external_id: String,
}

impl SiloGroup {
pub fn new(id: Uuid, silo_id: Uuid, external_id: String) -> Self {
Self { identity: SiloGroupIdentity::new(id), silo_id, external_id }
}
}

/// Describe which silo users belong to which silo groups
#[derive(Queryable, Insertable, Debug, Selectable)]
#[diesel(table_name = silo_group_membership)]
pub struct SiloGroupMembership {
pub silo_group_id: Uuid,
pub silo_user_id: Uuid,
}

impl SiloGroupMembership {
pub fn new(silo_group_id: Uuid, silo_user_id: Uuid) -> Self {
Self { silo_group_id, silo_user_id }
}
}
Loading