Skip to content
Open
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions nexus-config/src/nexus_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,7 @@ pub enum SchemeName {
Spoof,
SessionCookie,
AccessToken,
ScimToken,
}

impl std::str::FromStr for SchemeName {
Expand All @@ -932,6 +933,7 @@ impl std::str::FromStr for SchemeName {
"spoof" => Ok(SchemeName::Spoof),
"session_cookie" => Ok(SchemeName::SessionCookie),
"access_token" => Ok(SchemeName::AccessToken),
"scim_token" => Ok(SchemeName::ScimToken),
_ => Err(anyhow!("unsupported authn scheme: {:?}", s)),
}
}
Expand All @@ -943,6 +945,7 @@ impl std::fmt::Display for SchemeName {
SchemeName::Spoof => "spoof",
SchemeName::SessionCookie => "session_cookie",
SchemeName::AccessToken => "access_token",
SchemeName::ScimToken => "scim",
})
}
}
Expand Down
1 change: 1 addition & 0 deletions nexus/auth/src/authn/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use slog::trace;
use std::borrow::Borrow;
use uuid::Uuid;

pub mod scim;
pub mod session_cookie;
pub mod spoof;
pub mod token;
Expand Down
99 changes: 99 additions & 0 deletions nexus/auth/src/authn/external/scim.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! SCIM-only bearer tokens
use super::super::Details;
use super::HttpAuthnScheme;
use super::Reason;
use super::SchemeResult;
use crate::authn;
use async_trait::async_trait;
use headers::HeaderMapExt;
use headers::authorization::{Authorization, Bearer};

// This scheme is intended only for SCIM provisioning clients.
//
// For ease of integration into existing clients, we use RFC 6750 bearer tokens.
// This mechanism in turn uses HTTP's "Authorization" header. In practice, it
// looks like this:
//
// Authorization: Bearer oxide-scim-01c90c58085fed6a230d137b9b9b5e7501d0a523
// ^^^^^^^^^^^^^ ^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// | | | |
// | | | +--- specifies the token itself
// | | +--------------- specifies this "token" mechanism
// | +---------------------- specifies RFC 6750 bearer tokens
// +------------------------------------- standard HTTP authentication hdr
//
// (That's not a typo -- the "authorization" header is generally used to specify
// _authentication_ information. Similarly, the "Unauthorized" HTTP response
// code usually describes an _authentication_ error.)

pub const SCIM_TOKEN_SCHEME_NAME: authn::SchemeName =
authn::SchemeName("scim_token");

/// Prefix used on the bearer token to identify this scheme
// RFC 6750 expects bearer tokens to be opaque base64-encoded data. In our case,
// the data we want to represent (this prefix, plus valid tokens) are subsets of
// the base64 character set, so we do not bother encoding them.
const TOKEN_PREFIX: &str = "oxide-scim-";

/// Implements a SCIM provisioning client specific bearer-token-based
/// authentication scheme.
#[derive(Debug)]
pub struct HttpAuthnScimToken;

#[async_trait]
impl<T> HttpAuthnScheme<T> for HttpAuthnScimToken
where
T: ScimTokenContext + Send + Sync + 'static,
{
fn name(&self) -> authn::SchemeName {
SCIM_TOKEN_SCHEME_NAME
}

async fn authn(
&self,
ctx: &T,
_log: &slog::Logger,
request: &dropshot::RequestInfo,
) -> SchemeResult {
let headers = request.headers();
match parse_token(headers.typed_get().as_ref()) {
Err(error) => SchemeResult::Failed(error),
Ok(None) => SchemeResult::NotRequested,
Ok(Some(token)) => match ctx.scim_token_actor(token).await {
Err(error) => SchemeResult::Failed(error),
Ok(actor) => SchemeResult::Authenticated(Details { actor }),
},
}
}
}

fn parse_token(
raw_value: Option<&Authorization<Bearer>>,
) -> Result<Option<String>, Reason> {
let token = match raw_value {
None => return Ok(None),
Some(bearer) => bearer.token(),
};

if !token.starts_with(TOKEN_PREFIX) {
// This is some other kind of bearer token. Maybe another scheme knows
// how to deal with it.
return Ok(None);
}

Ok(Some(token[TOKEN_PREFIX.len()..].to_string()))
}

