Skip to content

Commit 9102e10

Browse files
authored
authz: protect various endpoints (#790)
1 parent b259f45 commit 9102e10

File tree

21 files changed

+350
-80
lines changed

21 files changed

+350
-80
lines changed

nexus/src/authn/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub mod saga;
2929

3030
pub use crate::db::fixed_data::user_builtin::USER_DB_INIT;
3131
pub use crate::db::fixed_data::user_builtin::USER_INTERNAL_API;
32+
pub use crate::db::fixed_data::user_builtin::USER_INTERNAL_READ;
3233
pub use crate::db::fixed_data::user_builtin::USER_SAGA_RECOVERY;
3334
pub use crate::db::fixed_data::user_builtin::USER_TEST_PRIVILEGED;
3435
pub use crate::db::fixed_data::user_builtin::USER_TEST_UNPRIVILEGED;
@@ -96,6 +97,11 @@ impl Context {
9697
Context::context_for_actor(USER_SAGA_RECOVERY.id)
9798
}
9899

100+
/// Returns an authenticated context for use by internal resource allocation
101+
pub fn internal_read() -> Context {
102+
Context::context_for_actor(USER_INTERNAL_READ.id)
103+
}
104+
99105
/// Returns an authenticated context for Nexus-startup database
100106
/// initialization
101107
pub fn internal_db_init() -> Context {
@@ -127,6 +133,7 @@ mod test {
127133
use super::Context;
128134
use super::USER_DB_INIT;
129135
use super::USER_INTERNAL_API;
136+
use super::USER_INTERNAL_READ;
130137
use super::USER_SAGA_RECOVERY;
131138
use super::USER_TEST_PRIVILEGED;
132139

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

153+
let authn = Context::internal_read();
154+
let actor = authn.actor().unwrap();
155+
assert_eq!(actor.0, USER_INTERNAL_READ.id);
156+
146157
let authn = Context::internal_db_init();
147158
let actor = authn.actor().unwrap();
148159
assert_eq!(actor.0, USER_DB_INIT.id);

nexus/src/authz/api_resources.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ impl Fleet {
135135
}
136136

137137
/// Returns an authz resource representing some other kind of child (e.g.,
138-
/// a built-in user, built-in role, etc. -- but _not_ an Organization)
138+
/// a built-in user, built-in role, etc. -- but _not_ an Organization or
139+
/// Sled)
139140
///
140141
/// Aside from Organizations (which you create with
141142
/// [`Fleet::organization()`] instead), all instances of all types of Fleet
@@ -149,6 +150,11 @@ impl Fleet {
149150
) -> FleetChild {
150151
FleetChild { resource_type, lookup_type }
151152
}
153+
154+
/// Returns an authz resource representing a Sled
155+
pub fn sled(&self, sled_id: Uuid, lookup_type: LookupType) -> Sled {
156+
Sled { sled_id, lookup_type }
157+
}
152158
}
153159

154160
impl Eq for Fleet {}
@@ -255,6 +261,52 @@ impl ApiResourceError for FleetChild {
255261
}
256262
}
257263

264+
/// Represents a Sled for authz purposes
265+
///
266+
/// This object is used for authorization checks on such resources by passing
267+
/// this as the `resource` argument to
268+
/// [`crate::context::OpContext::authorize()`]. You construct one of these
269+
/// using [`Fleet::sled()`].
270+
#[derive(Clone, Debug)]
271+
pub struct Sled {
272+
sled_id: Uuid,
273+
lookup_type: LookupType,
274+
}
275+
276+
impl Sled {
277+
pub fn id(&self) -> Uuid {
278+
self.sled_id
279+
}
280+
}
281+
282+
impl oso::PolarClass for Sled {
283+
fn get_polar_class_builder() -> oso::ClassBuilder<Self> {
284+
oso::Class::builder()
285+
.add_method(
286+
"has_role",
287+
// Roles are not supported on Sleds today.
288+
|_: &Sled, _: AuthenticatedActor, _: String| false,
289+
)
290+
.add_attribute_getter("fleet", |_: &Sled| FLEET)
291+
}
292+
}
293+
294+
impl ApiResource for Sled {
295+
fn db_resource(&self) -> Option<(ResourceType, Uuid)> {
296+
None
297+
}
298+
299+
fn parent(&self) -> Option<&dyn AuthorizedResource> {
300+
Some(&FLEET)
301+
}
302+
}
303+
304+
impl ApiResourceError for Sled {
305+
fn not_found(&self) -> Error {
306+
self.lookup_type.clone().into_not_found(ResourceType::Sled)
307+
}
308+
}
309+
258310
/// Represents a [`crate::db::model::Organization`] for authz purposes
259311
///
260312
/// This object is used for authorization checks on an Organization by passing

nexus/src/authz/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ pub use api_resources::NetworkInterface;
171171
pub use api_resources::Organization;
172172
pub use api_resources::Project;
173173
pub use api_resources::RouterRoute;
174+
pub use api_resources::Sled;
174175
pub use api_resources::Vpc;
175176
pub use api_resources::VpcRouter;
176177
pub use api_resources::VpcSubnet;

nexus/src/authz/omicron.polar

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ has_role(actor: AuthenticatedActor, "init", _resource: Database)
7575
#
7676
# - fleet.admin (superuser for the whole system)
7777
# - fleet.collaborator (can create and own orgs)
78+
# - fleet.viewer (can read fleet-wide data)
7879
# - organization.admin (complete control over an organization)
7980
# - organization.collaborator (can create, modify, and delete projects)
8081
# - project.admin (complete control over a project)
@@ -94,14 +95,17 @@ resource Fleet {
9495
"create_child",
9596
];
9697

97-
roles = [ "admin", "collaborator" ];
98+
roles = [ "admin", "collaborator", "viewer" ];
99+
100+
# Fleet viewers can view Fleet-wide data
101+
"list_children" if "viewer";
102+
"read" if "viewer";
98103

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

107111
# Fleet administrators are whole-system superusers.
@@ -185,7 +189,7 @@ resource ProjectChild {
185189
}
186190

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

197201
relations = { parent_fleet: Fleet };
198-
"list_children" if "admin" on "parent_fleet";
199-
"read" if "admin" on "parent_fleet";
202+
"list_children" if "viewer" on "parent_fleet";
203+
"read" if "viewer" on "parent_fleet";
204+
"modify" if "admin" on "parent_fleet";
205+
"create_child" if "admin" on "parent_fleet";
206+
}
207+
208+
resource Sled {
209+
permissions = [
210+
"list_children",
211+
"modify",
212+
"read",
213+
"create_child",
214+
];
215+
216+
relations = { parent_fleet: Fleet };
217+
"list_children" if "viewer" on "parent_fleet";
218+
"read" if "viewer" on "parent_fleet";
200219
"modify" if "admin" on "parent_fleet";
201220
"create_child" if "admin" on "parent_fleet";
202221
}
@@ -210,6 +229,8 @@ has_relation(project: Project, "parent_project", project_child: ProjectChild)
210229
if project_child.project = project;
211230
has_relation(fleet: Fleet, "parent_fleet", fleet_child: FleetChild)
212231
if fleet_child.fleet = fleet;
232+
has_relation(fleet: Fleet, "parent_fleet", sled: Sled)
233+
if sled.fleet = fleet;
213234

