Skip to content

Commit 2d0f763

Browse files
committed
session list endpoint
1 parent 438c9be commit 2d0f763

File tree

10 files changed

+239
-2
lines changed

10 files changed

+239
-2
lines changed

nexus/db-model/src/console_session.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
use chrono::{DateTime, Utc};
66
use nexus_db_schema::schema::console_session;
7-
use omicron_uuid_kinds::ConsoleSessionKind;
8-
use omicron_uuid_kinds::ConsoleSessionUuid;
7+
use nexus_types::external_api::views;
8+
use omicron_uuid_kinds::{ConsoleSessionKind, ConsoleSessionUuid, GenericUuid};
99
use uuid::Uuid;
1010

1111
use crate::typed_uuid::DbTypedUuid;
@@ -38,3 +38,13 @@ impl ConsoleSession {
3838
self.id.0
3939
}
4040
}
41+
42+
impl From<ConsoleSession> for views::ConsoleSession {
43+
fn from(session: ConsoleSession) -> Self {
44+
Self {
45+
id: session.id.into_untyped_uuid(),
46+
time_created: session.time_created,
47+
time_last_used: session.time_last_used,
48+
}
49+
}
50+
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::authn;
99
use crate::authz;
1010
use crate::context::OpContext;
1111
use crate::db::model::ConsoleSession;
12+
use crate::db::pagination::paginated;
1213
use async_bb8_diesel::AsyncRunQueryDsl;
1314
use chrono::Utc;
1415
use diesel::prelude::*;
@@ -17,13 +18,16 @@ use nexus_db_errors::public_error_from_diesel;
1718
use nexus_db_lookup::LookupPath;
1819
use nexus_db_schema::schema::console_session;
1920
use omicron_common::api::external::CreateResult;
21+
use omicron_common::api::external::DataPageParams;
2022
use omicron_common::api::external::DeleteResult;
2123
use omicron_common::api::external::Error;
24+
use omicron_common::api::external::ListResultVec;
2225
use omicron_common::api::external::LookupResult;
2326
use omicron_common::api::external::LookupType;
2427
use omicron_common::api::external::ResourceType;
2528
use omicron_common::api::external::UpdateResult;
2629
use omicron_uuid_kinds::GenericUuid;
30+
use uuid::Uuid;
2731

2832
impl DataStore {
2933
/// Look up session by token. The token is a kind of password, so simply
@@ -157,6 +161,26 @@ impl DataStore {
157161
})
158162
}
159163

164+
/// List console sessions for a specific user
165+
pub async fn silo_user_session_list(
166+
&self,
167+
opctx: &OpContext,
168+
user_authn_list: authz::SiloUserAuthnList,
169+
pagparams: &DataPageParams<'_, Uuid>,
170+
) -> ListResultVec<ConsoleSession> {
171+
opctx.authorize(authz::Action::ListChildren, &user_authn_list).await?;
172+
173+
let silo_user_id = user_authn_list.silo_user().id();
174+
175+
use nexus_db_schema::schema::console_session::dsl;
176+
paginated(dsl::console_session, dsl::id, &pagparams)
177+
.filter(dsl::silo_user_id.eq(silo_user_id))
178+
.select(ConsoleSession::as_select())
179+
.load_async(&*self.pool_connection_authorized(opctx).await?)
180+
.await
181+
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
182+
}
183+
160184
/// Delete all session for the user
161185
pub async fn silo_user_sessions_delete(
162186
&self,

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_session_list GET /v1/users/{user_id}/sessions
162163
user_token_list GET /v1/users/{user_id}/access-tokens
163164
user_view GET /v1/users/{user_id}
164165
utilization_view GET /v1/utilization

nexus/external-api/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,6 +3031,18 @@ pub trait NexusExternalApi {
30313031
query_params: Query<PaginatedById>,
30323032
) -> Result<HttpResponseOk<ResultsPage<views::DeviceAccessToken>>, HttpError>;
30333033

3034+
/// List user's console sessions
3035+
#[endpoint {
3036+
method = GET,
3037+
path = "/v1/users/{user_id}/sessions",
3038+
tags = ["silos"],
3039+
}]
3040+
async fn user_session_list(
3041+
rqctx: RequestContext<Self::Context>,
3042+
path_params: Path<params::UserPath>,
3043+
query_params: Query<PaginatedById>,
3044+
) -> Result<HttpResponseOk<ResultsPage<views::ConsoleSession>>, HttpError>;
3045+
30343046
/// Expire all of user's tokens and sessions
30353047
#[endpoint {
30363048
method = POST,