/// A context that can look up a Actor::Scim from a token.
#[async_trait]
pub trait ScimTokenContext {
async fn scim_token_actor(
&self,
token: String,
) -> Result<authn::Actor, Reason>;
}
66 changes: 46 additions & 20 deletions nexus/auth/src/authn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ pub use nexus_db_fixed_data::silo_user::USER_TEST_PRIVILEGED;
pub use nexus_db_fixed_data::silo_user::USER_TEST_UNPRIVILEGED;
pub use nexus_db_fixed_data::user_builtin::USER_DB_INIT;
pub use nexus_db_fixed_data::user_builtin::USER_EXTERNAL_AUTHN;
pub use nexus_db_fixed_data::user_builtin::USER_EXTERNAL_SCIM;
pub use nexus_db_fixed_data::user_builtin::USER_INTERNAL_API;
pub use nexus_db_fixed_data::user_builtin::USER_INTERNAL_READ;
pub use nexus_db_fixed_data::user_builtin::USER_SAGA_RECOVERY;
Expand All @@ -46,6 +45,7 @@ use nexus_types::external_api::shared::SiloRole;
use nexus_types::identity::Asset;
use omicron_common::api::external::LookupType;
use omicron_uuid_kinds::BuiltInUserUuid;
use omicron_uuid_kinds::GenericUuid;
use omicron_uuid_kinds::SiloUserUuid;
use serde::Deserialize;
use serde::Serialize;
Expand Down Expand Up @@ -126,6 +126,8 @@ impl Context {
/// Built-in users have no Silo, and so they usually can't do anything that
/// might use a Silo. You usually want to use [`Context::silo_required()`]
/// if you don't expect to be looking at a built-in user.
///
/// Additionally, non-user Actors may also be associated with a Silo.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to say that by "non-user actor" you mean the SCIM arm of the match. It's obvious when you're looking at the diff but might not be so obvious later.

pub fn silo_or_builtin(
&self,
) -> Result<Option<authz::Silo>, omicron_common::api::external::Error> {
Expand All @@ -136,6 +138,11 @@ impl Context {
LookupType::ById(*silo_id),
)),
Actor::UserBuiltin { .. } => None,
Actor::Scim { silo_id } => Some(authz::Silo::new(
authz::FLEET,
*silo_id,
LookupType::ById(*silo_id),
)),
})
}

Expand Down Expand Up @@ -200,12 +207,6 @@ impl Context {
Context::context_for_builtin_user(USER_SERVICE_BALANCER.id)
}

/// Returns an authenticated context for use for authenticating SCIM
/// requests
pub fn external_scim() -> Context {
Context::context_for_builtin_user(USER_EXTERNAL_SCIM.id)
}

