-
Notifications
You must be signed in to change notification settings - Fork 59
[SCIM 3/4]: SCIM client token CRUD + Bearer auth #9180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
dea9bf9
56d5288
6e67ae9
9bbb011
ed37a89
88c03f6
2b03a76
880fb63
76b452e
5cebf49
3b3b891
456cdc0
3b52803
8995f83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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. | ||
pub fn silo_or_builtin( | ||
&self, | ||
) -> Result<Option<authz::Silo>, omicron_common::api::external::Error> { | ||
|
@@ -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), | ||
)), | ||
}) | ||
} | ||
|
||
|
@@ -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( | ||
|
@@ -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 | ||
|
@@ -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] | ||
|
@@ -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)); | ||
} | ||
} | ||
|
||
|
@@ -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? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
} | ||
} | ||
} | ||
|
@@ -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(), | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
} | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes me feel bad, but it might just be the name |
||
.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, | ||
} | ||
|
@@ -149,6 +157,8 @@ impl oso::PolarClass for AuthenticatedActor { | |
} | ||
|
||
authn::Actor::UserBuiltin { .. } => false, | ||
|
||
authn::Actor::Scim { .. } => false, | ||
}, | ||
) | ||
} | ||
|
There was a problem hiding this comment.
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.