Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial implementation of local-only users #1784

Merged
merged 17 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
32 changes: 27 additions & 5 deletions nexus/src/app/iam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ impl super::Nexus {

// Silo users

pub async fn silo_users_list(
/// List users in the current Silo
pub async fn silo_users_list_current(
&self,
opctx: &OpContext,
pagparams: &DataPageParams<'_, Uuid>,
Expand All @@ -67,23 +68,44 @@ impl super::Nexus {
.authn
.silo_required()
.internal_context("listing current silo's users")?;
let authz_silo_user_list = authz::SiloUserList::new(authz_silo.clone());
self.db_datastore
.silo_users_list_by_id(opctx, &authz_silo, pagparams)
.silo_users_list_by_id(opctx, &authz_silo_user_list, pagparams)
.await
}

pub async fn silo_user_fetch_by_id(
/// Fetch the currently-authenticated Silo user
pub async fn silo_user_fetch_self(
&self,
opctx: &OpContext,
silo_user_id: &Uuid,
) -> LookupResult<db::model::SiloUser> {
let &actor = opctx
.authn
.actor_required()
.internal_context("loading current user")?;
let (.., db_silo_user) = LookupPath::new(opctx, &self.db_datastore)
.silo_user_id(*silo_user_id)
.silo_user_id(actor.actor_id())
.fetch()
.await?;
Ok(db_silo_user)
}

pub async fn silo_users_list(
&self,
opctx: &OpContext,
silo_name: &Name,
pagparams: &DataPageParams<'_, Uuid>,
) -> ListResultVec<db::model::SiloUser> {
let (authz_silo, ..) = LookupPath::new(opctx, &self.db_datastore)
.silo_name(silo_name)
.fetch()
.await?;
let authz_silo_user_list = authz::SiloUserList::new(authz_silo);
self.db_datastore
.silo_users_list_by_id(opctx, &authz_silo_user_list, pagparams)
.await
}

// Built-in users

pub async fn users_builtin_list(
Expand Down
89 changes: 86 additions & 3 deletions nexus/src/app/silo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

//! Silos, Users, and SSH Keys.

use crate::authz::ApiResource;
use crate::context::OpContext;
use crate::db;
use crate::db::identity::{Asset, Resource};
Expand All @@ -14,6 +15,7 @@ use crate::external_api::params;
use crate::external_api::shared;
use crate::{authn, authz};
use anyhow::Context;
use nexus_db_model::UserProvisionType;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::DataPageParams;
use omicron_common::api::external::DeleteResult;
Expand Down Expand Up @@ -140,16 +142,91 @@ impl super::Nexus {

// Users

pub async fn silo_api_user_create(
&self,
opctx: &OpContext,
silo_name: &Name,
new_user_params: params::UserCreate,
) -> CreateResult<db::model::SiloUser> {
let datastore = self.datastore();
let (authz_silo, db_silo) = LookupPath::new(opctx, &datastore)
.silo_name(silo_name)
.fetch()
.await?;

if db_silo.user_provision_type != UserProvisionType::ApiOnly {
return Err(Error::invalid_request(
"cannot create users in this kind of Silo",
));
}

let silo_user = db::model::SiloUser::new(
authz_silo.id(),
Uuid::new_v4(),
new_user_params.external_id.as_ref().to_owned(),
);
let authz_silo_user_list = authz::SiloUserList::new(authz_silo.clone());

// TODO-cleanup This authz check belongs in silo_user_create().
opctx
.authorize(authz::Action::CreateChild, &authz_silo_user_list)
.await?;
let (_, db_silo_user) =
datastore.silo_user_create(&authz_silo, silo_user).await?;
Ok(db_silo_user)
}

pub async fn silo_api_user_delete(
jmpesp marked this conversation as resolved.
Show resolved Hide resolved
&self,
opctx: &OpContext,
silo_name: &Name,
silo_user_id: Uuid,
) -> DeleteResult {
// Verify that this user is actually in this Silo.
let datastore = self.datastore();
let (authz_silo, db_silo) = LookupPath::new(opctx, datastore)
.silo_name(silo_name)
.fetch()
.await?;
let (_, authz_silo_user, db_silo_user) =
LookupPath::new(opctx, datastore)
.silo_user_id(silo_user_id)
.fetch_for(authz::Action::Delete)
.await?;
if db_silo_user.silo_id != authz_silo.id() {
return Err(authz_silo_user.not_found());
}

if db_silo.user_provision_type != UserProvisionType::ApiOnly {
return Err(Error::invalid_request(
"cannot delete users in this kind of Silo",
));
}

self.db_datastore.silo_user_delete(opctx, &authz_silo_user).await
}

pub async fn silo_user_fetch(
&self,
opctx: &OpContext,
silo_name: &Name,
silo_user_id: Uuid,
) -> LookupResult<db::model::SiloUser> {
let (.., db_silo_user) = LookupPath::new(opctx, &self.datastore())
.silo_user_id(silo_user_id)
let datastore = self.datastore();
let (authz_silo_requested, _) = LookupPath::new(opctx, datastore)
.silo_name(silo_name)
.fetch()
.await?;
Ok(db_silo_user)
let (_, authz_silo_user, db_silo_user) =
LookupPath::new(opctx, datastore)
.silo_user_id(silo_user_id)
.fetch()
.await?;
if db_silo_user.silo_id != authz_silo_requested.id() {
Err(authz_silo_user.not_found())
} else {
Ok(db_silo_user)
}
}

/// Based on an authenticated subject, fetch or create a silo user
Expand Down Expand Up @@ -378,6 +455,12 @@ impl super::Nexus {
.await?;
let authz_idp_list = authz::SiloIdentityProviderList::new(authz_silo);

if db_silo.user_provision_type != UserProvisionType::Jit {
return Err(Error::invalid_request(
"cannot create identity providers in this kind of Silo",
));
}

// This check is not strictly necessary yet. We'll check this
// permission in the DataStore when we actually update the list.
// But we check now to protect the code that fetches the descriptor from
Expand Down
57 changes: 57 additions & 0 deletions nexus/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,63 @@ impl AuthorizedResource for SiloIdentityProviderList {
}
}

/// Synthetic resource describing the list of Users in a Silo
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SiloUserList(Silo);

impl SiloUserList {
pub fn new(silo: Silo) -> SiloUserList {
SiloUserList(silo)
}

pub fn silo(&self) -> &Silo {
&self.0
}
}

impl oso::PolarClass for SiloUserList {
fn get_polar_class_builder() -> oso::ClassBuilder<Self> {
oso::Class::builder()
.with_equality_check()
.add_attribute_getter("silo", |list: &SiloUserList| list.0.clone())
}
}

impl AuthorizedResource for SiloUserList {
fn load_roles<'a, 'b, 'c, 'd, 'e, 'f>(
&'a self,
opctx: &'b OpContext,
datastore: &'c DataStore,
authn: &'d authn::Context,
roleset: &'e mut RoleSet,
) -> futures::future::BoxFuture<'f, Result<(), Error>>
where
'a: 'f,
'b: 'f,
'c: 'f,
'd: 'f,
'e: 'f,
{
// There are no roles on this resource, but we still need to load the
// Silo-related roles.
self.silo().load_roles(opctx, datastore, authn, roleset)
}

fn on_unauthorized(
&self,
_: &Authz,
error: Error,
_: AnyActor,
_: Action,
) -> Error {
error
}

fn polar_class(&self) -> oso::Class {
Self::get_polar_class()
}
}

// Main resource hierarchy: Organizations, Projects, and their resources

authz_resource! {
Expand Down
40 changes: 35 additions & 5 deletions nexus/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ has_relation(silo: Silo, "parent_silo", user: SiloUser)
has_permission(actor: AuthenticatedActor, _perm: String, silo_user: SiloUser)
if actor.equals_silo_user(silo_user);

has_permission(actor: AuthenticatedActor, "read", silo_user: SiloUser)
if silo_user.silo in actor.silo;

resource SiloGroup {
permissions = [
"list_children",
Expand Down Expand Up @@ -413,11 +416,12 @@ resource SiloIdentityProviderList {
"list_children" if "read" on "parent_silo";

# Fleet and Silo administrators can manage the Silo's identity provider
# configuration. This is the only area of Silo configuration that Fleet
# Administrators have permissions on. This is also the only case (so
# far) where we need to look two levels up the hierarchy to see if
# somebody has the right permission. For most other things, permissions
# cascade down the hierarchy so we only need to look at the parent.
# configuration. This is one of the only areas of Silo configuration
# that Fleet Administrators have permissions on. This is also one of
# the only cases where we need to look two levels up the hierarchy to
# see if somebody has the right permission. For most other things,
# permissions cascade down the hierarchy so we only need to look at the
# parent.
"create_child" if "admin" on "parent_silo";
"create_child" if "admin" on "parent_fleet";
}
Expand All @@ -426,6 +430,32 @@ has_relation(silo: Silo, "parent_silo", collection: SiloIdentityProviderList)
has_relation(fleet: Fleet, "parent_fleet", collection: SiloIdentityProviderList)
if collection.silo.fleet = fleet;

# Describes the policy for creating and managing Silo users (mostly intended for
# API-managed users)
resource SiloUserList {
permissions = [ "list_children", "create_child" ];

relations = { parent_silo: Silo, parent_fleet: Fleet };

# Everyone who can read the Silo (which includes all the users in the
# Silo) can see the users in it.
"list_children" if "read" on "parent_silo";

# Fleet and Silo administrators can manage the Silo's users. This is
# one of the only areas of Silo configuration that Fleet Administrators
# have permissions on. This is also one of the few cases (so far) where
# we need to look two levels up the hierarchy to see if somebody has the
# right permission. For most other things, permissions cascade down the
# hierarchy so we only need to look at the parent.
"create_child" if "admin" on "parent_silo";
"list_children" if "admin" on "parent_fleet";
"create_child" if "admin" on "parent_fleet";
}
has_relation(silo: Silo, "parent_silo", collection: SiloUserList)
if collection.silo = silo;
has_relation(fleet: Fleet, "parent_fleet", collection: SiloUserList)
if collection.silo.fleet = fleet;

# These rules grants the external authenticator role the permissions it needs to
# read silo users and modify their sessions. This is necessary for login to
# work.
Expand Down
1 change: 1 addition & 0 deletions nexus/src/authz/oso_generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<OsoInit, anyhow::Error> {
ConsoleSessionList::get_polar_class(),
DeviceAuthRequestList::get_polar_class(),
SiloIdentityProviderList::get_polar_class(),
SiloUserList::get_polar_class(),
];
for c in classes {
oso_builder = oso_builder.register_class(c)?;
Expand Down
17 changes: 17 additions & 0 deletions nexus/src/authz/policy_test/resource_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,20 @@ impl DynAuthorizedResource for authz::SiloIdentityProviderList {
format!("{}: identity provider list", self.silo().resource_name())
}
}

impl DynAuthorizedResource for authz::SiloUserList {
fn do_authorize<'a, 'b>(
&'a self,
opctx: &'b OpContext,
action: authz::Action,
) -> BoxFuture<'a, Result<(), Error>>
where
'b: 'a,
{
opctx.authorize(action, self).boxed()
}

fn resource_name(&self) -> String {
format!("{}: user list", self.silo().resource_name())
}
}
1 change: 1 addition & 0 deletions nexus/src/authz/policy_test/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ async fn make_silo(
}

builder.new_resource(authz::SiloIdentityProviderList::new(silo.clone()));
builder.new_resource(authz::SiloUserList::new(silo.clone()));

let norganizations = if first_branch { 2 } else { 1 };
for i in 0..norganizations {
Expand Down
Loading