Skip to content

Commit c08906c

Browse files
authored
Add support for Silo groups (#1358)
Add the necessary logic to: - provision an "admin" silo group when a silo is created, which is granted silo admin role. - after successful authentication, create groups during silo user provision if the Silo's provision type is JIT. - add a group's roles to a user's role set if they're part of that group. Silos now have an optional admin_group_name that is configured at silo provision time. If this is left out, users will currently have no way to be granted roles when they first log in. In the future, this may be selected and groups would be created another way. SAML identity providers now have an optional group_attribute_name that configures what attribute represents a group name. Groups can be passed in multiple attribute values, or in one as a comma separated list.
1 parent c7a2a47 commit c08906c

File tree

33 files changed

+1542
-119
lines changed

33 files changed

+1542
-119
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/api/external/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ pub enum ResourceType {
528528
Fleet,
529529
Silo,
530530
SiloUser,
531+
SiloGroup,
531532
IdentityProvider,
532533
SamlIdentityProvider,
533534
SshKey,

common/src/sql/dbinit.sql

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,50 @@ CREATE UNIQUE INDEX ON omicron.public.silo_user (
277277
) WHERE
278278
time_deleted IS NULL;
279279

280-
CREATE TYPE omicron.public.provider_type AS ENUM (
281-
'saml'
280+
/*
281+
* Silo groups
282+
*/
283+
284+
CREATE TABLE omicron.public.silo_group (
285+
id UUID PRIMARY KEY,
286+
time_created TIMESTAMPTZ NOT NULL,
287+
time_modified TIMESTAMPTZ NOT NULL,
288+
time_deleted TIMESTAMPTZ,
289+
290+
silo_id UUID NOT NULL,
291+
external_id TEXT NOT NULL
292+
);
293+
294+
CREATE UNIQUE INDEX ON omicron.public.silo_group (
295+
silo_id,
296+
external_id
297+
) WHERE
298+
time_deleted IS NULL;
299+
300+
/*
301+
* Silo group membership
302+
*/
303+
304+
CREATE TABLE omicron.public.silo_group_membership (
305+
silo_group_id UUID NOT NULL,
306+
silo_user_id UUID NOT NULL,
307+
308+
PRIMARY KEY (silo_group_id, silo_user_id)
309+
);
310+
311+
CREATE INDEX ON omicron.public.silo_group_membership (
312+
silo_user_id,
313+
silo_group_id
282314
);
283315

284316
/*
285317
* Silo identity provider list
286318
*/
319+
320+
CREATE TYPE omicron.public.provider_type AS ENUM (
321+
'saml'
322+
);
323+
287324
CREATE TABLE omicron.public.identity_provider (
288325
/* Identity metadata */
289326
id UUID PRIMARY KEY,
@@ -332,7 +369,9 @@ CREATE TABLE omicron.public.saml_identity_provider (
332369
technical_contact_email TEXT NOT NULL,
333370

334371
public_cert TEXT,
335-
private_key TEXT
372+
private_key TEXT,
373+
374+
group_attribute_name TEXT
336375
);
337376

338377
CREATE INDEX ON omicron.public.saml_identity_provider (
@@ -1442,7 +1481,8 @@ CREATE TABLE omicron.public.role_builtin (
14421481

14431482
CREATE TYPE omicron.public.identity_type AS ENUM (
14441483
'user_builtin',
1445-
'silo_user'
1484+
'silo_user',
1485+
'silo_group'
14461486
);
14471487

14481488
CREATE TABLE omicron.public.role_assignment (

nexus/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ path = "../rpaths"
99

1010
[dependencies]
1111
anyhow = "1.0"
12-
async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", rev = "ab1f49e0b3f95557aa96bf593282199fafeef4bd" }
12+
async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", rev = "51de79fe02b334899be5d5fd8b469f9d140ea887" }
1313
async-trait = "0.1.56"
1414
base64 = "0.13.0"
1515
bb8 = "0.8.0"

nexus/db-macros/src/lookup.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub struct Input {
2222
/// Name of the resource
2323
///
2424
/// This is taken as the name of the database model type in
25-
/// `omicron_nexus::db::model`, the name of the authz type in
25+
/// `omicron_nexus::db_model`, the name of the authz type in
2626
/// `omicron_nexus::authz`, and will be the name of the new type created by
2727
/// this macro. The snake case version of the name is taken as the name of
2828
/// the Diesel table interface in `db::schema`.
@@ -537,6 +537,13 @@ fn generate_lookup_methods(config: &Config) -> TokenStream {
537537
self.fetch_for(authz::Action::Read).await
538538
}
539539

540+
/// Turn the Result<T, E> of [`fetch`] into a Result<Option<T>, E>.
541+
pub async fn optional_fetch(
542+
&self,
543+
) -> LookupResult<Option<(#(authz::#path_types,)* nexus_db_model::#resource_name)>> {
544+
self.optional_fetch_for(authz::Action::Read).await
545+
}
546+
540547
/// Fetch the record corresponding to the selected resource and
541548
/// check whether the caller is allowed to do the specified `action`
542549
///
@@ -571,6 +578,25 @@ fn generate_lookup_methods(config: &Config) -> TokenStream {
571578
#silo_check_fetch
572579
}
573580

581+
/// Turn the Result<T, E> of [`fetch_for`] into a Result<Option<T>, E>.
582+
pub async fn optional_fetch_for(
583+
&self,
584+
action: authz::Action,
585+
) -> LookupResult<Option<(#(authz::#path_types,)* nexus_db_model::#resource_name)>> {
586+
let result = self.fetch_for(action).await;
587+
588+
match result {
589+
Err(Error::ObjectNotFound {
590+
type_name: _,
591+
lookup_type: _,
592+
}) => Ok(None),
593+
594+
_ => {
595+
Ok(Some(result?))
596+
}
597+
}
598+
}
599+
574600
/// Fetch an `authz` object for the selected resource and check
575601
/// whether the caller is allowed to do the specified `action`
576602
///
@@ -592,6 +618,25 @@ fn generate_lookup_methods(config: &Config) -> TokenStream {
592618
#silo_check_lookup
593619
}
594620

621+
/// Turn the Result<T, E> of [`lookup_for`] into a Result<Option<T>, E>.
622+
pub async fn optional_lookup_for(
623+
&self,
624+
action: authz::Action,
625+
) -> LookupResult<Option<(#(authz::#path_types,)*)>> {
626+
let result = self.lookup_for(action).await;
627+
628+
match result {
629+
Err(Error::ObjectNotFound {
630+
type_name: _,
631+
lookup_type: _,
632+
}) => Ok(None),
633+
634+
_ => {
635+
Ok(Some(result?))
636+
}
637+
}
638+
}
639+
595640
/// Fetch the "authz" objects for the selected resource and all its
596641
/// parents
597642
///

nexus/db-model/src/identity_provider.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,31 @@ pub struct SamlIdentityProvider {
6060

6161
pub silo_id: Uuid,
6262

63+
/// idp descriptor
6364
pub idp_metadata_document_string: String,
6465

66+
/// idp's entity id
6567
pub idp_entity_id: String,
68+
69+
/// sp's client id
6670
pub sp_client_id: String,
71+
72+
/// service provider endpoint where the response will be sent
6773
pub acs_url: String,
74+
75+
/// service provider endpoint where the idp should send log out requests
6876
pub slo_url: String,
77+
78+
/// customer's technical contact for saml configuration
6979
pub technical_contact_email: String,
80+
81+
/// base64 encoded DER corresponding to X509 pair
7082
pub public_cert: Option<String>,
7183
pub private_key: Option<String>,
84+
85+
/// if set, attributes with this name will be considered to denote a user's
86+
/// group membership, where the values will be the group names.
87+
pub group_attribute_name: Option<String>,
7288
}
7389

7490
impl From<SamlIdentityProvider> for views::SamlIdentityProvider {

nexus/db-model/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub mod schema;
4848
mod service;
4949
mod service_kind;
5050
mod silo;
51+
mod silo_group;
5152
mod silo_user;
5253
mod sled;
5354
mod snapshot;
@@ -110,6 +111,7 @@ pub use role_builtin::*;
110111
pub use service::*;
111112
pub use service_kind::*;
112113
pub use silo::*;
114+
pub use silo_group::*;
113115
pub use silo_user::*;
114116
pub use sled::*;
115117
pub use snapshot::*;

nexus/db-model/src/role_assignment.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ impl_enum_type!(
3030
// Enum values
3131
UserBuiltin => b"user_builtin"
3232
SiloUser => b"silo_user"
33+
SiloGroup => b"silo_group"
3334
);
3435

3536
impl From<shared::IdentityType> for IdentityType {
3637
fn from(other: shared::IdentityType) -> Self {
3738
match other {
3839
shared::IdentityType::SiloUser => IdentityType::SiloUser,
40+
shared::IdentityType::SiloGroup => IdentityType::SiloGroup,
3941
}
4042
}
4143
}
@@ -49,6 +51,7 @@ impl TryFrom<IdentityType> for shared::IdentityType {
4951
Err(anyhow!("unsupported db identity type: {:?}", other))
5052
}
5153
IdentityType::SiloUser => Ok(shared::IdentityType::SiloUser),
54+
IdentityType::SiloGroup => Ok(shared::IdentityType::SiloGroup),
5255
}
5356
}
5457
}

nexus/db-model/src/schema.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ table! {
193193

194194
discoverable -> Bool,
195195
user_provision_type -> crate::UserProvisionTypeEnum,
196+
196197
rcgen -> Int8,
197198
}
198199
}
@@ -209,6 +210,28 @@ table! {
209210
}
210211
}
211212

213+
table! {
214+
silo_group (id) {
215+
id -> Uuid,
216+
time_created -> Timestamptz,
217+
time_modified -> Timestamptz,
218+
time_deleted -> Nullable<Timestamptz>,
219+
220+
silo_id -> Uuid,
221+
external_id -> Text,
222+
}
223+
}
224+
225+
table! {
226+
silo_group_membership (silo_group_id, silo_user_id) {
227+
silo_group_id -> Uuid,
228+
silo_user_id -> Uuid,
229+
}
230+
}
231+
232+
allow_tables_to_appear_in_same_query!(silo_group, silo_group_membership);
233+
allow_tables_to_appear_in_same_query!(role_assignment, silo_group_membership);
234+
212235
table! {
213236
identity_provider (silo_id, id) {
214237
id -> Uuid,
@@ -243,6 +266,7 @@ table! {
243266
technical_contact_email -> Text,
244267
public_cert -> Nullable<Text>,
245268
private_key -> Nullable<Text>,
269+
group_attribute_name -> Nullable<Text>,
246270
}
247271
}
248272

nexus/db-model/src/silo_group.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
use crate::schema::{silo_group, silo_group_membership};
6+
use db_macros::Asset;
7+
use uuid::Uuid;
8+
9+
/// Describes a silo group within the database.
10+
#[derive(Asset, Queryable, Insertable, Debug, Selectable)]
11+
#[diesel(table_name = silo_group)]
12+
pub struct SiloGroup {
13+
#[diesel(embed)]
14+
identity: SiloGroupIdentity,
15+
16+
pub silo_id: Uuid,
17+
18+
/// The identity provider's name for this group.
19+
pub external_id: String,
20+
}
21+
22+
impl SiloGroup {
23+
pub fn new(id: Uuid, silo_id: Uuid, external_id: String) -> Self {
24+
Self { identity: SiloGroupIdentity::new(id), silo_id, external_id }
25+
}
26+
}
27+
28+
/// Describe which silo users belong to which silo groups
29+
#[derive(Queryable, Insertable, Debug, Selectable)]
30+
#[diesel(table_name = silo_group_membership)]
31+
pub struct SiloGroupMembership {
32+
pub silo_group_id: Uuid,
33+
pub silo_user_id: Uuid,
34+
}
35+
36+
impl SiloGroupMembership {
37+
pub fn new(silo_group_id: Uuid, silo_user_id: Uuid) -> Self {
38+
Self { silo_group_id, silo_user_id }
39+
}
40+
}

0 commit comments

Comments
 (0)