Skip to content

Commit 5e2b32d

Browse files
committed
DELETE /v1/me/tokens/{token_id}
1 parent 67ee8c9 commit 5e2b32d

File tree

8 files changed

+152
-5
lines changed

8 files changed

+152
-5
lines changed

nexus/db-queries/src/db/datastore/device_auth.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,31 @@ impl DataStore {
209209
.await
210210
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
211211
}
212+
213+
pub async fn device_access_token_delete(
214+
&self,
215+
opctx: &OpContext,
216+
authz_user: &authz::SiloUser,
217+
token_id: Uuid,
218+
) -> Result<(), Error> {
219+
// TODO: surely this is the wrong permission
220+
opctx.authorize(authz::Action::Modify, authz_user).await?;
221+
222+
use nexus_db_schema::schema::device_access_token::dsl;
223+
let num_deleted = diesel::delete(dsl::device_access_token)
224+
.filter(dsl::id.eq(token_id))
225+
.filter(dsl::silo_user_id.eq(authz_user.id()))
226+
.execute_async(&*self.pool_connection_authorized(opctx).await?)
227+
.await
228+
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;
229+
230+
if num_deleted == 0 {
231+
return Err(Error::not_found_by_id(
232+
ResourceType::DeviceAccessToken,
233+
&token_id,
234+
));
235+
}
236+
237+
Ok(())
238+
}
212239
}

nexus/external-api/output/nexus_tags.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ ping GET /v1/ping
289289

290290
API operations found with tag "tokens"
291291
OPERATION ID METHOD URL PATH
292+
current_user_token_delete DELETE /v1/me/tokens/{token_id}
292293
current_user_token_list GET /v1/me/tokens
293294

294295
API operations found with tag "vpcs"

nexus/external-api/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3165,6 +3165,19 @@ pub trait NexusExternalApi {
31653165
query_params: Query<PaginatedById>,
31663166
) -> Result<HttpResponseOk<ResultsPage<views::DeviceAccessToken>>, HttpError>;
31673167

3168+
/// Delete device access token
3169+
///
3170+
/// Delete a device access token for the currently authenticated user.
3171+
#[endpoint {
3172+
method = DELETE,
3173+
path = "/v1/me/tokens/{token_id}",
3174+
tags = ["tokens"],
3175+
}]
3176+
async fn current_user_token_delete(
3177+
rqctx: RequestContext<Self::Context>,
3178+
path_params: Path<params::TokenPath>,
3179+
) -> Result<HttpResponseDeleted, HttpError>;
3180+
31683181
// Support bundles (experimental)
31693182

31703183
/// List all support bundles

nexus/src/app/device_auth.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,4 +309,22 @@ impl super::Nexus {
309309
.device_access_tokens_list(opctx, &authz_user, pagparams)
310310
.await
311311
}
312+
313+
pub(crate) async fn current_user_token_delete(
314+
&self,
315+
opctx: &OpContext,
316+
token_id: Uuid,
317+
) -> Result<(), Error> {
318+
let &actor = opctx
319+
.authn
320+
.actor_required()
321+
.internal_context("loading current user to delete token")?;
322+
let (.., authz_user) = LookupPath::new(opctx, self.datastore())
323+
.silo_user_id(actor.actor_id())
324+
.lookup_for(authz::Action::Modify)
325+
.await?;
326+
self.db_datastore
327+
.device_access_token_delete(opctx, &authz_user, token_id)
328+
.await
329+
}
312330
}

nexus/src/external_api/http_entrypoints.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7098,6 +7098,26 @@ impl NexusExternalApi for NexusExternalApiImpl {
70987098
.await
70997099
}
71007100

