Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions common/src/api/external/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub enum LookupType {
ByName(String),
/** a specific id was requested */
ById(Uuid),
/** a session token was requested */
BySessionToken(String),
}

impl LookupType {
Expand Down Expand Up @@ -180,6 +182,9 @@ impl From<Error> for HttpError {
let (lookup_field, lookup_value) = match lt {
LookupType::ByName(name) => ("name", name),
LookupType::ById(id) => ("id", id.to_string()),
LookupType::BySessionToken(token) => {
("session token", token)
}
};
let message = format!(
"not found: {} with {} \"{}\"",
Expand Down
3 changes: 3 additions & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,9 @@ impl TryFrom<i64> for Generation {
#[display(style = "kebab-case")]
pub enum ResourceType {
Fleet,
Silo,
SiloUser,
ConsoleSession,
Organization,
Project,
Dataset,
Expand Down
48 changes: 45 additions & 3 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,57 @@ CREATE TABLE omicron.public.volume (
data TEXT NOT NULL
);

/*
* Silos
*/

CREATE TABLE omicron.public.silo (
/* Identity metadata */
id UUID PRIMARY KEY,

name STRING(128) NOT NULL,
description STRING(512) NOT NULL,

discoverable BOOL NOT NULL,

time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,
time_deleted TIMESTAMPTZ,

/* child resource generation number, per RFD 192 */
rcgen INT NOT NULL
);

CREATE UNIQUE INDEX ON omicron.public.silo (
name
) WHERE
time_deleted IS NULL;

/*
* Silo users
*/
CREATE TABLE omicron.public.silo_user (
/* silo user id */
id UUID PRIMARY KEY,

silo_id UUID NOT NULL,

time_created TIMESTAMPTZ NOT NULL,
time_modified TIMESTAMPTZ NOT NULL,
time_deleted TIMESTAMPTZ
);

/*
* Organizations
*/

CREATE TABLE omicron.public.organization (
/* Identity metadata */
id UUID PRIMARY KEY,

/* FK into Silo table */
silo_id UUID NOT NULL,

name STRING(63) NOT NULL,
description STRING(512) NOT NULL,
time_created TIMESTAMPTZ NOT NULL,
Expand Down Expand Up @@ -707,9 +751,7 @@ CREATE TABLE omicron.public.console_session (
token STRING(40) PRIMARY KEY,
time_created TIMESTAMPTZ NOT NULL,
time_last_used TIMESTAMPTZ NOT NULL,
-- we're agnostic about what this means until work starts on users, but the
-- naive interpretation is that it points to a row in the User table
user_id UUID NOT NULL
silo_user_id UUID NOT NULL
);

-- to be used for cleaning up old tokens
Expand Down
16 changes: 9 additions & 7 deletions nexus/src/authn/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ impl<T> Authenticator<T>
where
T: Send + Sync + 'static,
{
/// Build a new authentiator that allows only the specified schemes
/// Build a new authenticator that allows only the specified schemes
pub fn new(
allowed_schemes: Vec<Box<dyn HttpAuthnScheme<T>>>,
) -> Authenticator<T> {
Expand Down Expand Up @@ -187,9 +187,10 @@ mod test {
let count1 = Arc::new(AtomicU8::new(0));
let mut expected_count1 = 0;
let name1 = authn::SchemeName("grunt1");
let actor1 = authn::Actor(
"1c91bab2-4841-669f-cc32-de80da5bbf39".parse().unwrap(),
);
let actor1 = authn::Actor {
id: "1c91bab2-4841-669f-cc32-de80da5bbf39".parse().unwrap(),
silo_id: *crate::db::fixed_data::silo_builtin::SILO_ID,
};
let grunt1 = Box::new(GruntScheme {
name: name1,
next: Arc::clone(&flag1),
Expand All @@ -201,9 +202,10 @@ mod test {
let count2 = Arc::new(AtomicU8::new(0));
let mut expected_count2 = 0;
let name2 = authn::SchemeName("grunt2");
let actor2 = authn::Actor(
"799684af-533a-cb66-b5ac-ab55a791d5ef".parse().unwrap(),
);
let actor2 = authn::Actor {
id: "799684af-533a-cb66-b5ac-ab55a791d5ef".parse().unwrap(),
silo_id: *crate::db::fixed_data::silo_builtin::SILO_ID,
};
let grunt2 = Box::new(GruntScheme {
name: name2,
next: Arc::clone(&flag2),
Expand Down
26 changes: 21 additions & 5 deletions nexus/src/authn/external/session_cookie.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ use uuid::Uuid;
// https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html

pub trait Session {
fn user_id(&self) -> Uuid;
fn silo_user_id(&self) -> Uuid;
fn silo_id(&self) -> Uuid;
fn time_last_used(&self) -> DateTime<Utc>;
fn time_created(&self) -> DateTime<Utc>;
}
Expand Down Expand Up @@ -104,7 +105,8 @@ where
}
};

let actor = Actor(session.user_id());
let actor =
Actor { id: session.silo_user_id(), silo_id: session.silo_id() };

// if the session has gone unused for longer than idle_timeout, it is expired
let now = Utc::now();
Expand Down Expand Up @@ -188,13 +190,18 @@ mod test {

#[derive(Clone, Copy)]
struct FakeSession {
silo_user_id: Uuid,
silo_id: Uuid,
time_created: DateTime<Utc>,
time_last_used: DateTime<Utc>,
}

impl Session for FakeSession {
fn user_id(&self) -> Uuid {
Uuid::new_v4()
fn silo_user_id(&self) -> Uuid {
self.silo_user_id
}
fn silo_id(&self) -> Uuid {
self.silo_id
}
fn time_created(&self) -> DateTime<Utc> {
self.time_created
Expand Down Expand Up @@ -277,6 +284,8 @@ mod test {
sessions: Mutex::new(HashMap::from([(
"abc".to_string(),
FakeSession {
silo_user_id: Uuid::new_v4(),
silo_id: Uuid::new_v4(),
time_last_used: Utc::now() - Duration::hours(2),
time_created: Utc::now() - Duration::hours(2),
},
Expand All @@ -301,6 +310,8 @@ mod test {
sessions: Mutex::new(HashMap::from([(
"abc".to_string(),
FakeSession {
silo_user_id: Uuid::new_v4(),
silo_id: Uuid::new_v4(),
time_last_used: Utc::now(),
time_created: Utc::now() - Duration::hours(20),
},
Expand All @@ -326,7 +337,12 @@ mod test {
let context = TestServerContext {
sessions: Mutex::new(HashMap::from([(
"abc".to_string(),
FakeSession { time_last_used, time_created: Utc::now() },
FakeSession {
silo_user_id: Uuid::new_v4(),
silo_id: Uuid::new_v4(),
time_last_used,
time_created: Utc::now(),
},
)])),
};
let result = authn_with_cookie(&context, Some("session=abc")).await;
Expand Down
31 changes: 19 additions & 12 deletions nexus/src/authn/external/spoof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ const SPOOF_PREFIX: &str = "oxide-spoof-";

lazy_static! {
/// Actor (id) used for the special "bad credentials" error
static ref SPOOF_RESERVED_BAD_CREDS_ACTOR: Actor =
Actor("22222222-2222-2222-2222-222222222222".parse().unwrap());
static ref SPOOF_RESERVED_BAD_CREDS_ACTOR: Actor = Actor {
id: "22222222-2222-2222-2222-222222222222".parse().unwrap(),
silo_id: *crate::db::fixed_data::silo_builtin::SILO_ID,
};
/// Complete HTTP header value to trigger the "bad actor" error
pub static ref SPOOF_HEADER_BAD_ACTOR: Authorization<Bearer> =
make_header_value_str(SPOOF_RESERVED_BAD_ACTOR).unwrap();
Expand Down Expand Up @@ -119,7 +121,13 @@ fn authn_spoof(raw_value: Option<&Authorization<Bearer>>) -> SchemeResult {
}

match Uuid::parse_str(str_value).context("parsing header value as UUID") {
Ok(id) => SchemeResult::Authenticated(Details { actor: Actor(id) }),
Ok(id) => {
let actor = Actor {
id,
silo_id: *crate::db::fixed_data::silo_builtin::SILO_ID,
};
SchemeResult::Authenticated(Details { actor })
}
Err(source) => SchemeResult::Failed(Reason::BadFormat { source }),
}
}
Expand Down Expand Up @@ -160,15 +168,12 @@ pub fn make_header_value_raw(

#[cfg(test)]
mod test {
use super::super::super::Details;
use super::super::super::Reason;
use super::super::SchemeResult;
use super::authn_spoof;
use super::make_header_value;
use super::make_header_value_raw;
use super::make_header_value_str;
use crate::authn;
use authn::Actor;
use headers::authorization::Bearer;
use headers::authorization::Credentials;
use headers::Authorization;
Expand Down Expand Up @@ -228,12 +233,14 @@ mod test {

// Success case: the client provided a valid uuid in the header.
let success_case = authn_spoof(Some(&test_header));
assert!(matches!(
success_case,
SchemeResult::Authenticated(
Details { actor: Actor(i) }
) if i == test_uuid
));
match success_case {
SchemeResult::Authenticated(details) => {
assert_eq!(details.actor.id, test_uuid);
}
_ => {
assert!(false);
}
};
}

#[test]
Expand Down
Loading