Skip to content

Commit e475578

Browse files
committed
session list endpoint
1 parent bdc9d59 commit e475578

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
@@ -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_session_list GET /v1/users/{user_id}/sessions
161162
user_token_list GET /v1/users/{user_id}/access-tokens
162163
user_view GET /v1/users/{user_id}
163164
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
@@ -3099,6 +3099,18 @@ pub trait NexusExternalApi {
30993099
query_params: Query<PaginatedById>,
31003100
) -> Result<HttpResponseOk<ResultsPage<views::DeviceAccessToken>>, HttpError>;
31013101

3102+
/// List user's console sessions
3103+
#[endpoint {
3104+
method = GET,
3105+
path = "/v1/users/{user_id}/sessions",
3106+
tags = ["silos"],
3107+
}]
3108+
async fn user_session_list(
3109+
rqctx: RequestContext<Self::Context>,
3110+
path_params: Path<params::UserPath>,
3111+
query_params: Query<PaginatedById>,
3112+
) -> Result<HttpResponseOk<ResultsPage<views::ConsoleSession>>, HttpError>;
3113+
31023114
/// Expire all of user's tokens and sessions
31033115
#[endpoint {
31043116
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
@@ -6914,6 +6914,38 @@ impl NexusExternalApi for NexusExternalApiImpl {
69146914
.await
69156915
}
69166916

6917+
async fn user_session_list(
6918+
rqctx: RequestContext<Self::Context>,
6919+
path_params: Path<params::UserPath>,
6920+
query_params: Query<PaginatedById>,
6921+
) -> Result<HttpResponseOk<ResultsPage<views::ConsoleSession>>, HttpError> {
6922+
let apictx = rqctx.context();
6923+
let handler = async {
6924+
let nexus = &apictx.context.nexus;
6925+
let path = path_params.into_inner();
6926+
let query = query_params.into_inner();
6927+
let pag_params = data_page_params_for(&rqctx, &query)?;
6928+
let opctx =
6929+
crate::context::op_context_for_external_api(&rqctx).await?;
6930+
let sessions = nexus
6931+
.silo_user_session_list(&opctx, path.user_id, &pag_params)
6932+
.await?
6933+
.into_iter()
6934+
.map(views::ConsoleSession::from)
6935+
.collect();
6936+
Ok(HttpResponseOk(ScanById::results_page(
6937+
&query,
6938+
sessions,
6939+
&marker_for_id,
6940+
)?))
6941+
};
6942+
apictx
6943+
.context
6944+
.external_latencies
6945+
.instrument_dropshot_handler(&rqctx, handler)
6946+
.await
6947+
}
6948+
69176949
async fn user_logout(
69186950
rqctx: RequestContext<Self::Context>,
69196951
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
@@ -143,6 +143,8 @@ pub static DEMO_SILO_USER_ID_IN_SILO_URL: LazyLock<String> =
143143
LazyLock::new(|| "/v1/users/{id}".to_string());
144144
pub static DEMO_SILO_USER_TOKEN_LIST_URL: LazyLock<String> =
145145
LazyLock::new(|| "/v1/users/{id}/access-tokens".to_string());
146+
pub static DEMO_SILO_USER_SESSION_LIST_URL: LazyLock<String> =
147+
LazyLock::new(|| "/v1/users/{id}/sessions".to_string());
146148
pub static DEMO_SILO_USER_LOGOUT_URL: LazyLock<String> =
147149
LazyLock::new(|| "/v1/users/{id}/logout".to_string());
148150

@@ -1694,6 +1696,12 @@ pub static VERIFY_ENDPOINTS: LazyLock<Vec<VerifyEndpoint>> =
16941696
unprivileged_access: UnprivilegedAccess::None,
16951697
allowed_methods: vec![AllowedMethod::Get],
16961698
},
1699+
VerifyEndpoint {
1700+
url: &DEMO_SILO_USER_SESSION_LIST_URL,
1701+
visibility: Visibility::Public,
1702+
unprivileged_access: UnprivilegedAccess::None,
1703+
allowed_methods: vec![AllowedMethod::Get],
1704+
},
16971705
VerifyEndpoint {
16981706
url: &DEMO_SILO_USER_LOGOUT_URL,
16991707
visibility: Visibility::Public,

nexus/tests/integration_tests/unauthorized.rs

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

nexus/types/src/external_api/views.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,21 @@ impl SimpleIdentity for DeviceAccessToken {
10001000
}
10011001
}
10021002