7101+
async fn current_user_token_delete(
7102+
rqctx: RequestContext<Self::Context>,
7103+
path_params: Path<params::TokenPath>,
7104+
) -> Result<HttpResponseDeleted, HttpError> {
7105+
let apictx = rqctx.context();
7106+
let handler = async {
7107+
let opctx =
7108+
crate::context::op_context_for_external_api(&rqctx).await?;
7109+
let nexus = &apictx.context.nexus;
7110+
let path = path_params.into_inner();
7111+
nexus.current_user_token_delete(&opctx, path.token_id).await?;
7112+
Ok(HttpResponseDeleted())
7113+
};
7114+
apictx
7115+
.context
7116+
.external_latencies
7117+
.instrument_dropshot_handler(&rqctx, handler)
7118+
.await
7119+
}
7120+
71017121
async fn support_bundle_list(
71027122
rqctx: RequestContext<ApiContext>,
71037123
query_params: Query<PaginatedById>,

nexus/tests/integration_tests/device_auth.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,6 @@ async fn test_device_auth_flow(cptestctx: &ControlPlaneTestContext) {
179179
assert_eq!(tokens_unpriv_after.len(), 1);
180180
assert_eq!(tokens_unpriv_after[0].time_expires, None);
181181

182-
// now make a request with the token. it 403s because unpriv user has no roles
183-
project_list(&testctx, &token.access_token, StatusCode::FORBIDDEN)
184-
.await
185-
.expect("projects list should 403 with no roles");
186-
187182
// make sure it also fails with a nonsense token
188183
project_list(&testctx, "oxide-token-xyz", StatusCode::UNAUTHORIZED)
189184
.await
@@ -203,6 +198,45 @@ async fn test_device_auth_flow(cptestctx: &ControlPlaneTestContext) {
203198
project_list(&testctx, &token.access_token, StatusCode::OK)
204199
.await
205200
.expect("failed to get projects with token");
201+
202+
let token_id = tokens_unpriv_after[0].id;
203+
204+
// Test that privileged user cannot delete unpriv's token through this
205+
// endpoint, though it will probably be able to do it via a different one
206+
let token_url = format!("/v1/me/tokens/{}", token_id);
207+
NexusRequest::new(
208+
RequestBuilder::new(testctx, Method::DELETE, &token_url)
209+
.expect_status(Some(StatusCode::NOT_FOUND)),
210+
)
211+
.authn_as(AuthnMode::PrivilegedUser)
212+
.execute()
213+
.await
214+
.expect("privileged user should get a 404 when trying to delete another user's token");
215+
216+
// Test deleting the token as the owner
217+
NexusRequest::object_delete(testctx, &token_url)
218+
.authn_as(AuthnMode::UnprivilegedUser)
219+
.execute()
220+
.await
221+
.expect("failed to delete token");
222+
223+
// Verify token is gone from the list
224+
assert_eq!(get_tokens_unpriv(testctx).await.len(), 0);
225+
226+
// Token should no longer work for API calls
227+
project_list(&testctx, &token.access_token, StatusCode::UNAUTHORIZED)
228+
.await
229+
.expect("deleted token should be unauthorized");
230+
231+
// Trying to delete the same token again should 404
232+
NexusRequest::new(
233+
RequestBuilder::new(testctx, Method::DELETE, &token_url)
234+
.expect_status(Some(StatusCode::NOT_FOUND)),
235+
)
236+
.authn_as(AuthnMode::UnprivilegedUser)
237+
.execute()
238+
.await
239+
.expect("double delete should 404");
206240
}
207241

208242
/// Helper to make the test cute. Goes through the whole flow, returns the token

nexus/types/src/external_api/params.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ path_param!(CertificatePath, certificate, "certificate");
9797

9898
id_path_param!(SupportBundlePath, bundle_id, "support bundle");
9999
id_path_param!(GroupPath, group_id, "group");
100+
id_path_param!(TokenPath, token_id, "token");
100101

101102
// TODO: The hardware resources should be represented by its UUID or a hardware
102103
// ID that can be used to deterministically generate the UUID.

openapi/nexus.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5702,6 +5702,39 @@
57025702
}
57035703
}
57045704
},
5705+
"/v1/me/tokens/{token_id}": {
5706+
"delete": {
5707+
"tags": [
5708+
"tokens"
5709+
],
5710+
"summary": "Delete device access token",
5711+
"description": "Delete a device access token for the currently authenticated user.",
5712+
"operationId": "current_user_token_delete",
5713+
"parameters": [
5714+
{
5715+
"in": "path",
5716+
"name": "token_id",
5717+
"description": "ID of the token",
5718+
"required": true,
5719+
"schema": {
5720+
"type": "string",
5721+
"format": "uuid"
5722+
}
5723+
}
5724+
],
5725+
"responses": {
5726+
"204": {
5727+
"description": "successful deletion"
5728+
},
5729+
"4XX": {
5730+
"$ref": "#/components/responses/Error"
5731+
},
5732+
"5XX": {
5733+
"$ref": "#/components/responses/Error"
5734+
}
5735+
}
5736+
}
5737+
},
57055738
"/v1/metrics/{metric_name}": {
57065739
"get": {
57075740
"tags": [

0 commit comments

Comments
 (0)