Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions nexus/src/app/silo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -806,4 +806,12 @@ impl super::Nexus {
.await?;
Ok(saml_identity_provider)
}

pub fn silo_group_lookup<'a>(
&'a self,
opctx: &'a OpContext,
group_id: &'a Uuid,
) -> db::lookup::SiloGroup<'a> {
LookupPath::new(opctx, &self.db_datastore).silo_group_id(*group_id)
}
}
23 changes: 23 additions & 0 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ pub fn external_api() -> NexusApiDescription {
api.register(silo_user_view)?;
api.register(group_list)?;
api.register(group_list_v1)?;
api.register(group_view)?;

// Console API operations
api.register(console_api::login_begin)?;
Expand Down Expand Up @@ -7562,6 +7563,28 @@ async fn group_list_v1(
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

/// Fetch group
#[endpoint {
method = GET,
path = "/v1/groups/{group}",
tags = ["silos"],
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder about the tag here. Isn't anything to be changed in this PR, but should silo scoped resources be listed in the silos tag?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree that it's weird. Perhaps even weirder: it seems to only be silo-scoped resources that get this tag. All the /system/silos/* endpoints for working with silos themselves are tagged system. We could probably stand to rework all the tags.

API operations found with tag "silos"
OPERATION ID                             URL PATH
group_list                               /groups
group_list_v1                            /v1/groups
group_view                               /v1/groups/{group}
policy_update                            /policy
policy_update_v1                         /v1/policy
policy_view                              /policy
policy_view_v1                           /v1/policy
user_list                                /users
user_list_v1                             /v1/user

}]
async fn group_view(
rqctx: RequestContext<Arc<ServerContext>>,
path_params: Path<params::GroupPath>,
) -> Result<HttpResponseOk<Group>, HttpError> {
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.nexus;
let path = path_params.into_inner();
let opctx = OpContext::for_external_api(&rqctx).await?;
let (.., group) =
nexus.silo_group_lookup(&opctx, &path.group).fetch().await?;
Ok(HttpResponseOk(group.into()))
};
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

// Built-in (system) users

/// List built-in users
Expand Down
10 changes: 10 additions & 0 deletions nexus/tests/integration_tests/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,16 @@ lazy_static! {
],
},

VerifyEndpoint {
// non-existent UUID that will 404
url: "/v1/groups/8d90b9a5-1cea-4a2b-9af4-71467dd33a04",
visibility: Visibility::Public,
unprivileged_access: UnprivilegedAccess::ReadOnly,
allowed_methods: vec![
AllowedMethod::GetNonexistent,
],
},

VerifyEndpoint {
url: &DEMO_SILO_USERS_LIST_URL,
visibility: Visibility::Public,
Expand Down
46 changes: 33 additions & 13 deletions nexus/tests/integration_tests/silo_users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,31 @@ async fn test_silo_group_users(cptestctx: &ControlPlaneTestContext) {
authz::Silo::new(authz::FLEET, *SILO_ID, LookupType::ById(*SILO_ID));

// create a group
let group = nexus
.silo_group_lookup_or_create_by_name(
&opctx,
&authz_silo,
&"group1".to_string(),
)
let group_name = "group1".to_string();
nexus
.silo_group_lookup_or_create_by_name(&opctx, &authz_silo, &group_name)
.await
.unwrap();
.expect("Group created");

// now we have a group
let groups =
objects_list_page_authz::<views::User>(client, &"/v1/groups").await;
let group_names: Vec<&str> =
groups.items.iter().map(|g| g.display_name.as_str()).collect();
assert_same_items(group_names, vec!["group1"]);
assert_eq!(groups.items.len(), 1);

let group_users_url = format!("/v1/users?group={}", group.id());
let group = groups.items.get(0).unwrap();
assert_eq!(group.display_name, group_name);

// we can now fetch the group by ID and get an empty list of users
// we can fetch that group by ID
let group_url = format!("/v1/groups/{}", group.id);
let group = NexusRequest::object_get(&client, &group_url)
.authn_as(AuthnMode::PrivilegedUser)
.execute_and_parse_unwrap::<views::Group>()
.await;
assert_eq!(group.display_name, group_name);

let group_users_url = format!("/v1/users?group={}", group.id);

// we can now fetch the group's user list and get an empty list of users
let group_users =
objects_list_page_authz::<views::User>(client, &group_users_url).await;

Expand All @@ -84,7 +90,7 @@ async fn test_silo_group_users(cptestctx: &ControlPlaneTestContext) {
.silo_group_membership_replace_for_user(
&opctx,
&authz_silo_user,
vec![group.id()],
vec![group.id],
)
.await
.expect("Failed to set user group memberships");
Expand Down Expand Up @@ -114,6 +120,20 @@ async fn test_silo_group_users_bad_group_id(
expect_failure(&client, &"/v1/users?group=", StatusCode::BAD_REQUEST).await;
}

#[nexus_test]
async fn test_silo_group_detail_bad_group_id(
cptestctx: &ControlPlaneTestContext,
) {
let client = &cptestctx.external_client;

// 404 on UUID that doesn't exist
let nonexistent_group = format!("/v1/groups/{}", Uuid::new_v4());
expect_failure(&client, &nonexistent_group, StatusCode::NOT_FOUND).await;

// 400 on non-UUID identifier
expect_failure(&client, &"/v1/groups/abc", StatusCode::BAD_REQUEST).await;
}

async fn expect_failure(
client: &ClientTestContext,
url: &str,
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/output/nexus_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ API operations found with tag "silos"
OPERATION ID URL PATH
group_list /groups
group_list_v1 /v1/groups
group_view /v1/groups/{group}
policy_update /policy
policy_update_v1 /v1/policy
policy_view /policy
Expand Down
9 changes: 9 additions & 0 deletions nexus/types/src/external_api/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ pub struct SnapshotPath {
pub snapshot: NameOrId,
}

// Only by ID because groups have an `external_id` instead of a name and
// therefore don't implement `ObjectIdentity`, which makes lookup by name
// inconvenient. We should figure this out more generally, as there are several
// resources like this.
#[derive(Deserialize, JsonSchema)]
pub struct GroupPath {
pub group: Uuid,
}

#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct OrganizationSelector {
pub organization: NameOrId,
Expand Down
38 changes: 38 additions & 0 deletions openapi/nexus.json
Original file line number Diff line number Diff line change
Expand Up @@ -8066,6 +8066,44 @@
"x-dropshot-pagination": true
}
},
"/v1/groups/{group}": {
"get": {
"tags": [
"silos"
],
"summary": "Fetch group",
"operationId": "group_view",
"parameters": [
{
"in": "path",
"name": "group",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Group"
}
}
}
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
}
}
},
"/v1/instances": {
"get": {
"tags": [
Expand Down