Skip to content

Commit e603db7

Browse files
authored
authz: check project list endpoints (#617)
1 parent 21c0e87 commit e603db7

File tree

6 files changed

+136
-67
lines changed

6 files changed

+136
-67
lines changed

nexus/src/db/datastore.rs

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -607,45 +607,40 @@ impl DataStore {
607607

608608
pub async fn projects_list_by_id(
609609
&self,
610-
organization_id: &Uuid,
610+
opctx: &OpContext,
611+
authz_org: &authz::Organization,
611612
pagparams: &DataPageParams<'_, Uuid>,
612613
) -> ListResultVec<Project> {
613614
use db::schema::project::dsl;
615+
616+
opctx.authorize(authz::Action::ListChildren, authz_org).await?;
617+
614618
paginated(dsl::project, dsl::id, pagparams)
615-
.filter(dsl::organization_id.eq(*organization_id))
619+
.filter(dsl::organization_id.eq(authz_org.id()))
616620
.filter(dsl::time_deleted.is_null())
617621
.select(Project::as_select())
618-
.load_async(self.pool())
622+
.load_async(self.pool_authorized(opctx).await?)
619623
.await
620-
.map_err(|e| {
621-
public_error_from_diesel_pool(
622-
e,
623-
ResourceType::Project,
624-
LookupType::Other("Listing All".to_string()),
625-
)
626-
})
624+
.map_err(public_error_from_diesel_pool_shouldnt_fail)
627625
}
628626

629627
pub async fn projects_list_by_name(
630628
&self,
631-
organization_id: &Uuid,
629+
opctx: &OpContext,
630+
authz_org: &authz::Organization,
632631
pagparams: &DataPageParams<'_, Name>,
633632
) -> ListResultVec<Project> {
634633
use db::schema::project::dsl;
635634

635+
opctx.authorize(authz::Action::ListChildren, authz_org).await?;
636+
636637
paginated(dsl::project, dsl::name, &pagparams)
637-
.filter(dsl::organization_id.eq(*organization_id))
638+
.filter(dsl::organization_id.eq(authz_org.id()))
638639
.filter(dsl::time_deleted.is_null())
639640
.select(Project::as_select())
640-
.load_async(self.pool())
641+
.load_async(self.pool_authorized(opctx).await?)
641642
.await
642-
.map_err(|e| {
643-
public_error_from_diesel_pool(
644-
e,
645-
ResourceType::Project,
646-
LookupType::Other("Listing All".to_string()),
647-
)
648-
})
643+
.map_err(public_error_from_diesel_pool_shouldnt_fail)
649644
}
650645

651646
/// Updates a project by name (clobbering update -- no etag)

nexus/src/external_api/http_entrypoints.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,13 +381,18 @@ async fn organization_projects_get(
381381
let organization_name = &path.organization_name;
382382

383383
let handler = async {
384+
let opctx = OpContext::for_external_api(&rqctx).await?;
384385
let params = ScanByNameOrId::from_query(&query)?;
385386
let field = pagination_field_for_scan_params(params);
386387
let projects = match field {
387388
PagField::Id => {
388389
let page_selector = data_page_params_nameid_id(&rqctx, &query)?;
389390
nexus
390-
.projects_list_by_id(&organization_name, &page_selector)
391+
.projects_list_by_id(
392+
&opctx,
393+
&organization_name,
394+
&page_selector,
395+
)
391396
.await?
392397
}
393398

@@ -396,7 +401,11 @@ async fn organization_projects_get(
396401
data_page_params_nameid_name(&rqctx, &query)?
397402
.map_name(|n| Name::ref_cast(n));
398403
nexus
399-
.projects_list_by_name(&organization_name, &page_selector)
404+
.projects_list_by_name(
405+
&opctx,
406+
&organization_name,
407+
&page_selector,
408+
)
400409
.await?
401410
}
402411
}

nexus/src/nexus.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -590,30 +590,28 @@ impl Nexus {
590590

591591
pub async fn projects_list_by_name(
592592
&self,
593+
opctx: &OpContext,
593594
organization_name: &Name,
594595
pagparams: &DataPageParams<'_, Name>,
595596
) -> ListResultVec<db::model::Project> {
596-
let organization_id = self
597-
.db_datastore
598-
.organization_lookup_id(organization_name)
599-
.await?
600-
.id();
597+
let authz_org =
598+
self.db_datastore.organization_lookup_id(organization_name).await?;
601599
self.db_datastore
602-
.projects_list_by_name(&organization_id, pagparams)
600+
.projects_list_by_name(opctx, &authz_org, pagparams)
603601
.await
604602
}
605603

606604
pub async fn projects_list_by_id(
607605
&self,
606+
opctx: &OpContext,
608607
organization_name: &Name,
609608
pagparams: &DataPageParams<'_, Uuid>,
610609
) -> ListResultVec<db::model::Project> {
611-
let organization_id = self
612-
.db_datastore
613-
.organization_lookup_id(organization_name)
614-
.await?
615-
.id();
616-
self.db_datastore.projects_list_by_id(&organization_id, pagparams).await
610+
let authz_org =
611+
self.db_datastore.organization_lookup_id(organization_name).await?;
612+
self.db_datastore
613+
.projects_list_by_id(opctx, &authz_org, pagparams)
614+
.await
617615
}
618616

619617
pub async fn project_delete(

nexus/tests/integration_tests/basic.rs

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
* TODO-coverage add test for racks, sleds
1010
*/
1111

12-
use dropshot::test_util::iter_collection;
1312
use dropshot::test_util::object_get;
1413
use dropshot::test_util::objects_list_page;
1514
use dropshot::test_util::read_json;
@@ -177,11 +176,32 @@ async fn test_projects_basic(cptestctx: &ControlPlaneTestContext) {
177176

178177
let org_name = "test-org";
179178
create_organization(&client, &org_name).await;
179+
let projects_url = "/organizations/test-org/projects";
180+
181+
/* Unauthenticated and unauthorized users cannot list projects. */
182+
NexusRequest::expect_failure(
183+
client,
184+
http::StatusCode::NOT_FOUND,
185+
http::Method::GET,
186+
projects_url,
187+
)
188+
.execute()
189+
.await
190+
.expect("failed to make request");
191+
NexusRequest::expect_failure(
192+
client,
193+
http::StatusCode::NOT_FOUND,
194+
http::Method::GET,
195+
projects_url,
196+
)
197+
.authn_as(AuthnMode::UnprivilegedUser)
198+
.execute()
199+
.await
200+
.expect("failed to make request");
180201

181202
/*
182203
* Verify that there are no projects to begin with.
183204
*/
184-
let projects_url = "/organizations/test-org/projects";
185205
let projects = projects_list(&client, &projects_url).await;
186206
assert_eq!(0, projects.len());
187207

@@ -564,9 +584,15 @@ async fn test_projects_list(cptestctx: &ControlPlaneTestContext) {
564584
* increasing order of name.
565585
*/
566586
let found_projects_by_name =
567-
iter_collection::<Project>(&client, projects_url, "", projects_subset)
568-
.await
569-
.0;
587+
NexusRequest::iter_collection_authn::<Project>(
588+
&client,
589+
projects_url,
590+
"",
591+
projects_subset,
592+
)
593+
.await
594+
.expect("failed to list projects")
595+
.all_items;
570596
assert_eq!(found_projects_by_name.len(), project_names_by_name.len());
571597
assert_eq!(
572598
project_names_by_name,
@@ -580,14 +606,16 @@ async fn test_projects_list(cptestctx: &ControlPlaneTestContext) {
580606
* Page through all the projects in ascending order by name, which should be
581607
* the same as above.
582608
*/
583-
let found_projects_by_name = iter_collection::<Project>(
584-
&client,
585-
projects_url,
586-
"sort_by=name-ascending",
587-
projects_subset,
588-
)
589-
.await
590-
.0;
609+
let found_projects_by_name =
610+
NexusRequest::iter_collection_authn::<Project>(
611+
&client,
612+
projects_url,
613+
"sort_by=name-ascending",
614+
projects_subset,
615+
)
616+
.await
617+
.expect("failed to list projects")
618+
.all_items;
591619
assert_eq!(found_projects_by_name.len(), project_names_by_name.len());
592620
assert_eq!(
593621
project_names_by_name,
@@ -601,14 +629,16 @@ async fn test_projects_list(cptestctx: &ControlPlaneTestContext) {
601629
* Page through all the projects in descending order by name, which should be
602630
* the reverse of the above.
603631
*/
604-
let mut found_projects_by_name = iter_collection::<Project>(
605-
&client,
606-
projects_url,
607-
"sort_by=name-descending",
608-
projects_subset,
609-
)
610-
.await
611-
.0;
632+
let mut found_projects_by_name =
633+
NexusRequest::iter_collection_authn::<Project>(
634+
&client,
635+
projects_url,
636+
"sort_by=name-descending",
637+
projects_subset,
638+
)
639+
.await
640+
.expect("failed to list projects")
641+
.all_items;
612642
assert_eq!(found_projects_by_name.len(), project_names_by_name.len());
613643
found_projects_by_name.reverse();
614644
assert_eq!(
@@ -622,14 +652,15 @@ async fn test_projects_list(cptestctx: &ControlPlaneTestContext) {
622652
/*
623653
* Page through the projects in ascending order by id.
624654
*/
625-
let found_projects_by_id = iter_collection::<Project>(
655+
let found_projects_by_id = NexusRequest::iter_collection_authn::<Project>(
626656
&client,
627657
projects_url,
628658
"sort_by=id-ascending",
629659
projects_subset,
630660
)
631661
.await
632-
.0;
662+
.expect("failed to list projects")
663+
.all_items;
633664
assert_eq!(found_projects_by_id.len(), project_names_by_id.len());
634665
assert_eq!(
635666
project_names_by_id,
@@ -679,7 +710,10 @@ async fn projects_list(
679710
client: &ClientTestContext,
680711
projects_url: &str,
681712
) -> Vec<Project> {
682-
objects_list_page::<Project>(client, projects_url).await.items
713+
NexusRequest::iter_collection_authn(client, projects_url, "", 10)
714+
.await
715+
.expect("failed to list projects")
716+
.all_items
683717
}
684718

685719
async fn project_get(client: &ClientTestContext, project_url: &str) -> Project {

nexus/tests/integration_tests/projects.rs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5+
use nexus_test_utils::http_testing::AuthnMode;
6+
use nexus_test_utils::http_testing::NexusRequest;
57
use omicron_nexus::external_api::views::Project;
68

79
use dropshot::test_util::object_get;
8-
use dropshot::test_util::objects_list_page;
910

1011
use nexus_test_utils::resource_helpers::{create_organization, create_project};
1112
use nexus_test_utils::ControlPlaneTestContext;
@@ -34,12 +35,41 @@ async fn test_projects(cptestctx: &ControlPlaneTestContext) {
3435
let project: Project = object_get(&client, &p2_url).await;
3536
assert_eq!(project.identity.name, p2_name);
3637

37-
let projects = objects_list_page::<Project>(
38-
client,
39-
&format!("/organizations/{}/projects", org_name),
38+
/*
39+
* Unauthenticated and unauthorized users should not be able to list
40+
* Projects.
41+
*/
42+
let projects_url = format!("/organizations/{}/projects", org_name);
43+
NexusRequest::expect_failure(
44+
&client,
45+
http::StatusCode::NOT_FOUND,
46+
http::Method::GET,
47+
&projects_url,
4048
)
49+
.execute()
4150
.await
42-
.items;
51+
.expect("failed to make request");
52+
NexusRequest::expect_failure(
53+
&client,
54+
http::StatusCode::NOT_FOUND,
55+
http::Method::GET,
56+
&projects_url,
57+
)
58+
.authn_as(AuthnMode::UnprivilegedUser)
59+
.execute()
60+
.await
61+
.expect("failed to make request");
62+
63+
/* Verify the list of Projects. */
64+
let projects = NexusRequest::iter_collection_authn::<Project>(
65+
&client,
66+
&projects_url,
67+
"",
68+
10,
69+
)
70+
.await
71+
.expect("failed to list projects")
72+
.all_items;
4373
assert_eq!(projects.len(), 2);
4474
// alphabetical order for now
4575
assert_eq!(projects[0].identity.name, p2_name);
@@ -54,12 +84,15 @@ async fn test_projects(cptestctx: &ControlPlaneTestContext) {
5484
assert_ne!(org_p1_id, org2_p1_id);
5585

5686
// Make sure the list projects results for the new org make sense
57-
let projects = objects_list_page::<Project>(
58-
client,
87+
let projects = NexusRequest::iter_collection_authn::<Project>(
88+
&client,
5989
&format!("/organizations/{}/projects", org2_name),
90+
"",
91+
10,
6092
)
6193
.await
62-
.items;
94+
.expect("failed to list projects")
95+
.all_items;
6396
assert_eq!(projects.len(), 1);
6497
assert_eq!(projects[0].identity.name, p1_name);
6598
}

tools/oxapi_demo

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ function cmd_organization_get
217217
function cmd_projects_list
218218
{
219219
[[ $# != 1 ]] && usage "expected ORGANIZATION_NAME"
220-
do_curl "/organizations/$1/projects"
220+
do_curl_authn "/organizations/$1/projects"
221221
}
222222

223223
function cmd_project_create_demo

0 commit comments

Comments
 (0)