Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -66,6 +66,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 @@ -158,6 +160,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 @@ -514,6 +514,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 @@ -747,9 +791,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 @@ -106,7 +107,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 @@ -190,13 +192,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 @@ -279,6 +286,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 @@ -303,6 +312,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 @@ -328,7 +339,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