Skip to content

Commit

Permalink
authz: protect various endpoints (#790)
Browse files Browse the repository at this point in the history
  • Loading branch information
davepacheco authored Mar 23, 2022
1 parent b259f45 commit 9102e10
Show file tree
Hide file tree
Showing 21 changed files with 350 additions and 80 deletions.
11 changes: 11 additions & 0 deletions nexus/src/authn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub mod saga;

pub use crate::db::fixed_data::user_builtin::USER_DB_INIT;
pub use crate::db::fixed_data::user_builtin::USER_INTERNAL_API;
pub use crate::db::fixed_data::user_builtin::USER_INTERNAL_READ;
pub use crate::db::fixed_data::user_builtin::USER_SAGA_RECOVERY;
pub use crate::db::fixed_data::user_builtin::USER_TEST_PRIVILEGED;
pub use crate::db::fixed_data::user_builtin::USER_TEST_UNPRIVILEGED;
Expand Down Expand Up @@ -96,6 +97,11 @@ impl Context {
Context::context_for_actor(USER_SAGA_RECOVERY.id)
}

/// Returns an authenticated context for use by internal resource allocation
pub fn internal_read() -> Context {
Context::context_for_actor(USER_INTERNAL_READ.id)
}

/// Returns an authenticated context for Nexus-startup database
/// initialization
pub fn internal_db_init() -> Context {
Expand Down Expand Up @@ -127,6 +133,7 @@ mod test {
use super::Context;
use super::USER_DB_INIT;
use super::USER_INTERNAL_API;
use super::USER_INTERNAL_READ;
use super::USER_SAGA_RECOVERY;
use super::USER_TEST_PRIVILEGED;

Expand All @@ -143,6 +150,10 @@ mod test {
let actor = authn.actor().unwrap();
assert_eq!(actor.0, USER_TEST_PRIVILEGED.id);

let authn = Context::internal_read();
let actor = authn.actor().unwrap();
assert_eq!(actor.0, USER_INTERNAL_READ.id);

let authn = Context::internal_db_init();
let actor = authn.actor().unwrap();
assert_eq!(actor.0, USER_DB_INIT.id);
Expand Down
54 changes: 53 additions & 1 deletion nexus/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ impl Fleet {
}

/// Returns an authz resource representing some other kind of child (e.g.,
/// a built-in user, built-in role, etc. -- but _not_ an Organization)
/// a built-in user, built-in role, etc. -- but _not_ an Organization or
/// Sled)
///
/// Aside from Organizations (which you create with
/// [`Fleet::organization()`] instead), all instances of all types of Fleet
Expand All @@ -149,6 +150,11 @@ impl Fleet {
) -> FleetChild {
FleetChild { resource_type, lookup_type }
}

/// Returns an authz resource representing a Sled
pub fn sled(&self, sled_id: Uuid, lookup_type: LookupType) -> Sled {
Sled { sled_id, lookup_type }
}
}

impl Eq for Fleet {}
Expand Down Expand Up @@ -255,6 +261,52 @@ impl ApiResourceError for FleetChild {
}
}

/// Represents a Sled for authz purposes
///
/// This object is used for authorization checks on such resources by passing
/// this as the `resource` argument to
/// [`crate::context::OpContext::authorize()`]. You construct one of these
/// using [`Fleet::sled()`].
#[derive(Clone, Debug)]
pub struct Sled {
sled_id: Uuid,
lookup_type: LookupType,
}

impl Sled {
pub fn id(&self) -> Uuid {
self.sled_id
}
}

impl oso::PolarClass for Sled {
fn get_polar_class_builder() -> oso::ClassBuilder<Self> {
oso::Class::builder()
.add_method(
"has_role",
// Roles are not supported on Sleds today.
|_: &Sled, _: AuthenticatedActor, _: String| false,
)
.add_attribute_getter("fleet", |_: &Sled| FLEET)
}
}

impl ApiResource for Sled {
fn db_resource(&self) -> Option<(ResourceType, Uuid)> {
None
}

fn parent(&self) -> Option<&dyn AuthorizedResource> {
Some(&FLEET)
}
}

impl ApiResourceError for Sled {
fn not_found(&self) -> Error {
self.lookup_type.clone().into_not_found(ResourceType::Sled)
}
}

/// Represents a [`crate::db::model::Organization`] for authz purposes
///
/// This object is used for authorization checks on an Organization by passing
Expand Down
1 change: 1 addition & 0 deletions nexus/src/authz/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ pub use api_resources::NetworkInterface;
pub use api_resources::Organization;
pub use api_resources::Project;
pub use api_resources::RouterRoute;
pub use api_resources::Sled;
pub use api_resources::Vpc;
pub use api_resources::VpcRouter;
pub use api_resources::VpcSubnet;
Expand Down
33 changes: 27 additions & 6 deletions nexus/src/authz/omicron.polar
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ has_role(actor: AuthenticatedActor, "init", _resource: Database)
#
# - fleet.admin (superuser for the whole system)
# - fleet.collaborator (can create and own orgs)
# - fleet.viewer (can read fleet-wide data)
# - organization.admin (complete control over an organization)
# - organization.collaborator (can create, modify, and delete projects)
# - project.admin (complete control over a project)
Expand All @@ -94,14 +95,17 @@ resource Fleet {
"create_child",
];

roles = [ "admin", "collaborator" ];
roles = [ "admin", "collaborator", "viewer" ];

# Fleet viewers can view Fleet-wide data
"list_children" if "viewer";
"read" if "viewer";

# Fleet collaborators can create Organizations and see fleet-wide
# information, including Organizations that they don't have permissions
# on. (They cannot list projects within those organizations, however.)
# They cannot modify fleet-wide information.
"list_children" if "collaborator";
"read" if "collaborator";
"viewer" if "collaborator";
"create_child" if "collaborator";

# Fleet administrators are whole-system superusers.
Expand Down Expand Up @@ -185,7 +189,7 @@ resource ProjectChild {
}

# Similarly, we use a generic resource to represent every kind of fleet-wide
# resource that's not part of the Organization/Project hierarchy.
# resource that's not part of the Organization/Project hierarchy and not a Sled.
resource FleetChild {
permissions = [
"list_children",
Expand All @@ -195,8 +199,23 @@ resource FleetChild {
];

relations = { parent_fleet: Fleet };
"list_children" if "admin" on "parent_fleet";
"read" if "admin" on "parent_fleet";
"list_children" if "viewer" on "parent_fleet";
"read" if "viewer" on "parent_fleet";
"modify" if "admin" on "parent_fleet";
"create_child" if "admin" on "parent_fleet";
}

resource Sled {
permissions = [
"list_children",
"modify",
"read",
"create_child",
];

relations = { parent_fleet: Fleet };
"list_children" if "viewer" on "parent_fleet";
"read" if "viewer" on "parent_fleet";
"modify" if "admin" on "parent_fleet";
"create_child" if "admin" on "parent_fleet";
}
Expand All @@ -210,6 +229,8 @@ has_relation(project: Project, "parent_project", project_child: ProjectChild)
if project_child.project = project;
has_relation(fleet: Fleet, "parent_fleet", fleet_child: FleetChild)
if fleet_child.fleet = fleet;
has_relation(fleet: Fleet, "parent_fleet", sled: Sled)
if sled.fleet = fleet;

# Define role relationships
has_role(actor: AuthenticatedActor, role: String, resource: Resource)
Expand Down
2 changes: 2 additions & 0 deletions nexus/src/authz/oso_generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use super::api_resources::FleetChild;
use super::api_resources::Organization;
use super::api_resources::Project;
use super::api_resources::ProjectChild;
use super::api_resources::Sled;
use super::context::AuthorizedResource;
use super::roles::RoleSet;
use super::Authz;
Expand Down Expand Up @@ -48,6 +49,7 @@ pub fn make_omicron_oso() -> Result<Oso, anyhow::Error> {
Project::get_polar_class(),
ProjectChild::get_polar_class(),
FleetChild::get_polar_class(),
Sled::get_polar_class(),
];
for c in classes {
oso.register_class(c).context("registering class")?;
Expand Down
41 changes: 28 additions & 13 deletions nexus/src/db/datastore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,30 +158,34 @@ impl DataStore {

pub async fn sled_list(
&self,
opctx: &OpContext,
pagparams: &DataPageParams<'_, Uuid>,
) -> ListResultVec<Sled> {
opctx.authorize(authz::Action::Read, &authz::FLEET).await?;
use db::schema::sled::dsl;
paginated(dsl::sled, dsl::id, pagparams)
.select(Sled::as_select())
.load_async(self.pool())
.load_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

pub async fn sled_fetch(&self, id: Uuid) -> LookupResult<Sled> {
pub async fn sled_fetch(
&self,
opctx: &OpContext,
authz_sled: &authz::Sled,
) -> LookupResult<Sled> {
opctx.authorize(authz::Action::Read, authz_sled).await?;
use db::schema::sled::dsl;
dsl::sled
.filter(dsl::id.eq(id))
.filter(dsl::id.eq(authz_sled.id()))
.select(Sled::as_select())
.first_async(self.pool())
.first_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| {
public_error_from_diesel_pool(
e,
ErrorHandler::NotFoundByLookup(
ResourceType::Sled,
LookupType::ById(id),
),
ErrorHandler::NotFoundByResource(authz_sled),
)
})
}
Expand Down Expand Up @@ -3153,6 +3157,7 @@ impl DataStore {
// Note: "db_init" is also a builtin user, but that one by necessity
// is created with the database.
&*authn::USER_INTERNAL_API,
&*authn::USER_INTERNAL_READ,
&*authn::USER_SAGA_RECOVERY,
&*authn::USER_TEST_PRIVILEGED,
&*authn::USER_TEST_UNPRIVILEGED,
Expand Down Expand Up @@ -3338,30 +3343,37 @@ impl DataStore {

pub async fn update_available_artifact_upsert(
&self,
opctx: &OpContext,
artifact: UpdateAvailableArtifact,
) -> CreateResult<UpdateAvailableArtifact> {
opctx.authorize(authz::Action::Modify, &authz::FLEET).await?;

use db::schema::update_available_artifact::dsl;
diesel::insert_into(dsl::update_available_artifact)
.values(artifact.clone())
.on_conflict((dsl::name, dsl::version, dsl::kind))
.do_update()
.set(artifact.clone())
.returning(UpdateAvailableArtifact::as_returning())
.get_result_async(self.pool())
.get_result_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}

pub async fn update_available_artifact_hard_delete_outdated(
&self,
opctx: &OpContext,
current_targets_role_version: i64,
) -> DeleteResult {
// We use the `targets_role_version` column in the table to delete any old rows, keeping
// the table in sync with the current copy of artifacts.json.
opctx.authorize(authz::Action::Modify, &authz::FLEET).await?;

// We use the `targets_role_version` column in the table to delete any
// old rows, keeping the table in sync with the current copy of
// artifacts.json.
use db::schema::update_available_artifact::dsl;
diesel::delete(dsl::update_available_artifact)
.filter(dsl::targets_role_version.lt(current_targets_role_version))
.execute_async(self.pool())
.execute_async(self.pool_authorized(opctx).await?)
.await
.map(|_rows_deleted| ())
.map_err(|e| {
Expand All @@ -3374,8 +3386,11 @@ impl DataStore {

pub async fn update_available_artifact_fetch(
&self,
opctx: &OpContext,
artifact: &UpdateArtifact,
) -> LookupResult<UpdateAvailableArtifact> {
opctx.authorize(authz::Action::Read, &authz::FLEET).await?;

use db::schema::update_available_artifact::dsl;
dsl::update_available_artifact
.filter(
Expand All @@ -3385,7 +3400,7 @@ impl DataStore {
.and(dsl::kind.eq(UpdateArtifactKind(artifact.kind))),
)
.select(UpdateAvailableArtifact::as_select())
.first_async(self.pool())
.first_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| {
Error::internal_error(&format!(
Expand Down
11 changes: 11 additions & 0 deletions nexus/src/db/fixed_data/role_assignment_builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,16 @@ lazy_static! {
*FLEET_ID,
role_builtin::FLEET_ADMIN.role_name,
),

// The "internal-read" user gets the "viewer" role on the sole Fleet.
// This will grant them the ability to read various control plane
// data (like the list of sleds), which is in turn used to talk to
// sleds or allocate resources.
RoleAssignmentBuiltin::new(
user_builtin::USER_INTERNAL_READ.id,
role_builtin::FLEET_VIEWER.resource_type,
*FLEET_ID,
role_builtin::FLEET_VIEWER.role_name,
),
];
}
6 changes: 6 additions & 0 deletions nexus/src/db/fixed_data/role_builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ lazy_static! {
role_name: "admin",
description: "Fleet Administrator",
};
pub static ref FLEET_VIEWER: RoleBuiltinConfig = RoleBuiltinConfig {
resource_type: api::external::ResourceType::Fleet,
role_name: "viewer",
description: "Fleet Viewer",
};
pub static ref BUILTIN_ROLES: Vec<RoleBuiltinConfig> = vec![
FLEET_ADMIN.clone(),
FLEET_VIEWER.clone(),
RoleBuiltinConfig {
resource_type: api::external::ResourceType::Fleet,
role_name: "collaborator",
Expand Down
11 changes: 11 additions & 0 deletions nexus/src/db/fixed_data/user_builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ lazy_static! {
"used by Nexus when handling internal API requests",
);

/// Internal user used by Nexus to read privileged control plane data
pub static ref USER_INTERNAL_READ: UserBuiltinConfig =
UserBuiltinConfig::new_static(
// "4ead" looks like "read"
"001de000-05e4-4000-8000-000000004ead",
"internal-read",
"used by Nexus to read privileged control plane data",
);

/// Internal user used by Nexus when recovering sagas
pub static ref USER_SAGA_RECOVERY: UserBuiltinConfig =
UserBuiltinConfig::new_static(
Expand Down Expand Up @@ -83,6 +92,7 @@ mod test {
use super::super::assert_valid_uuid;
use super::USER_DB_INIT;
use super::USER_INTERNAL_API;
use super::USER_INTERNAL_READ;
use super::USER_SAGA_RECOVERY;
use super::USER_TEST_PRIVILEGED;
use super::USER_TEST_UNPRIVILEGED;
Expand All @@ -91,6 +101,7 @@ mod test {
fn test_builtin_user_ids_are_valid() {
assert_valid_uuid(&USER_DB_INIT.id);
assert_valid_uuid(&USER_INTERNAL_API.id);
assert_valid_uuid(&USER_INTERNAL_READ.id);
assert_valid_uuid(&USER_SAGA_RECOVERY.id);
assert_valid_uuid(&USER_TEST_PRIVILEGED.id);
assert_valid_uuid(&USER_TEST_UNPRIVILEGED.id);
Expand Down
Loading

0 comments on commit 9102e10

Please sign in to comment.