nexus/src/app/silo.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,26 @@ impl super::Nexus {
373373
.await
374374
}
375375

376+
/// List console sessions for a user in a Silo
377+
pub(crate) async fn silo_user_session_list(
378+
&self,
379+
opctx: &OpContext,
380+
silo_user_id: Uuid,
381+
pagparams: &DataPageParams<'_, Uuid>,
382+
) -> ListResultVec<db::model::ConsoleSession> {
383+
let (_, authz_silo_user, _db_silo_user) =
384+
LookupPath::new(opctx, self.datastore())
385+
.silo_user_id(silo_user_id)
386+
.fetch_for(authz::Action::Read)
387+
.await?;
388+
389+
let user_authn_list = authz::SiloUserAuthnList::new(authz_silo_user);
390+
391+
self.datastore()
392+
.silo_user_session_list(opctx, user_authn_list, pagparams)
393+
.await
394+
}
395+
376396
// The "local" identity provider (available only in `LocalOnly` Silos)
377397

378398
/// 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
@@ -6759,6 +6759,38 @@ impl NexusExternalApi for NexusExternalApiImpl {
67596759
.await
67606760
}
67616761

6762+
async fn user_session_list(
6763+
rqctx: RequestContext<Self::Context>,
6764+
path_params: Path<params::UserPath>,
6765+
query_params: Query<PaginatedById>,
6766+
) -> Result<HttpResponseOk<ResultsPage<views::ConsoleSession>>, HttpError> {
6767+
let apictx = rqctx.context();
6768+
let handler = async {
6769+
let nexus = &apictx.context.nexus;
6770+
let path = path_params.into_inner();
6771+
let query = query_params.into_inner();
6772+
let pag_params = data_page_params_for(&rqctx, &query)?;
6773+
let opctx =
6774+
crate::context::op_context_for_external_api(&rqctx).await?;
6775+
let sessions = nexus
6776+
.silo_user_session_list(&opctx, path.user_id, &pag_params)
6777+
.await?
6778+
.into_iter()
6779+
.map(views::ConsoleSession::from)
6780+
.collect();
6781+
Ok(HttpResponseOk(ScanById::results_page(
6782+
&query,
6783+
sessions,
6784+
&marker_for_id,
6785+
)?))
6786+
};
6787+
apictx
6788+
.context
6789+
.external_latencies
6790+
.instrument_dropshot_handler(&rqctx, handler)
6791+
.await
6792+
}
6793+
67626794
async fn user_logout(
67636795
rqctx: RequestContext<Self::Context>,
67646796
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
@@ -142,6 +142,8 @@ pub static DEMO_SILO_USER_ID_IN_SILO_URL: LazyLock<String> =
142142
LazyLock::new(|| "/v1/users/{id}".to_string());
143143
pub static DEMO_SILO_USER_TOKEN_LIST_URL: LazyLock<String> =
144144
LazyLock::new(|| "/v1/users/{id}/access-tokens".to_string());
145+
pub static DEMO_SILO_USER_SESSION_LIST_URL: LazyLock<String> =
146+
LazyLock::new(|| "/v1/users/{id}/sessions".to_string());
145147
pub static DEMO_SILO_USER_LOGOUT_URL: LazyLock<String> =
146148
LazyLock::new(|| "/v1/users/{id}/logout".to_string());
147149

@@ -1675,6 +1677,12 @@ pub static VERIFY_ENDPOINTS: LazyLock<Vec<VerifyEndpoint>> =
16751677
unprivileged_access: UnprivilegedAccess::None,
16761678
allowed_methods: vec![AllowedMethod::Get],
16771679
},
1680+
VerifyEndpoint {
1681+
url: &DEMO_SILO_USER_SESSION_LIST_URL,
1682+
visibility: Visibility::Public,
1683+
unprivileged_access: UnprivilegedAccess::None,
1684+
allowed_methods: vec![AllowedMethod::Get],
1685+
},
16781686
VerifyEndpoint {
16791687
url: &DEMO_SILO_USER_LOGOUT_URL,
16801688
visibility: Visibility::Public,

nexus/tests/integration_tests/unauthorized.rs

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

nexus/types/src/external_api/views.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,21 @@ impl SimpleIdentity for DeviceAccessToken {
10081008
}
10091009
}
10101010

