Skip to content

Commit 653bd29

Browse files
committed
user token list endpoint
1 parent d80a4d6 commit 653bd29

File tree

9 files changed

+179
-3
lines changed

9 files changed

+179
-3
lines changed

nexus/auth/src/authz/omicron.polar

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,21 +450,27 @@ resource ConsoleSessionList {
450450
has_relation(fleet: Fleet, "parent_fleet", collection: ConsoleSessionList)
451451
if collection.fleet = fleet;
452452

453-
# Allow silo admins to delete user sessions
453+
# Allow silo admins to delete user sessions and list user tokens
454454
resource SiloUserAuthnList {
455-
permissions = [ "modify" ];
455+
permissions = [ "modify", "list_children" ];
456456
relations = { parent_silo: Silo };
457457

458458
# A silo admin can modify (e.g., delete) a user's sessions.
459459
"modify" if "admin" on "parent_silo";
460+
461+
# A silo admin can list a user's tokens and sessions.
462+
"list_children" if "admin" on "parent_silo";
460463
}
461464
has_relation(silo: Silo, "parent_silo", sessions: SiloUserAuthnList)
462465
if sessions.silo_user.silo = silo;
463466

464-
# also give users 'modify' on their own sessions
467+
# also give users 'modify' and 'list_children' on their own sessions
465468
has_permission(actor: AuthenticatedActor, "modify", sessions: SiloUserAuthnList)
466469
if actor.equals_silo_user(sessions.silo_user);
467470

471+
has_permission(actor: AuthenticatedActor, "list_children", sessions: SiloUserAuthnList)
472+
if actor.equals_silo_user(sessions.silo_user);
473+
468474
# Describes the policy for creating and managing device authorization requests.
469475
resource DeviceAuthRequestList {
470476
permissions = [ "create_child" ];

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,33 @@ impl DataStore {
214214
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
215215
}
216216

217+
/// List device access tokens for a specific user
218+
pub async fn silo_user_token_list(
219+
&self,
220+
opctx: &OpContext,
221+
user_authn_list: authz::SiloUserAuthnList,
222+
pagparams: &DataPageParams<'_, Uuid>,
223+
) -> ListResultVec<DeviceAccessToken> {
224+
opctx.authorize(authz::Action::ListChildren, &user_authn_list).await?;
225+
226+
let silo_user_id = user_authn_list.silo_user().id();
227+
228+
use nexus_db_schema::schema::device_access_token::dsl;
229+
paginated(dsl::device_access_token, dsl::id, &pagparams)
230+
.filter(dsl::silo_user_id.eq(silo_user_id))
231+
// we don't have time_deleted on tokens. unfortunately this is not
232+
// indexed well. maybe it can be!
233+
.filter(
234+
dsl::time_expires
235+
.is_null()
236+
.or(dsl::time_expires.gt(Utc::now())),
237+
)
238+
.select(DeviceAccessToken::as_select())
239+
.load_async(&*self.pool_connection_authorized(opctx).await?)
240+
.await
241+
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
242+
}
243+
217244
pub async fn current_user_token_delete(
218245
&self,
219246
opctx: &OpContext,

nexus/external-api/output/nexus_tags.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ policy_update PUT /v1/policy
158158
policy_view GET /v1/policy
159159
user_list GET /v1/users
160160
user_logout POST /v1/users/{user_id}/logout
161+
user_token_list GET /v1/users/{user_id}/access-tokens
161162
user_view GET /v1/users/{user_id}
162163
utilization_view GET /v1/utilization
163164

nexus/external-api/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3087,6 +3087,18 @@ pub trait NexusExternalApi {
30873087
path_params: Path<params::UserPath>,
30883088
) -> Result<HttpResponseOk<views::User>, HttpError>;
30893089

3090+
/// List user's device access tokens
3091+
#[endpoint {
3092+
method = GET,
3093+
path = "/v1/users/{user_id}/access-tokens",
3094+
tags = ["silos"],
3095+
}]
3096+
async fn user_token_list(
3097+
rqctx: RequestContext<Self::Context>,
3098+
path_params: Path<params::UserPath>,
3099+
query_params: Query<PaginatedById>,
3100+
) -> Result<HttpResponseOk<ResultsPage<views::DeviceAccessToken>>, HttpError>;
3101+
30903102
/// Expire all of user's tokens and sessions
30913103
#[endpoint {
30923104
method = POST,

nexus/src/app/silo.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,26 @@ impl super::Nexus {
353353
Ok((authz_silo_user, db_silo_user))
354354
}
355355

356+
/// List device access tokens for a user in a Silo
357+
pub(crate) async fn silo_user_token_list(
358+
&self,
359+
opctx: &OpContext,
360+
silo_user_id: Uuid,
361+
pagparams: &DataPageParams<'_, Uuid>,
362+
) -> ListResultVec<db::model::DeviceAccessToken> {
363+
let (_, authz_silo_user, _db_silo_user) =
364+
LookupPath::new(opctx, self.datastore())
365+
.silo_user_id(silo_user_id)
366+
.fetch_for(authz::Action::Read)
367+
.await?;
368+
369+
let user_authn_list = authz::SiloUserAuthnList::new(authz_silo_user);
370+
371+
self.datastore()
372+
.silo_user_token_list(opctx, user_authn_list, pagparams)
373+
.await
374+
}
375+
356376
// The "local" identity provider (available only in `LocalOnly` Silos)
357377

358378
/// Helper function for looking up a LocalOnly Silo by name

nexus/src/external_api/http_entrypoints.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6882,6 +6882,38 @@ impl NexusExternalApi for NexusExternalApiImpl {
68826882
.await
68836883
}
68846884

6885+
async fn user_token_list(
6886+
rqctx: RequestContext<Self::Context>,
6887+
path_params: Path<params::UserPath>,
6888+
query_params: Query<PaginatedById>,
6889+
) -> Result<HttpResponseOk<ResultsPage<views::DeviceAccessToken>>, HttpError> {
6890+
let apictx = rqctx.context();
6891+
let handler = async {
6892+
let nexus = &apictx.context.nexus;
6893+
let path = path_params.into_inner();
6894+
let query = query_params.into_inner();
6895+
let pag_params = data_page_params_for(&rqctx, &query)?;
6896+
let opctx =
6897+
crate::context::op_context_for_external_api(&rqctx).await?;
6898+
let tokens = nexus
6899+
.silo_user_token_list(&opctx, path.user_id, &pag_params)
6900+
.await?
6901+
.into_iter()
6902+
.map(views::DeviceAccessToken::from)
6903+
.collect();
6904+
Ok(HttpResponseOk(ScanById::results_page(
6905+
&query,
6906+
tokens,
6907+
&marker_for_id,
6908+
)?))
6909+
};
6910+
apictx
6911+
.context
6912+
.external_latencies
6913+
.instrument_dropshot_handler(&rqctx, handler)
6914+
.await
6915+
}
6916+
68856917
async fn user_logout(
68866918
rqctx: RequestContext<Self::Context>,
68876919
path_params: Path<params::UserPath>,

nexus/tests/integration_tests/endpoints.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ pub static DEMO_SILO_USER_ID_GET_URL: LazyLock<String> = LazyLock::new(|| {
141141
});
142142
pub static DEMO_SILO_USER_ID_IN_SILO_URL: LazyLock<String> =
143143
LazyLock::new(|| "/v1/users/{id}".to_string());
144+
pub static DEMO_SILO_USER_TOKEN_LIST_URL: LazyLock<String> =
145+
LazyLock::new(|| "/v1/users/{id}/access-tokens".to_string());
144146
pub static DEMO_SILO_USER_LOGOUT_URL: LazyLock<String> =
145147
LazyLock::new(|| "/v1/users/{id}/logout".to_string());
146148

@@ -1686,6 +1688,12 @@ pub static VERIFY_ENDPOINTS: LazyLock<Vec<VerifyEndpoint>> =
16861688
unprivileged_access: UnprivilegedAccess::ReadOnly,
16871689
allowed_methods: vec![AllowedMethod::Get],
16881690
},
1691+
VerifyEndpoint {
1692+
url: &DEMO_SILO_USER_TOKEN_LIST_URL,
1693+
visibility: Visibility::Public,
1694+
unprivileged_access: UnprivilegedAccess::None,
1695+
allowed_methods: vec![AllowedMethod::Get],
1696+
},
16891697
VerifyEndpoint {
16901698
url: &DEMO_SILO_USER_LOGOUT_URL,
16911699
visibility: Visibility::Public,

nexus/tests/integration_tests/unauthorized.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ static SETUP_REQUESTS: LazyLock<Vec<SetupReq>> = LazyLock::new(|| {
251251
&*DEMO_SILO_USER_ID_DELETE_URL,
252252
&*DEMO_SILO_USER_ID_SET_PASSWORD_URL,
253253
&*DEMO_SILO_USER_ID_IN_SILO_URL,
254+
&*DEMO_SILO_USER_TOKEN_LIST_URL,
254255
&*DEMO_SILO_USER_LOGOUT_URL,
255256
],
256257
},

openapi/nexus.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11616,6 +11616,75 @@
1161611616
}
1161711617
}
1161811618
},
11619+
"/v1/users/{user_id}/access-tokens": {
11620+
"get": {
11621+
"tags": [
11622+
"silos"
11623+
],
11624+
"summary": "List user's device access tokens",
11625+
"operationId": "user_token_list",
11626+
"parameters": [
11627+
{
11628+
"in": "path",
11629+
"name": "user_id",
11630+
"description": "ID of the user",
11631+
"required": true,
11632+
"schema": {
11633+
"type": "string",
11634+
"format": "uuid"
11635+
}
11636+
},
11637+
{
11638+
"in": "query",
11639+
"name": "limit",
11640+
"description": "Maximum number of items returned by a single call",
11641+
"schema": {
11642+
"nullable": true,
11643+
"type": "integer",
11644+
"format": "uint32",
11645+
"minimum": 1
11646+
}
11647+
},
11648+
{
11649+
"in": "query",
11650+
"name": "page_token",
11651+
"description": "Token returned by previous call to retrieve the subsequent page",
11652+
"schema": {
11653+
"nullable": true,
11654+
"type": "string"
11655+
}
11656+
},
11657+
{
11658+
"in": "query",
11659+
"name": "sort_by",
11660+
"schema": {
11661+
"$ref": "#/components/schemas/IdSortMode"
11662+
}
11663+
}
11664+
],
11665+
"responses": {
11666+
"200": {
11667+
"description": "successful operation",
11668+
"content": {
11669+
"application/json": {
11670+
"schema": {
11671+
"$ref": "#/components/schemas/DeviceAccessTokenResultsPage"
11672+
}
11673+
}
11674+
}
11675+
},
11676+
"4XX": {
11677+
"$ref": "#/components/responses/Error"
11678+
},
11679+
"5XX": {
11680+
"$ref": "#/components/responses/Error"
11681+
}
11682+
},
11683+
"x-dropshot-pagination": {
11684+
"required": []
11685+
}
11686+
}
11687+
},
1161911688
"/v1/users/{user_id}/logout": {
1162011689
"post": {
1162111690
"tags": [

0 commit comments

Comments
 (0)