Skip to content

Commit 5bbfa20

Browse files
committed
user token list endpoint
1 parent a148374 commit 5bbfa20

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
@@ -440,21 +440,27 @@ resource ConsoleSessionList {
440440
has_relation(fleet: Fleet, "parent_fleet", collection: ConsoleSessionList)
441441
if collection.fleet = fleet;
442442

443-
# Allow silo admins to delete user sessions
443+
# Allow silo admins to delete user sessions and list user tokens
444444
resource SiloUserAuthnList {
445-
permissions = [ "modify" ];
445+
permissions = [ "modify", "list_children" ];
446446
relations = { parent_silo: Silo };
447447

448448
# A silo admin can modify (e.g., delete) a user's sessions.
449449
"modify" if "admin" on "parent_silo";
450+
451+
# A silo admin can list a user's tokens and sessions.
452+
"list_children" if "admin" on "parent_silo";
450453
}
451454
has_relation(silo: Silo, "parent_silo", sessions: SiloUserAuthnList)
452455
if sessions.silo_user.silo = silo;
453456

454-
# also give users 'modify' on their own sessions
457+
# also give users 'modify' and 'list_children' on their own sessions
455458
has_permission(actor: AuthenticatedActor, "modify", sessions: SiloUserAuthnList)
456459
if actor.equals_silo_user(sessions.silo_user);
457460

461+
has_permission(actor: AuthenticatedActor, "list_children", sessions: SiloUserAuthnList)
462+
if actor.equals_silo_user(sessions.silo_user);
463+
458464
# Describes the policy for creating and managing device authorization requests.
459465
resource DeviceAuthRequestList {
460466
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
@@ -159,6 +159,7 @@ policy_update PUT /v1/policy
159159
policy_view GET /v1/policy
160160
user_list GET /v1/users
161161
user_logout POST /v1/users/{user_id}/logout
162+
user_token_list GET /v1/users/{user_id}/access-tokens
162163
user_view GET /v1/users/{user_id}
163164
utilization_view GET /v1/utilization
164165

nexus/external-api/src/lib.rs

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

3022+
/// List user's device access tokens
3023+
#[endpoint {
3024+
method = GET,
3025+
path = "/v1/users/{user_id}/access-tokens",
3026+
tags = ["silos"],
3027+
}]
3028+
async fn user_token_list(
3029+
rqctx: RequestContext<Self::Context>,
3030+
path_params: Path<params::UserPath>,
3031+
query_params: Query<PaginatedById>,
3032+
) -> Result<HttpResponseOk<ResultsPage<views::DeviceAccessToken>>, HttpError>;
3033+
30223034
/// Expire all of user's tokens and sessions
30233035
#[endpoint {
30243036
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
@@ -6727,6 +6727,38 @@ impl NexusExternalApi for NexusExternalApiImpl {
67276727
.await
67286728
}
67296729