1011+
/// View of a console session
1012+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
1013+
pub struct ConsoleSession {
1014+
/// A unique, immutable, system-controlled identifier for the session
1015+
pub id: Uuid,
1016+
pub time_created: DateTime<Utc>,
1017+
pub time_last_used: DateTime<Utc>,
1018+
}
1019+
1020+
impl SimpleIdentity for ConsoleSession {
1021+
fn id(&self) -> Uuid {
1022+
self.id
1023+
}
1024+
}
1025+
10111026
// OAUTH 2.0 DEVICE AUTHORIZATION REQUESTS & TOKENS
10121027

10131028
/// Response to an initial device authorization request.

openapi/nexus.json

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11589,6 +11589,75 @@
1158911589
}
1159011590
}
1159111591
},
11592+
"/v1/users/{user_id}/sessions": {
11593+
"get": {
11594+
"tags": [
11595+
"silos"
11596+
],
11597+
"summary": "List user's console sessions",
11598+
"operationId": "user_session_list",
11599+
"parameters": [
11600+
{
11601+
"in": "path",
11602+
"name": "user_id",
11603+
"description": "ID of the user",
11604+
"required": true,
11605+
"schema": {
11606+
"type": "string",
11607+
"format": "uuid"
11608+
}
11609+
},
11610+
{
11611+
"in": "query",
11612+
"name": "limit",
11613+
"description": "Maximum number of items returned by a single call",
11614+
"schema": {
11615+
"nullable": true,
11616+
"type": "integer",
11617+
"format": "uint32",
11618+
"minimum": 1
11619+
}
11620+
},
11621+
{
11622+
"in": "query",
11623+
"name": "page_token",
11624+
"description": "Token returned by previous call to retrieve the subsequent page",
11625+
"schema": {
11626+
"nullable": true,
11627+
"type": "string"
11628+
}
11629+
},
11630+
{
11631+
"in": "query",
11632+
"name": "sort_by",
11633+
"schema": {
11634+
"$ref": "#/components/schemas/IdSortMode"
11635+
}
11636+
}
11637+
],
11638+
"responses": {
11639+
"200": {
11640+
"description": "successful operation",
11641+
"content": {
11642+
"application/json": {
11643+
"schema": {
11644+
"$ref": "#/components/schemas/ConsoleSessionResultsPage"
11645+
}
11646+
}
11647+
}
11648+
},
11649+
"4XX": {
11650+
"$ref": "#/components/responses/Error"
11651+
},
11652+
"5XX": {
11653+
"$ref": "#/components/responses/Error"
11654+
}
11655+
},
11656+
"x-dropshot-pagination": {
11657+
"required": []
11658+
}
11659+
}
11660+
},
1159211661
"/v1/utilization": {
1159311662
"get": {
1159411663
"tags": [
@@ -16177,6 +16246,51 @@
1617716246
"items"
1617816247
]
1617916248
},
16249+
"ConsoleSession": {
16250+
"description": "View of a console session",
16251+
"type": "object",
16252+
"properties": {
16253+
"id": {
16254+
"description": "A unique, immutable, system-controlled identifier for the session",
16255+
"type": "string",
16256+
"format": "uuid"
16257+
},
16258+
"time_created": {
16259+
"type": "string",
16260+
"format": "date-time"
16261+
},
16262+
"time_last_used": {
16263+
"type": "string",
16264+
"format": "date-time"
16265+
}
16266+
},
16267+
"required": [
16268+
"id",
16269+
"time_created",
16270+
"time_last_used"
16271+
]
16272+
},
16273+
"ConsoleSessionResultsPage": {
16274+
"description": "A single page of results",
16275+
"type": "object",
16276+
"properties": {
16277+
"items": {
16278+
"description": "list of items on this page of results",
16279+
"type": "array",
16280+
"items": {
16281+
"$ref": "#/components/schemas/ConsoleSession"
16282+
}
16283+
},
16284+
"next_page": {
16285+
"nullable": true,
16286+
"description": "token used to fetch the next page of results (if any)",
16287+
"type": "string"
16288+
}
16289+
},
16290+
"required": [
16291+
"items"
16292+
]
16293+
},
1618016294
"Cumulativedouble": {
1618116295
"description": "A cumulative or counter data type.",
1618216296
"type": "object",

0 commit comments

Comments
 (0)