-
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?
Conversation
Implement the CRUD routines for the tokens that will be used to authenticate SCIM clients for SamlScim Silos. Also implement the Bearer based authentication for SCIM clients. Fill in the skeleton of CrdbScimProviderStore, which when implemented will complete the SCIM implementation in Nexus.
Instead of silo Also grant silo admin role to USER_TEST_PRIVILEGED.id() instead of OpContext::for_tests in tests.
"create_child" if "external-authenticator" on "parent_fleet"; | ||
|
||
# external scim has to be able to read SCIM tokens | ||
"list_children" if "external-scim" on "parent_fleet"; |
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.
Why is this needed?
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.
Yeah, I can't find a spot where the external-scim
opctx needs to do this. Is it intended for the unimplemented list users operation etc?
|
||
/// Authenticate a SCIM client based on a bearer token, and return a SCIM | ||
/// provider store implementation that is scoped to a Silo. | ||
pub(crate) async fn scim_idp_get_provider( |
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.
I think it would be good for this name to make clearer that it handles token authentication. scim_idp_get_provider
sounds like a lookup. It might even make sense to split it into two pieces, one that is the authn and one that gets the provider based on the token. But that's silly if they're always called together, and you don't really want to return the token back to the call site either. scim_token_auth_and_get_provider
is a bit much I guess.
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.
This might be more relevant in the next PR, when you implement the actual SCIM operations, but it could make sense to mirror op_context_for_external_api
and wrap up the authentication and the creation of the op context into one operation. Right now it looks like the unimplemented SCIM things don't take and OpContext but they will need to in order to do DB operations. Or at least, that's how I think it should work: op context created at top level at authn time and then passed into the provider methods.
Lines 335 to 359 in ef318b3
/// Authenticates an incoming request to the external API and produces a new | |
/// operation context for it | |
pub(crate) async fn op_context_for_external_api( | |
rqctx: &dropshot::RequestContext<ApiContext>, | |
) -> Result<OpContext, dropshot::HttpError> { | |
let apictx = rqctx.context(); | |
OpContext::new_async( | |
&rqctx.log, | |
async { | |
let authn = Arc::new( | |
apictx.context.external_authn.authn_request(rqctx).await?, | |
); | |
let datastore = Arc::clone(apictx.context.nexus.datastore()); | |
let authz = authz::Context::new( | |
Arc::clone(&authn), | |
Arc::clone(&apictx.context.authz), | |
datastore, | |
); | |
Ok((authn, authz)) | |
}, | |
|metadata| OpContext::load_request_metadata(rqctx, metadata), | |
OpKind::ExternalApiRequest, | |
) | |
.await | |
} |
I had a good conversation with the Codex CLI about the choice to effectively bypass the existing authn schemes setup here, treating these calls as "unauthed" from the point of view of the main authn system. The robot was pretty persuasive but I think the choice is worth pointing out and documenting in comments, probably on the https://gist.github.com/david-crespo/14d25cb770f9e10195f6d0ea45286874 |
opctx: &OpContext, | ||
authz_silo: &authz::Silo, | ||
) -> ListResultVec<ScimClientBearerToken> { | ||
opctx.authorize(authz::Action::ListChildren, authz_silo).await?; |
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.
This seems to me too lenient of a check. Any silo viewer has ListChildren
on the silo. Maybe that's fine considering the list of tokens doesn't actually tell you anything. But it's something to think about.
omicron/nexus/db-queries/tests/output/authz-roles.out
Lines 183 to 195 in ef318b3
resource: Silo "silo1" | |
USER Q R LC RP M MP CC D | |
fleet-admin ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ | |
fleet-collaborator ✘ ✔ ✘ ✔ ✔ ✔ ✘ ✔ | |
fleet-viewer ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ | |
silo1-admin ✘ ✔ ✔ ✔ ✔ ✔ ✔ ✔ | |
silo1-collaborator ✘ ✔ ✔ ✔ ✘ ✘ ✔ ✘ | |
silo1-viewer ✘ ✔ ✔ ✔ ✘ ✘ ✘ ✘ | |
silo1-proj1-admin ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ | |
silo1-proj1-collaborator ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ | |
silo1-proj1-viewer ✘ ✔ ✘ ✔ ✘ ✘ ✘ ✘ | |
unauthenticated ! ! ! ! ! ! ! ! |
The only way I know of to make this more restrictive is to create a "synthetic" authz resource representing the list of scim tokens for a given silo and then do the authz checks against that. Here's an example of a synthetic authz list resource with its own permissions distinct from silo ListChildren
:
omicron/nexus/auth/src/authz/api_resources.rs
Lines 672 to 695 in ef318b3
/// Synthetic resource describing the list of Identity Providers associated with | |
/// a Silo | |
#[derive(Clone, Debug, Eq, PartialEq)] | |
pub struct SiloIdentityProviderList(Silo); | |
impl SiloIdentityProviderList { | |
pub fn new(silo: Silo) -> SiloIdentityProviderList { | |
SiloIdentityProviderList(silo) | |
} | |
pub fn silo(&self) -> &Silo { | |
&self.0 | |
} | |
} | |
impl oso::PolarClass for SiloIdentityProviderList { | |
fn get_polar_class_builder() -> oso::ClassBuilder<Self> { | |
oso::Class::builder() | |
.with_equality_check() | |
.add_attribute_getter("silo", |list: &SiloIdentityProviderList| { | |
list.0.clone() | |
}) | |
} | |
} |
) -> ListResultVec<views::ScimClientBearerToken> { | ||
let (.., _authz_silo, _) = | ||
let (.., authz_silo, _) = | ||
silo_lookup.fetch_for(authz::Action::ListChildren).await?; |
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.
I don't think you need to specify ListChildren
here, you can just do .fetch()
and let the datastore method handle the permission relevant on the token list.
id: Uuid::new_v4(), | ||
time_created: Utc::now(), | ||
time_deleted: None, | ||
// TODO: allow setting an expiry? have a silo default? |
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.
Do you mind filing a follow up ticket for this and noting it in the comment
use nexus_db_schema::schema::scim_client_bearer_token::dsl; | ||
let maybe_token = dsl::scim_client_bearer_token | ||
.filter(dsl::bearer_token.eq(bearer_token)) | ||
.filter(dsl::time_deleted.is_null()) |
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.
do we need filters for where we are not past the expired time here and other places?
let token = match header.to_str() { | ||
Ok(v) => v, | ||
Err(_) => { | ||
return Err(Error::Unauthenticated { | ||
internal_message: "Invalid bearer token".to_string(), | ||
}); | ||
} | ||
}; |
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.
let token = match header.to_str() { | |
Ok(v) => v, | |
Err(_) => { | |
return Err(Error::Unauthenticated { | |
internal_message: "Invalid bearer token".to_string(), | |
}); | |
} | |
}; | |
let Ok(token) = header.to_str() else { | |
return Err(Error::Unauthenticated { | |
internal_message: "Invalid bearer token".to_string(), | |
}); | |
}; | |
|
||
const BEARER: &str = "Bearer "; | ||
|
||
if !token.starts_with(BEARER) { |
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.
I don't remember where we landed on this but assuming this logic is correct we are going to require operators to use Bearer xxx-xxx-xxx-xxx
on the IdP side rather than just a raw header value?
Also the logic could simply become:
let Some(token) = token.strip_prefix(BEARER) else {
return Err(Error::Unauthenticated {
internal_message: "Invalid bearer token".to_string(),
});
};
Then that could be passed to scim_idp_lookup_token_by_bearer()
directly.
|
||
assert!(tokens.is_empty()); | ||
|
||
// Fleet admins can create SCIM client tokens |
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.
Just going off of the comment here, but can we add a test that a non fleet admin cannot create a SCIM client token?
Implement the CRUD routines for the tokens that will be used to authenticate SCIM clients for SamlScim Silos. Also implement the Bearer based authentication for SCIM clients.
Fill in the skeleton of CrdbScimProviderStore, which when implemented will complete the SCIM implementation in Nexus.