fn context_for_builtin_user(user_builtin_id: BuiltInUserUuid) -> Context {
Context {
kind: Kind::Authenticated(
Expand Down Expand Up @@ -260,6 +261,18 @@ impl Context {
schemes_tried: Vec::new(),
}
}

/// Returns an authenticated context for a Silo's SCIM Actor. Not marked as
/// #[cfg(test)] so that this is available in integration tests.
pub fn for_scim(silo_id: Uuid) -> Context {
Context {
kind: Kind::Authenticated(
Details { actor: Actor::Scim { silo_id } },
None,
),
schemes_tried: Vec::new(),
}
}
}

/// Authentication-related policy derived from a user's Silo
Expand Down Expand Up @@ -307,7 +320,6 @@ mod test {
use super::USER_TEST_PRIVILEGED;
use super::USER_TEST_UNPRIVILEGED;
use nexus_db_fixed_data::user_builtin::USER_EXTERNAL_AUTHN;
use nexus_db_fixed_data::user_builtin::USER_EXTERNAL_SCIM;
use nexus_types::identity::Asset;

#[test]
Expand Down Expand Up @@ -350,10 +362,6 @@ mod test {
let authn = Context::internal_api();
let actor = authn.actor().unwrap();
assert_eq!(actor.built_in_user_id(), Some(USER_INTERNAL_API.id));

let authn = Context::external_scim();
let actor = authn.actor().unwrap();
assert_eq!(actor.built_in_user_id(), Some(USER_EXTERNAL_SCIM.id));
}
}

Expand Down Expand Up @@ -382,38 +390,52 @@ pub struct Details {
pub enum Actor {
UserBuiltin { user_builtin_id: BuiltInUserUuid },
SiloUser { silo_user_id: SiloUserUuid, silo_id: Uuid },
Scim { silo_id: Uuid },
}

impl Actor {
pub fn silo_id(&self) -> Option<Uuid> {
match self {
Actor::UserBuiltin { .. } => None,
Actor::SiloUser { silo_id, .. } => Some(*silo_id),
Actor::Scim { .. } => None, // XXX scim actor does have a silo id?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

easy fix

}
}

pub fn silo_user_id(&self) -> Option<SiloUserUuid> {
match self {
Actor::UserBuiltin { .. } => None,
Actor::SiloUser { silo_user_id, .. } => Some(*silo_user_id),
Actor::Scim { .. } => None,
}
}

pub fn built_in_user_id(&self) -> Option<BuiltInUserUuid> {
match self {
Actor::UserBuiltin { user_builtin_id } => Some(*user_builtin_id),
Actor::SiloUser { .. } => None,
Actor::Scim { .. } => None,
}
}
}

impl From<&Actor> for nexus_db_model::IdentityType {
fn from(actor: &Actor) -> nexus_db_model::IdentityType {
match actor {
Actor::UserBuiltin { .. } => {
nexus_db_model::IdentityType::UserBuiltin
}
Actor::SiloUser { .. } => nexus_db_model::IdentityType::SiloUser,
/// Return a generic UUID and db-model IdentityType for use with looking up
/// role assignments, or None if a role assignment for this type of Actor is
/// invalid.
pub fn id_and_type_for_role_assignment(
&self,
) -> Option<(Uuid, nexus_db_model::IdentityType)> {
match &self {
Actor::UserBuiltin { user_builtin_id } => Some((
user_builtin_id.into_untyped_uuid(),
nexus_db_model::IdentityType::UserBuiltin,
)),
Actor::SiloUser { silo_user_id, .. } => Some((
silo_user_id.into_untyped_uuid(),
nexus_db_model::IdentityType::SiloUser,
)),
// a role assignment for this Actor is invalid, they have a fixed
// policy.
Actor::Scim { .. } => None,
}
}
}
Expand All @@ -437,6 +459,10 @@ impl std::fmt::Debug for Actor {
.field("silo_user_id", &silo_user_id)
.field("silo_id", &silo_id)
.finish_non_exhaustive(),
Actor::Scim { silo_id } => f
.debug_struct("Actor::Scim")
.field("silo_id", &silo_id)
.finish_non_exhaustive(),
}
}
}
Expand Down
24 changes: 17 additions & 7 deletions nexus/auth/src/authz/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,23 @@ impl oso::PolarClass for AuthenticatedActor {
},
"USER_INTERNAL_API",
)
.add_attribute_getter("is_user", |a: &AuthenticatedActor| {
match a.actor {
authn::Actor::SiloUser { .. } => true,

authn::Actor::UserBuiltin { .. } => true,

authn::Actor::Scim { .. } => false,
}
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me feel bad, but it might just be the name is_user, which, while it technically does divide the options the right way, doesn't feel like it names the meaning of the distinction. Or maybe the problem is that we're going to leave off is_user by mistake and cause a bug that way. If we see this extending to service accounts, are those going to also be non-users? Should they have read perms on their own silo? Maybe this is a question for later, but it illustrates the weirdness of this concept.

.add_attribute_getter("silo", |a: &AuthenticatedActor| {
match a.actor {
authn::Actor::SiloUser { silo_id, .. } => {
Some(super::Silo::new(
super::FLEET,
silo_id,
LookupType::ById(silo_id),
))
}
authn::Actor::SiloUser { silo_id, .. }
| authn::Actor::Scim { silo_id } => Some(super::Silo::new(
super::FLEET,
silo_id,
LookupType::ById(silo_id),
)),

authn::Actor::UserBuiltin { .. } => None,
}
Expand All @@ -149,6 +157,8 @@ impl oso::PolarClass for AuthenticatedActor {
}

authn::Actor::UserBuiltin { .. } => false,

authn::Actor::Scim { .. } => false,
},
)
}
Expand Down
Loading
Loading