Skip to content

authz: protect disk list, disk create endpoints #661

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

Merged
merged 11 commits into from
Feb 3, 2022
10 changes: 6 additions & 4 deletions nexus/src/db/datastore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,16 +1197,18 @@ impl DataStore {

pub async fn project_list_disks(
&self,
project_id: &Uuid,
opctx: &OpContext,
authz_project: &authz::Project,
pagparams: &DataPageParams<'_, Name>,
) -> ListResultVec<Disk> {
use db::schema::disk::dsl;
opctx.authorize(authz::Action::ListChildren, authz_project).await?;

use db::schema::disk::dsl;
paginated(dsl::disk, dsl::name, &pagparams)
.filter(dsl::time_deleted.is_null())
.filter(dsl::project_id.eq(*project_id))
.filter(dsl::project_id.eq(authz_project.id()))
.select(Disk::as_select())
.load_async::<Disk>(self.pool())
.load_async::<Disk>(self.pool_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}
Expand Down
4 changes: 4 additions & 0 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,8 +577,10 @@ async fn project_disks_get(
let organization_name = &path.organization_name;
let project_name = &path.project_name;
let handler = async {
let opctx = OpContext::for_external_api(&rqctx).await?;
let disks = nexus
.project_list_disks(
&opctx,
organization_name,
project_name,
&data_page_params_for(&rqctx, &query)?
Expand Down Expand Up @@ -616,8 +618,10 @@ async fn project_disks_post(
let project_name = &path.project_name;
let new_disk_params = &new_disk.into_inner();
let handler = async {
let opctx = OpContext::for_external_api(&rqctx).await?;
let disk = nexus
.project_create_disk(
&opctx,
&organization_name,
&project_name,
&new_disk_params,
Expand Down
16 changes: 12 additions & 4 deletions nexus/src/nexus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,20 +665,23 @@ impl Nexus {

pub async fn project_list_disks(
&self,
opctx: &OpContext,
organization_name: &Name,
project_name: &Name,
pagparams: &DataPageParams<'_, Name>,
) -> ListResultVec<db::model::Disk> {
let project_id = self
let authz_project = self
.db_datastore
.project_lookup_path(organization_name, project_name)
.await?
.id();
self.db_datastore.project_list_disks(&project_id, pagparams).await
.await?;
self.db_datastore
.project_list_disks(opctx, &authz_project, pagparams)
.await
}

pub async fn project_create_disk(
self: &Arc<Self>,
opctx: &OpContext,
organization_name: &Name,
project_name: &Name,
params: &params::DiskCreate,
Expand All @@ -688,6 +691,11 @@ impl Nexus {
.project_lookup_path(organization_name, project_name)
.await?;

// TODO-security This may need to be revisited once we implement authz
// checks for saga actions. Even then, though, it still will be correct
// (if possibly redundant) to check this here.
opctx.authorize(authz::Action::CreateChild, &authz_project).await?;

/*
* Until we implement snapshots, do not allow disks to be created with a
* snapshot id.
Expand Down
6 changes: 4 additions & 2 deletions nexus/test-utils/src/http_testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -514,15 +514,17 @@ impl<'a> NexusRequest<'a> {
testctx: &'a ClientTestContext,
collection_url: &str,
initial_params: &str,
limit: usize,
limit: Option<usize>,
) -> Result<Collection<T>, anyhow::Error>
where
T: Clone + serde::de::DeserializeOwned,
{
let url_base = format!("{}?limit={}&", collection_url, limit);
let mut npages = 0;
let mut all_items = Vec::new();
let mut next_token: Option<String> = None;
const DEFAULT_PAGE_SIZE: usize = 10;
let limit = limit.unwrap_or(DEFAULT_PAGE_SIZE);
let url_base = format!("{}?limit={}&", collection_url, limit);

loop {
let url = if let Some(next_token) = &next_token {
Expand Down
75 changes: 57 additions & 18 deletions nexus/test-utils/src/resource_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use dropshot::test_util::ClientTestContext;
use dropshot::HttpErrorResponseBody;
use dropshot::Method;
use http::StatusCode;
use omicron_common::api::external::ByteCount;
use omicron_common::api::external::Disk;
use omicron_common::api::external::IdentityMetadataCreateParams;
use omicron_common::api::external::VpcRouter;
use omicron_nexus::external_api::params;
Expand All @@ -30,46 +32,83 @@ where
.unwrap()
}

pub async fn create_organization(
pub async fn object_create<InputType, OutputType>(
client: &ClientTestContext,
organization_name: &str,
) -> Organization {
let input = params::OrganizationCreate {
identity: IdentityMetadataCreateParams {
name: organization_name.parse().unwrap(),
description: "an org".to_string(),
},
};
NexusRequest::objects_post(client, "/organizations", &input)
path: &str,
input: &InputType,
) -> OutputType
where
InputType: serde::Serialize,
OutputType: serde::de::DeserializeOwned,
{
NexusRequest::objects_post(client, path, input)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.expect("failed to make request")
.expect("failed to make \"create\" request")
.parsed_body()
.unwrap()
}

pub async fn create_organization(
client: &ClientTestContext,
organization_name: &str,
) -> Organization {
object_create(
client,
"/organizations",
&params::OrganizationCreate {
identity: IdentityMetadataCreateParams {
name: organization_name.parse().unwrap(),
description: "an org".to_string(),
},
},
)
.await
}

pub async fn create_project(
client: &ClientTestContext,
organization_name: &str,
project_name: &str,
) -> Project {
NexusRequest::objects_post(
let url = format!("/organizations/{}/projects", &organization_name);
object_create(
client,
&format!("/organizations/{}/projects", &organization_name),
&url,
&params::ProjectCreate {
identity: IdentityMetadataCreateParams {
name: project_name.parse().unwrap(),
description: "a pier".to_string(),
},
},
)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.expect("failed to make request")
.parsed_body()
.unwrap()
}

pub async fn create_disk(
client: &ClientTestContext,
organization_name: &str,
project_name: &str,
disk_name: &str,
) -> Disk {
let url = format!(
"/organizations/{}/projects/{}/disks",
organization_name, project_name
);
object_create(
client,
&url,
&params::DiskCreate {
identity: IdentityMetadataCreateParams {
name: disk_name.parse().unwrap(),
description: String::from("sells rainsticks"),
},
snapshot_id: None,
size: ByteCount::from_gibibytes_u32(1),
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is definitely something some tests would like to customize, but I suppose it doesn't matter in the general case.

We don't need to change this now, but I wonder if a builder pattern might accommodate both use-cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. It occurred to me while adding this that the analogs for Organization and Project don't take create-time arguments, but that's because there basically aren't any that are useful for testing. If (or when) we find it's useful for Disks it should be easy to add.

},
)
.await
}

pub async fn create_vpc(
Expand Down
10 changes: 5 additions & 5 deletions nexus/tests/integration_tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ async fn test_projects_list(cptestctx: &ControlPlaneTestContext) {
&client,
projects_url,
"",
projects_subset,
Some(projects_subset),
)
.await
.expect("failed to list projects")
Expand All @@ -572,7 +572,7 @@ async fn test_projects_list(cptestctx: &ControlPlaneTestContext) {
&client,
projects_url,
"sort_by=name-ascending",
projects_subset,
Some(projects_subset),
)
.await
.expect("failed to list projects")
Expand All @@ -595,7 +595,7 @@ async fn test_projects_list(cptestctx: &ControlPlaneTestContext) {
&client,
projects_url,
"sort_by=name-descending",
projects_subset,
Some(projects_subset),
)
.await
.expect("failed to list projects")
Expand All @@ -617,7 +617,7 @@ async fn test_projects_list(cptestctx: &ControlPlaneTestContext) {
&client,
projects_url,
"sort_by=id-ascending",
projects_subset,
Some(projects_subset),
)
.await
.expect("failed to list projects")
Expand Down Expand Up @@ -671,7 +671,7 @@ async fn projects_list(
client: &ClientTestContext,
projects_url: &str,
) -> Vec<Project> {
NexusRequest::iter_collection_authn(client, projects_url, "", 10)
NexusRequest::iter_collection_authn(client, projects_url, "", None)
.await
.expect("failed to list projects")
.all_items
Expand Down
Loading