Skip to content

Commit c0f8d23

Browse files
authored
[1/n] Add id column on tokens and sessions (#8137)
Closes #8139. Token IDs are used in #8227 and #8231 for token CRUD. WIP. Everything compiles and existing tests work, but we are not yet testing retrieval by ID and we need to think about authz checks on the datastore functions for retrieving tokens and sessions by token string. This PR would be a lot shorter if not for the fact that the `token` column was the primary key on both tables, so adding the `id` requires a bunch of migration steps to change the primary key. There were also a lot of changes to code and tests around making things ID-centric instead of token-centric. * [x] SQL migrations for adding `id` col and changing primary key to that * [x] Add `TypedUuid` to session and token DB models * [x] Update `authz_resource!` and `lookup_resource!` calls for new primary key * [x] Update `Session` trait methods to use `id` instead of `token` * [x] Update app and datastore session and token methods to use ID instead of token * [x] Update two distinct but nearly identical `FakeSession` implementations (one for unit tests, one for integration tests) to a) track sessions in a `Vec` instead of a `HashMap` with token keys * [x] Figure out authz situation in new fetch session/token by token string methods
1 parent 5ecc899 commit c0f8d23

File tree

33 files changed

+493
-181
lines changed

33 files changed

+493
-181
lines changed

nexus/auth/src/authn/external/session_cookie.rs

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ use chrono::{DateTime, Duration, Utc};
1313
use dropshot::HttpError;
1414
use http::HeaderValue;
1515
use nexus_types::authn::cookies::parse_cookies;
16+
use omicron_uuid_kinds::ConsoleSessionUuid;
1617
use slog::debug;
1718
use uuid::Uuid;
1819

1920
// many parts of the implementation will reference this OWASP guide
2021
// https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
2122

2223
pub trait Session {
24+
fn id(&self) -> ConsoleSessionUuid;
2325
fn silo_user_id(&self) -> Uuid;
2426
fn silo_id(&self) -> Uuid;
2527
fn time_last_used(&self) -> DateTime<Utc>;
@@ -39,11 +41,11 @@ pub trait SessionStore {
3941
/// Extend session by updating time_last_used to now
4042
async fn session_update_last_used(
4143
&self,
42-
token: String,
44+
id: ConsoleSessionUuid,
4345
) -> Option<Self::SessionModel>;
4446

4547
/// Mark session expired
46-
async fn session_expire(&self, token: String) -> Option<()>;
48+
async fn session_expire(&self, id: ConsoleSessionUuid) -> Option<()>;
4749

4850
/// Maximum time session can remain idle before expiring
4951
fn session_idle_timeout(&self) -> Duration;
@@ -131,7 +133,7 @@ where
131133
// expired
132134
let now = Utc::now();
133135
if session.time_last_used() + ctx.session_idle_timeout() < now {
134-
let expired_session = ctx.session_expire(token.clone()).await;
136+
let expired_session = ctx.session_expire(session.id()).await;
135137
if expired_session.is_none() {
136138
debug!(log, "failed to expire session")
137139
}
@@ -151,7 +153,7 @@ where
151153
// existed longer than absolute_timeout, it is expired and we can no
152154
// longer extend the session
153155
if session.time_created() + ctx.session_absolute_timeout() < now {
154-
let expired_session = ctx.session_expire(token.clone()).await;
156+
let expired_session = ctx.session_expire(session.id()).await;
155157
if expired_session.is_none() {
156158
debug!(log, "failed to expire session")
157159
}
@@ -172,7 +174,7 @@ where
172174
// authenticated for this request at this point. The next request might
173175
// be wrongly considered idle, but that's a problem for the next
174176
// request.
175-
let updated_session = ctx.session_update_last_used(token).await;
177+
let updated_session = ctx.session_update_last_used(session.id()).await;
176178
if updated_session.is_none() {
177179
debug!(log, "failed to extend session")
178180
}
@@ -199,26 +201,31 @@ mod test {
199201
use async_trait::async_trait;
200202
use chrono::{DateTime, Duration, Utc};
201203
use http;
204+
use omicron_uuid_kinds::ConsoleSessionUuid;
202205
use slog;
203-
use std::collections::HashMap;
204206
use std::sync::Mutex;
205207
use uuid::Uuid;
206208

207209
// the mutex is annoying, but we need it in order to mutate the hashmap
208210
// without passing TestServerContext around as mutable
209211
struct TestServerContext {
210-
sessions: Mutex<HashMap<String, FakeSession>>,
212+
sessions: Mutex<Vec<FakeSession>>,
211213
}
212214

213-
#[derive(Clone, Copy)]
215+
#[derive(Clone)]
214216
struct FakeSession {
217+
id: ConsoleSessionUuid,
218+
token: String,
215219
silo_user_id: Uuid,
216220
silo_id: Uuid,
217221
time_created: DateTime<Utc>,
218222
time_last_used: DateTime<Utc>,
219223
}
220224

221225
impl Session for FakeSession {
226+
fn id(&self) -> ConsoleSessionUuid {
227+
self.id
228+
}
222229
fn silo_user_id(&self) -> Uuid {
223230
self.silo_user_id
224231
}
@@ -241,23 +248,34 @@ mod test {
241248
&self,
242249
token: String,
243250
) -> Option<Self::SessionModel> {
244-
self.sessions.lock().unwrap().get(&token).map(|s| *s)
251+
self.sessions
252+
.lock()
253+
.unwrap()
254+
.iter()
255+
.find(|s| s.token == token)
256+
.map(|s| s.clone())
245257
}
246258

247259
async fn session_update_last_used(
248260
&self,
249-
token: String,
261+
id: ConsoleSessionUuid,
250262
) -> Option<Self::SessionModel> {
251263
let mut sessions = self.sessions.lock().unwrap();
252-
let session = *sessions.get(&token).unwrap();
253-
let new_session =
254-
FakeSession { time_last_used: Utc::now(), ..session };
255-
(*sessions).insert(token, new_session)
264+
if let Some(pos) = sessions.iter().position(|s| s.id == id) {
265+
let new_session = FakeSession {
266+
time_last_used: Utc::now(),
267+
..sessions[pos].clone()
268+
};
269+
sessions[pos] = new_session.clone();
270+
Some(new_session)
271+
} else {
272+
None
273+
}
256274
}
257275

258-
async fn session_expire(&self, token: String) -> Option<()> {
276+
async fn session_expire(&self, id: ConsoleSessionUuid) -> Option<()> {
259277
let mut sessions = self.sessions.lock().unwrap();
260-
(*sessions).remove(&token);
278+
sessions.retain(|s| s.id != id);
261279
Some(())
262280
}
263281

@@ -295,32 +313,29 @@ mod test {
295313

296314
#[tokio::test]
297315
async fn test_missing_cookie() {
298-
let context =
299-
TestServerContext { sessions: Mutex::new(HashMap::new()) };
316+
let context = TestServerContext { sessions: Mutex::new(Vec::new()) };
300317
let result = authn_with_cookie(&context, None).await;
301318
assert!(matches!(result, SchemeResult::NotRequested));
302319
}
303320

304321
#[tokio::test]
305322
async fn test_other_cookie() {
306-
let context =
307-
TestServerContext { sessions: Mutex::new(HashMap::new()) };
323+
let context = TestServerContext { sessions: Mutex::new(Vec::new()) };
308324
let result = authn_with_cookie(&context, Some("other=def")).await;
309325
assert!(matches!(result, SchemeResult::NotRequested));
310326
}
311327

312328
#[tokio::test]
313329
async fn test_expired_cookie_idle() {
314330
let context = TestServerContext {
315-
sessions: Mutex::new(HashMap::from([(
316-
"abc".to_string(),
317-
FakeSession {
318-
silo_user_id: Uuid::new_v4(),
319-
silo_id: Uuid::new_v4(),
320-
time_last_used: Utc::now() - Duration::hours(2),
321-
time_created: Utc::now() - Duration::hours(2),
322-
},
323-
)])),
331+
sessions: Mutex::new(vec![FakeSession {
332+
id: ConsoleSessionUuid::new_v4(),
333+
token: "abc".to_string(),
334+
silo_user_id: Uuid::new_v4(),
335+
silo_id: Uuid::new_v4(),
336+
time_last_used: Utc::now() - Duration::hours(2),
337+
time_created: Utc::now() - Duration::hours(2),
338+
}]),
324339
};
325340
let result = authn_with_cookie(&context, Some("session=abc")).await;
326341
assert!(matches!(
@@ -332,21 +347,21 @@ mod test {
332347
));
333348

334349
// key should be removed from sessions dict, i.e., session deleted
335-
assert!(!context.sessions.lock().unwrap().contains_key("abc"))
350+
let sessions = context.sessions.lock().unwrap();
351+
assert!(!sessions.iter().any(|s| s.token == "abc"))
336352
}
337353

338354
#[tokio::test]
339355
async fn test_expired_cookie_absolute() {
340356
let context = TestServerContext {
341-
sessions: Mutex::new(HashMap::from([(
342-
"abc".to_string(),
343-
FakeSession {
344-
silo_user_id: Uuid::new_v4(),
345-
silo_id: Uuid::new_v4(),
346-
time_last_used: Utc::now(),
347-
time_created: Utc::now() - Duration::hours(20),
348-
},
349-
)])),
357+
sessions: Mutex::new(vec![FakeSession {
358+
id: ConsoleSessionUuid::new_v4(),
359+
token: "abc".to_string(),
360+
silo_user_id: Uuid::new_v4(),
361+
silo_id: Uuid::new_v4(),
362+
time_last_used: Utc::now(),
363+
time_created: Utc::now() - Duration::hours(20),
364+
}]),
350365
};
351366
let result = authn_with_cookie(&context, Some("session=abc")).await;
352367
assert!(matches!(
@@ -359,22 +374,21 @@ mod test {
359374

360375
// key should be removed from sessions dict, i.e., session deleted
361376
let sessions = context.sessions.lock().unwrap();
362-
assert!(!sessions.contains_key("abc"))
377+
assert!(!sessions.iter().any(|s| s.token == "abc"))
363378
}
364379

365380
#[tokio::test]
366381
async fn test_valid_cookie() {
367382
let time_last_used = Utc::now() - Duration::seconds(5);
368383
let context = TestServerContext {
369-
sessions: Mutex::new(HashMap::from([(
370-
"abc".to_string(),
371-
FakeSession {
372-
silo_user_id: Uuid::new_v4(),
373-
silo_id: Uuid::new_v4(),
374-
time_last_used,
375-
time_created: Utc::now(),
376-
},
377-
)])),
384+
sessions: Mutex::new(vec![FakeSession {
385+
id: ConsoleSessionUuid::new_v4(),
386+
token: "abc".to_string(),
387+
silo_user_id: Uuid::new_v4(),
388+
silo_id: Uuid::new_v4(),
389+
time_last_used,
390+
time_created: Utc::now(),
391+
}]),
378392
};
379393
let result = authn_with_cookie(&context, Some("session=abc")).await;
380394
assert!(matches!(
@@ -384,13 +398,13 @@ mod test {
384398

385399
// valid cookie should have updated time_last_used
386400
let sessions = context.sessions.lock().unwrap();
387-
assert!(sessions.get("abc").unwrap().time_last_used > time_last_used)
401+
let session = sessions.iter().find(|s| s.token == "abc").unwrap();
402+
assert!(session.time_last_used > time_last_used)
388403
}
389404

390405
#[tokio::test]
391406
async fn test_garbage_cookie() {
392-
let context =
393-
TestServerContext { sessions: Mutex::new(HashMap::new()) };
407+
let context = TestServerContext { sessions: Mutex::new(Vec::new()) };
394408
let result =
395409
authn_with_cookie(&context, Some("unparsable garbage!!!!!1")).await;
396410
assert!(matches!(result, SchemeResult::NotRequested));

nexus/auth/src/authz/api_resources.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,7 @@ authz_resource! {
939939
authz_resource! {
940940
name = "ConsoleSession",
941941
parent = "Fleet",
942-
primary_key = String,
942+
primary_key = { uuid_kind = ConsoleSessionKind },
943943
roles_allowed = false,
944944
polar_snippet = FleetChild,
945945
}
@@ -955,7 +955,7 @@ authz_resource! {
955955
authz_resource! {
956956
name = "DeviceAccessToken",
957957
parent = "Fleet",
958-
primary_key = String, // token
958+
primary_key = { uuid_kind = AccessTokenKind },
959959
roles_allowed = false,
960960
polar_snippet = FleetChild,
961961
}

nexus/auth/src/context.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::authz::AuthorizedResource;
1111
use crate::storage::Storage;
1212
use chrono::{DateTime, Utc};
1313
use omicron_common::api::external::Error;
14+
use omicron_uuid_kinds::ConsoleSessionUuid;
1415
use slog::debug;
1516
use slog::o;
1617
use slog::trace;
@@ -352,6 +353,10 @@ impl OpContext {
352353
}
353354

354355
impl Session for ConsoleSessionWithSiloId {
356+
fn id(&self) -> ConsoleSessionUuid {
357+
self.console_session.id()
358+
}
359+
355360
fn silo_user_id(&self) -> Uuid {
356361
self.console_session.silo_user_id
357362
}

nexus/db-lookup/src/lookup.rs

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ use nexus_types::identity::Resource;
2626
use omicron_common::api::external::Error;
2727
use omicron_common::api::external::InternalContext;
2828
use omicron_common::api::external::{LookupResult, LookupType, ResourceType};
29+
use omicron_uuid_kinds::AccessTokenKind;
2930
use omicron_uuid_kinds::AlertReceiverUuid;
3031
use omicron_uuid_kinds::AlertUuid;
32+
use omicron_uuid_kinds::ConsoleSessionUuid;
3133
use omicron_uuid_kinds::PhysicalDiskUuid;
3234
use omicron_uuid_kinds::SupportBundleUuid;
3335
use omicron_uuid_kinds::TufArtifactKind;
@@ -199,19 +201,12 @@ impl<'a> LookupPath<'a> {
199201

200202
// Fleet-level resources
201203

202-
/// Select a resource of type ConsoleSession, identified by its `token`
203-
pub fn console_session_token<'b, 'c>(
204+
/// Select a resource of type ConsoleSession, identified by its `id`
205+
pub fn console_session_id(
204206
self,
205-
token: &'b str,
206-
) -> ConsoleSession<'c>
207-
where
208-
'a: 'c,
209-
'b: 'c,
210-
{
211-
ConsoleSession::PrimaryKey(
212-
Root { lookup_root: self },
213-
token.to_string(),
214-
)
207+
id: ConsoleSessionUuid,
208+
) -> ConsoleSession<'a> {
209+
ConsoleSession::PrimaryKey(Root { lookup_root: self }, id)
215210
}
216211

217212
/// Select a resource of type DeviceAuthRequest, identified by its `user_code`
@@ -229,19 +224,12 @@ impl<'a> LookupPath<'a> {
229224
)
230225
}
231226

232-
/// Select a resource of type DeviceAccessToken, identified by its `token`
233-
pub fn device_access_token<'b, 'c>(
227+
/// Select a resource of type DeviceAccessToken, identified by its `id`
228+
pub fn device_access_token_id(
234229
self,
235-
token: &'b str,
236-
) -> DeviceAccessToken<'c>
237-
where
238-
'a: 'c,
239-
'b: 'c,
240-
{
241-
DeviceAccessToken::PrimaryKey(
242-
Root { lookup_root: self },
243-
token.to_string(),
244-
)
230+
id: TypedUuid<AccessTokenKind>,
231+
) -> DeviceAccessToken<'a> {
232+
DeviceAccessToken::PrimaryKey(Root { lookup_root: self }, id)
245233
}
246234

247235
/// Select a resource of type RoleBuiltin, identified by its `name`
@@ -761,9 +749,7 @@ lookup_resource! {
761749
ancestors = [],
762750
lookup_by_name = false,
763751
soft_deletes = false,
764-
primary_key_columns = [
765-
{ column_name = "token", rust_type = String },
766-
]
752+
primary_key_columns = [ { column_name = "id", uuid_kind = ConsoleSessionKind } ]
767753
}
768754

769755
lookup_resource! {
@@ -781,9 +767,7 @@ lookup_resource! {
781767
ancestors = [],
782768
lookup_by_name = false,
783769
soft_deletes = false,
784-
primary_key_columns = [
785-
{ column_name = "token", rust_type = String },
786-
]
770+
primary_key_columns = [ { column_name = "id", uuid_kind = AccessTokenKind } ]
787771
}
788772

789773
lookup_resource! {

0 commit comments

Comments
 (0)