214235
# Define role relationships
215236
has_role(actor: AuthenticatedActor, role: String, resource: Resource)

nexus/src/authz/oso_generic.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use super::api_resources::FleetChild;
1111
use super::api_resources::Organization;
1212
use super::api_resources::Project;
1313
use super::api_resources::ProjectChild;
14+
use super::api_resources::Sled;
1415
use super::context::AuthorizedResource;
1516
use super::roles::RoleSet;
1617
use super::Authz;
@@ -48,6 +49,7 @@ pub fn make_omicron_oso() -> Result<Oso, anyhow::Error> {
4849
Project::get_polar_class(),
4950
ProjectChild::get_polar_class(),
5051
FleetChild::get_polar_class(),
52+
Sled::get_polar_class(),
5153
];
5254
for c in classes {
5355
oso.register_class(c).context("registering class")?;

nexus/src/db/datastore.rs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -158,30 +158,34 @@ impl DataStore {
158158

159159
pub async fn sled_list(
160160
&self,
161+
opctx: &OpContext,
161162
pagparams: &DataPageParams<'_, Uuid>,
162163
) -> ListResultVec<Sled> {
164+
opctx.authorize(authz::Action::Read, &authz::FLEET).await?;
163165
use db::schema::sled::dsl;
164166
paginated(dsl::sled, dsl::id, pagparams)
165167
.select(Sled::as_select())
166-
.load_async(self.pool())
168+
.load_async(self.pool_authorized(opctx).await?)
167169
.await
168170
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
169171
}
170172

171-
pub async fn sled_fetch(&self, id: Uuid) -> LookupResult<Sled> {
173+
pub async fn sled_fetch(
174+
&self,
175+
opctx: &OpContext,
176+
authz_sled: &authz::Sled,
177+
) -> LookupResult<Sled> {
178+
opctx.authorize(authz::Action::Read, authz_sled).await?;
172179
use db::schema::sled::dsl;
173180
dsl::sled
174-
.filter(dsl::id.eq(id))
181+
.filter(dsl::id.eq(authz_sled.id()))
175182
.select(Sled::as_select())
176-
.first_async(self.pool())
183+
.first_async(self.pool_authorized(opctx).await?)
177184
.await
178185
.map_err(|e| {
179186
public_error_from_diesel_pool(
180187
e,
181-
ErrorHandler::NotFoundByLookup(
182-
ResourceType::Sled,
183-
LookupType::ById(id),
184-
),
188+
ErrorHandler::NotFoundByResource(authz_sled),
185189
)
186190
})
187191
}
@@ -3153,6 +3157,7 @@ impl DataStore {
31533157
// Note: "db_init" is also a builtin user, but that one by necessity
31543158
// is created with the database.
31553159
&*authn::USER_INTERNAL_API,
3160+
&*authn::USER_INTERNAL_READ,
31563161
&*authn::USER_SAGA_RECOVERY,
31573162
&*authn::USER_TEST_PRIVILEGED,
31583163
&*authn::USER_TEST_UNPRIVILEGED,
@@ -3338,30 +3343,37 @@ impl DataStore {
33383343

33393344
pub async fn update_available_artifact_upsert(
33403345
&self,
3346+
opctx: &OpContext,
33413347
artifact: UpdateAvailableArtifact,
33423348
) -> CreateResult<UpdateAvailableArtifact> {
3349+
opctx.authorize(authz::Action::Modify, &authz::FLEET).await?;
3350+
33433351
use db::schema::update_available_artifact::dsl;
33443352
diesel::insert_into(dsl::update_available_artifact)
33453353
.values(artifact.clone())
33463354
.on_conflict((dsl::name, dsl::version, dsl::kind))
33473355
.do_update()
33483356
.set(artifact.clone())
33493357
.returning(UpdateAvailableArtifact::as_returning())
3350-
.get_result_async(self.pool())
3358+
.get_result_async(self.pool_authorized(opctx).await?)
33513359
.await
33523360
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
33533361
}
33543362

33553363
pub async fn update_available_artifact_hard_delete_outdated(
33563364
&self,
3365+
opctx: &OpContext,
33573366
current_targets_role_version: i64,
33583367
) -> DeleteResult {
3359-
// We use the `targets_role_version` column in the table to delete any old rows, keeping
3360-
// the table in sync with the current copy of artifacts.json.
3368+
opctx.authorize(authz::Action::Modify, &authz::FLEET).await?;
3369+
3370+
// We use the `targets_role_version` column in the table to delete any
3371+
// old rows, keeping the table in sync with the current copy of
3372+
// artifacts.json.
33613373
use db::schema::update_available_artifact::dsl;
33623374
diesel::delete(dsl::update_available_artifact)
33633375
.filter(dsl::targets_role_version.lt(current_targets_role_version))
3364-
.execute_async(self.pool())
3376+
.execute_async(self.pool_authorized(opctx).await?)
33653377
.await
33663378
.map(|_rows_deleted| ())
33673379
.map_err(|e| {
@@ -3374,8 +3386,11 @@ impl DataStore {
33743386

33753387
pub async fn update_available_artifact_fetch(
33763388
&self,
3389+
opctx: &OpContext,
33773390
artifact: &UpdateArtifact,
33783391
) -> LookupResult<UpdateAvailableArtifact> {
3392+
opctx.authorize(authz::Action::Read, &authz::FLEET).await?;
3393+
33793394
use db::schema::update_available_artifact::dsl;
33803395
dsl::update_available_artifact
33813396
.filter(
@@ -3385,7 +3400,7 @@ impl DataStore {
33853400
.and(dsl::kind.eq(UpdateArtifactKind(artifact.kind))),
33863401
)
33873402
.select(UpdateAvailableArtifact::as_select())
3388-
.first_async(self.pool())
3403+
.first_async(self.pool_authorized(opctx).await?)
33893404
.await
33903405
.map_err(|e| {
33913406
Error::internal_error(&format!(

nexus/src/db/fixed_data/role_assignment_builtin.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,16 @@ lazy_static! {
3131
*FLEET_ID,
3232
role_builtin::FLEET_ADMIN.role_name,
3333
),
34+
35+
// The "internal-read" user gets the "viewer" role on the sole Fleet.
36+
// This will grant them the ability to read various control plane
37+
// data (like the list of sleds), which is in turn used to talk to
38+
// sleds or allocate resources.
39+
RoleAssignmentBuiltin::new(
40+
user_builtin::USER_INTERNAL_READ.id,
41+
role_builtin::FLEET_VIEWER.resource_type,
42+
*FLEET_ID,
43+
role_builtin::FLEET_VIEWER.role_name,
44+
),
3445
];
3546
}

nexus/src/db/fixed_data/role_builtin.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ lazy_static! {
1919
role_name: "admin",
2020
description: "Fleet Administrator",
2121
};
22+
pub static ref FLEET_VIEWER: RoleBuiltinConfig = RoleBuiltinConfig {
23+
resource_type: api::external::ResourceType::Fleet,
24+
role_name: "viewer",
25+
description: "Fleet Viewer",
26+
};
2227
pub static ref BUILTIN_ROLES: Vec<RoleBuiltinConfig> = vec![
2328
FLEET_ADMIN.clone(),
29+
FLEET_VIEWER.clone(),
2430
RoleBuiltinConfig {
2531
resource_type: api::external::ResourceType::Fleet,
2632
role_name: "collaborator",

nexus/src/db/fixed_data/user_builtin.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ lazy_static! {
4747
"used by Nexus when handling internal API requests",
4848
);
4949

50+
/// Internal user used by Nexus to read privileged control plane data
51+
pub static ref USER_INTERNAL_READ: UserBuiltinConfig =
52+
UserBuiltinConfig::new_static(
53+
// "4ead" looks like "read"
54+
"001de000-05e4-4000-8000-000000004ead",
55+
"internal-read",
56+
"used by Nexus to read privileged control plane data",
57+
);
58+
5059
/// Internal user used by Nexus when recovering sagas
5160
pub static ref USER_SAGA_RECOVERY: UserBuiltinConfig =
5261
UserBuiltinConfig::new_static(
@@ -83,6 +92,7 @@ mod test {
8392
use super::super::assert_valid_uuid;
8493
use super::USER_DB_INIT;
8594
use super::USER_INTERNAL_API;
95+
use super::USER_INTERNAL_READ;
8696
use super::USER_SAGA_RECOVERY;
8797
use super::USER_TEST_PRIVILEGED;
8898
use super::USER_TEST_UNPRIVILEGED;
@@ -91,6 +101,7 @@ mod test {
91101
fn test_builtin_user_ids_are_valid() {
92102
assert_valid_uuid(&USER_DB_INIT.id);
93103
assert_valid_uuid(&USER_INTERNAL_API.id);
104+
assert_valid_uuid(&USER_INTERNAL_READ.id);
94105
assert_valid_uuid(&USER_SAGA_RECOVERY.id);
95106
assert_valid_uuid(&USER_TEST_PRIVILEGED.id);
96107
assert_valid_uuid(&USER_TEST_UNPRIVILEGED.id);

0 commit comments

Comments
 (0)