1003+
/// View of a console session
1004+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
1005+
pub struct ConsoleSession {
1006+
/// A unique, immutable, system-controlled identifier for the session
1007+
pub id: Uuid,
1008+
pub time_created: DateTime<Utc>,
1009+
pub time_last_used: DateTime<Utc>,
1010+
}
1011+
1012+
impl SimpleIdentity for ConsoleSession {
1013+
fn id(&self) -> Uuid {
1014+
self.id
1015+
}
1016+
}
1017+
10031018
// OAUTH 2.0 DEVICE AUTHORIZATION REQUESTS & TOKENS
10041019

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

openapi/nexus.json

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11717,6 +11717,75 @@
1171711717
}
1171811718
}
1171911719
},
11720+
"/v1/users/{user_id}/sessions": {
11721+
"get": {
11722+
"tags": [
11723+
"silos"
11724+
],
11725+
"summary": "List user's console sessions",
11726+
"operationId": "user_session_list",
11727+
"parameters": [
11728+
{
11729+
"in": "path",
11730+
"name": "user_id",
11731+
"description": "ID of the user",
11732+
"required": true,
11733+
"schema": {
11734+
"type": "string",
11735+
"format": "uuid"
11736+
}
11737+
},
11738+
{
11739+
"in": "query",
11740+
"name": "limit",
11741+
"description": "Maximum number of items returned by a single call",
11742+
"schema": {
11743+
"nullable": true,
11744+
"type": "integer",
11745+
"format": "uint32",
11746+
"minimum": 1
11747+
}
11748+
},
11749+
{
11750+
"in": "query",
11751+
"name": "page_token",
11752+
"description": "Token returned by previous call to retrieve the subsequent page",
11753+
"schema": {
11754+
"nullable": true,
11755+
"type": "string"
11756+
}
11757+
},
11758+
{
11759+
"in": "query",
11760+
"name": "sort_by",
11761+
"schema": {
11762+
"$ref": "#/components/schemas/IdSortMode"
11763+
}
11764+
}
11765+
],
11766+
"responses": {
11767+
"200": {
11768+
"description": "successful operation",
11769+
"content": {
11770+
"application/json": {
11771+
"schema": {
11772+
"$ref": "#/components/schemas/ConsoleSessionResultsPage"
11773+
}
11774+
}
11775+
}
11776+
},
11777+
"4XX": {
11778+
"$ref": "#/components/responses/Error"
11779+
},
11780+
"5XX": {
11781+
"$ref": "#/components/responses/Error"
11782+
}
11783+
},
11784+
"x-dropshot-pagination": {
11785+
"required": []
11786+
}
11787+
}
11788+
},
1172011789
"/v1/utilization": {
1172111790
"get": {
1172211791
"tags": [
@@ -16305,6 +16374,51 @@
1630516374
"items"
1630616375
]
1630716376
},
16377+
"ConsoleSession": {
16378+
"description": "View of a console session",
16379+
"type": "object",
16380+
"properties": {
16381+
"id": {
16382+
"description": "A unique, immutable, system-controlled identifier for the session",
16383+
"type": "string",
16384+
"format": "uuid"
16385+
},
16386+
"time_created": {
16387+
"type": "string",
16388+
"format": "date-time"
16389+
},
16390+
"time_last_used": {
16391+
"type": "string",
16392+
"format": "date-time"
16393+
}
16394+
},
16395+
"required": [
16396+
"id",
16397+
"time_created",
16398+
"time_last_used"
16399+
]
16400+
},
16401+
"ConsoleSessionResultsPage": {
16402+
"description": "A single page of results",
16403+
"type": "object",
16404+
"properties": {
16405+
"items": {
16406+
"description": "list of items on this page of results",
16407+
"type": "array",
16408+
"items": {
16409+
"$ref": "#/components/schemas/ConsoleSession"
16410+
}
16411+
},
16412+
"next_page": {
16413+
"nullable": true,
16414+
"description": "token used to fetch the next page of results (if any)",
16415+
"type": "string"
16416+
}
16417+
},
16418+
"required": [
16419+
"items"
16420+
]
16421+
},
1630816422
"Cumulativedouble": {
1630916423
"description": "A cumulative or counter data type.",
1631016424
"type": "object",

0 commit comments

Comments
 (0)