6730+
async fn user_token_list(
6731+
rqctx: RequestContext<Self::Context>,
6732+
path_params: Path<params::UserPath>,
6733+
query_params: Query<PaginatedById>,
6734+
) -> Result<HttpResponseOk<ResultsPage<views::DeviceAccessToken>>, HttpError> {
6735+
let apictx = rqctx.context();
6736+
let handler = async {
6737+
let nexus = &apictx.context.nexus;
6738+
let path = path_params.into_inner();
6739+
let query = query_params.into_inner();
6740+
let pag_params = data_page_params_for(&rqctx, &query)?;
6741+
let opctx =
6742+
crate::context::op_context_for_external_api(&rqctx).await?;
6743+
let tokens = nexus
6744+
.silo_user_token_list(&opctx, path.user_id, &pag_params)
6745+
.await?
6746+
.into_iter()
6747+
.map(views::DeviceAccessToken::from)
6748+
.collect();
6749+
Ok(HttpResponseOk(ScanById::results_page(
6750+
&query,
6751+
tokens,
6752+
&marker_for_id,
6753+
)?))
6754+
};
6755+
apictx
6756+
.context
6757+
.external_latencies
6758+
.instrument_dropshot_handler(&rqctx, handler)
6759+
.await
6760+
}
6761+
67306762
async fn user_logout(
67316763
rqctx: RequestContext<Self::Context>,
67326764
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
@@ -140,6 +140,8 @@ pub static DEMO_SILO_USER_ID_GET_URL: LazyLock<String> = LazyLock::new(|| {
140140
});
141141
pub static DEMO_SILO_USER_ID_IN_SILO_URL: LazyLock<String> =
142142
LazyLock::new(|| "/v1/users/{id}".to_string());
143+
pub static DEMO_SILO_USER_TOKEN_LIST_URL: LazyLock<String> =
144+
LazyLock::new(|| "/v1/users/{id}/access-tokens".to_string());
143145
pub static DEMO_SILO_USER_LOGOUT_URL: LazyLock<String> =
144146
LazyLock::new(|| "/v1/users/{id}/logout".to_string());
145147

@@ -1667,6 +1669,12 @@ pub static VERIFY_ENDPOINTS: LazyLock<Vec<VerifyEndpoint>> =
16671669
unprivileged_access: UnprivilegedAccess::ReadOnly,
16681670
allowed_methods: vec![AllowedMethod::Get],
16691671
},
1672+
VerifyEndpoint {
1673+
url: &DEMO_SILO_USER_TOKEN_LIST_URL,
1674+
visibility: Visibility::Public,
1675+
unprivileged_access: UnprivilegedAccess::None,
1676+
allowed_methods: vec![AllowedMethod::Get],
1677+
},
16701678
VerifyEndpoint {
16711679
url: &DEMO_SILO_USER_LOGOUT_URL,
16721680
visibility: Visibility::Public,

nexus/tests/integration_tests/unauthorized.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ static SETUP_REQUESTS: LazyLock<Vec<SetupReq>> = LazyLock::new(|| {
225225
&*DEMO_SILO_USER_ID_DELETE_URL,
226226
&*DEMO_SILO_USER_ID_SET_PASSWORD_URL,
227227
&*DEMO_SILO_USER_ID_IN_SILO_URL,
228+
&*DEMO_SILO_USER_TOKEN_LIST_URL,
228229
&*DEMO_SILO_USER_LOGOUT_URL,
229230
],
230231
},

openapi/nexus.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11488,6 +11488,75 @@
1148811488
}
1148911489
}
1149011490
},
11491+
"/v1/users/{user_id}/access-tokens": {
11492+
"get": {
11493+
"tags": [
11494+
"silos"
11495+
],
11496+
"summary": "List user's device access tokens",
11497+
"operationId": "user_token_list",
11498+
"parameters": [
11499+
{
11500+
"in": "path",
11501+
"name": "user_id",
11502+
"description": "ID of the user",
11503+
"required": true,
11504+
"schema": {
11505+
"type": "string",
11506+
"format": "uuid"
11507+
}
11508+
},
11509+
{
11510+
"in": "query",
11511+
"name": "limit",
11512+
"description": "Maximum number of items returned by a single call",
11513+
"schema": {
11514+
"nullable": true,
11515+
"type": "integer",
11516+
"format": "uint32",
11517+
"minimum": 1
11518+
}
11519+
},
11520+
{
11521+
"in": "query",
11522+
"name": "page_token",
11523+
"description": "Token returned by previous call to retrieve the subsequent page",
11524+
"schema": {
11525+
"nullable": true,
11526+
"type": "string"
11527+
}
11528+
},
11529+
{
11530+
"in": "query",
11531+
"name": "sort_by",
11532+
"schema": {
11533+
"$ref": "#/components/schemas/IdSortMode"
11534+
}
11535+
}
11536+
],
11537+
"responses": {
11538+
"200": {
11539+
"description": "successful operation",
11540+
"content": {
11541+
"application/json": {
11542+
"schema": {
11543+
"$ref": "#/components/schemas/DeviceAccessTokenResultsPage"
11544+
}
11545+
}
11546+
}
11547+
},
11548+
"4XX": {
11549+
"$ref": "#/components/responses/Error"
11550+
},
11551+
"5XX": {
11552+
"$ref": "#/components/responses/Error"
11553+
}
11554+
},
11555+
"x-dropshot-pagination": {
11556+
"required": []
11557+
}
11558+
}
11559+
},
1149111560
"/v1/users/{user_id}/logout": {
1149211561
"post": {
1149311562
"tags": [

0 commit comments

Comments
 (0)