-
Notifications
You must be signed in to change notification settings - Fork 75
editoast: fix a couple issues in new authentication middlewares #16980
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: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| use std::convert::Infallible; | ||
|
|
||
| use authz::Authorizer; | ||
| use authz::Role; | ||
| use authz::StorageDriver as _; | ||
|
|
@@ -95,30 +97,29 @@ pub(in crate::views) async fn authentication_validation_middleware( | |
| mut req: Request, | ||
| next: Next, | ||
| ) -> Result<Response> { | ||
| let is_impersonation = matches!( | ||
| authn, | ||
| crate::authentication::Authentication::Impersonating { .. } | ||
| ); | ||
|
|
||
| fn origin_user( | ||
| user: Option<editoast_models::User>, | ||
| fn warn_mismatched_name( | ||
| user: &Option<editoast_models::User>, | ||
| identity: &str, | ||
| header_name: &str, | ||
| ) -> Option<(editoast_models::User, ::authz::v2::Protected<Vec<Role>>)> { | ||
| user.inspect(|user| { | ||
| if user.name != header_name { | ||
| tracing::warn!( | ||
| identity, | ||
| header = header_name, | ||
| stored = user.name, | ||
| "provided name for identity differ from stored", | ||
| ); | ||
| } | ||
| }) | ||
| .map(|user| { | ||
| let authz_user = ::authz::Subject::user(user.id); | ||
| (user, ::authz::v2::subject_roles(authz_user)) | ||
| }) | ||
| ) { | ||
| if user.as_ref().is_some_and(|u| u.name != header_name) { | ||
| tracing::warn!( | ||
| identity, | ||
| header = header_name, | ||
| stored = user.as_ref().map(|u| u.name.clone()), | ||
| "provided name for identity differ from stored", | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| fn zip_roles( | ||
| user: editoast_models::User, | ||
| ) -> ( | ||
| Option<editoast_models::User>, | ||
| ::authz::v2::Protected<Vec<Role>>, | ||
| ) { | ||
| let subject = ::authz::Subject::user(user.id); | ||
| (Some(user), ::authz::v2::subject_roles(subject)) | ||
| } | ||
|
|
||
| async fn register_origin_user( | ||
|
|
@@ -139,7 +140,7 @@ pub(in crate::views) async fn authentication_validation_middleware( | |
| Ok(user) => user, | ||
| Err(AddIdentitiesError::DuplicateIdentity(_)) => { | ||
| unreachable!( | ||
| "the current function is only called when the user don't exists, and the `.register()`\n\ | ||
| "the current function is only called when the user doesn't exists, and the `.register()`\n\ | ||
| operation above only creates the user with one unique identity" | ||
| ); | ||
| } | ||
|
|
@@ -149,54 +150,83 @@ pub(in crate::views) async fn authentication_validation_middleware( | |
| Ok((Some(user), ::authz::v2::subject_roles(authz_user))) | ||
|
Contributor
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. If we only call that method on nonexistent user they should not have roles yet aswell ? Maybe we can either directly return an empty roles list keep retrieving the roles but in order to check that the retrieved roles list is indeed empty and panic if it is not ? |
||
| } | ||
|
|
||
| let (user, roles_prot) = if let Some(req_origin) = authn.origin() { | ||
| let conn = db_pool.get().await?; | ||
| conn.transaction(async |conn| { | ||
| let origin = match &authn { | ||
| crate::authentication::Authentication::Authenticated { identity, name } => { | ||
| let user = | ||
| editoast_models::User::retrieve_by_identity(identity, conn.clone()).await?; | ||
| origin_user(user, identity, name) | ||
| async fn check_impersonation_privilege( | ||
| openfga: &fga::Client, | ||
| user: &editoast_models::User, | ||
| ) -> Result<()> { | ||
| let Ok(roles) = ::authz::v2::subject_roles(::authz::Subject::user(user.id)) | ||
| .access_authorized::<Infallible>(openfga) | ||
| .access() | ||
| .await?; | ||
| if roles.contains(&Role::Admin) { | ||
| Ok(()) | ||
| } else { | ||
| Err(AuthorizationError::ForbiddenImpersonation.into()) | ||
| } | ||
| } | ||
|
|
||
| let (user, roles_prot) = match &authn { | ||
| crate::authentication::Authentication::Authenticated { identity, name } | ||
| | crate::authentication::Authentication::Skip { | ||
| identity: Some(identity), | ||
| name: Some(name), | ||
| } => { | ||
| let conn = db_pool.get().await?; | ||
| conn.transaction(async |conn| { | ||
| let user = | ||
| editoast_models::User::retrieve_by_identity(identity, conn.clone()).await?; | ||
| warn_mismatched_name(&user, identity, name); | ||
|
Contributor
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. nit: the Maybe rename it to |
||
| Ok::<_, crate::error::InternalError>(if let Some(user) = user { | ||
| zip_roles(user) | ||
| } else { | ||
| register_origin_user(conn.clone(), (identity, name)).await? | ||
| }) | ||
| }) | ||
| .await? | ||
| } | ||
| crate::authentication::Authentication::Impersonating { | ||
| impersonator_identity, | ||
| impersonator_name, | ||
| impersonated_identity, | ||
| } => { | ||
| let conn = db_pool.get().await?; | ||
| conn.transaction(async |conn| { | ||
| let (impersonator, impersonated) = tokio::try_join!( | ||
| // The batching API is annoying, that's the best I can do concisely for now. We should | ||
| // work on the DB user management that got worse since we added multiple identities support. | ||
| editoast_models::User::retrieve_by_identity( | ||
| impersonator_identity, | ||
| conn.clone() | ||
| ), | ||
| editoast_models::User::retrieve_by_identity( | ||
| impersonated_identity, | ||
| conn.clone() | ||
| ) | ||
| )?; | ||
| warn_mismatched_name(&impersonator, impersonator_identity, impersonator_name); | ||
| if let Some(impersonator) = impersonator { | ||
| check_impersonation_privilege(regulator.openfga(), &impersonator).await?; | ||
| } else { | ||
| register_origin_user(conn.clone(), (impersonator_identity, impersonator_name)) | ||
| .await?; | ||
| } | ||
|
Comment on lines
+207
to
212
Contributor
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. If the impersonator does not exist yet we skip the impersonation check and create the impersonator instead ? That feels wrong If I'm not mistaken it means that trying to impersonate with a user which does not exist always succeeds even though it is supposed to be reserved to admins |
||
| crate::authentication::Authentication::Impersonating { | ||
| impersonator_identity, | ||
| impersonator_name, | ||
| impersonated_identity, | ||
| } => { | ||
| let (impersonator, impersonated) = tokio::try_join!( | ||
| // The batching API is annoying, that's the best I can do concisely for now. We should | ||
| // work on the DB user management that got worse since we added multiple identities support. | ||
| editoast_models::User::retrieve_by_identity( | ||
| impersonator_identity, | ||
| conn.clone() | ||
| ), | ||
| editoast_models::User::retrieve_by_identity( | ||
| impersonated_identity, | ||
| conn.clone() | ||
| ) | ||
| )?; | ||
| if impersonated.is_none() { | ||
| return Err::<_, crate::error::InternalError>( | ||
| AuthorizationError::ImpersonatedUserNotFound { | ||
| identity: impersonated_identity.to_owned(), | ||
| } | ||
| .into(), | ||
| ); | ||
| } | ||
| origin_user(impersonator, impersonator_identity, impersonator_name) | ||
| if let Some(impersonated) = impersonated { | ||
| Ok(zip_roles(impersonated)) | ||
| } else { | ||
| Err::<_, crate::error::InternalError>( | ||
| AuthorizationError::ImpersonatedUserNotFound { | ||
| identity: impersonated_identity.to_owned(), | ||
| } | ||
| .into(), | ||
| ) | ||
| } | ||
| crate::authentication::Authentication::Unauthenticated | ||
| | crate::authentication::Authentication::Skip { .. } => None, | ||
| }; | ||
|
|
||
| Ok(match origin { | ||
| Some((user, roles_prot)) => (Some(user), roles_prot), | ||
| None => register_origin_user(conn, req_origin).await?, | ||
| }) | ||
| }) | ||
| .await? | ||
| } else { | ||
| (None, ::authz::v2::Protected::default()) | ||
| .await? | ||
| } | ||
| crate::authentication::Authentication::Unauthenticated | ||
| | crate::authentication::Authentication::Skip { .. } => { | ||
| (None, ::authz::v2::Protected::default()) | ||
| } | ||
| }; | ||
|
|
||
| // A failed OpenFGA request does not invalidate the creation of a new user | ||
|
|
@@ -206,14 +236,6 @@ pub(in crate::views) async fn authentication_validation_middleware( | |
| .await | ||
| .map_err(AuthorizationError::from)?; | ||
|
|
||
| if is_impersonation { | ||
| if !roles.contains(&Role::Admin) { | ||
| return Err(AuthorizationError::ForbiddenImpersonation.into()); | ||
| } else { | ||
| tracing::info!("impersonation enabled"); | ||
| } | ||
| } | ||
|
|
||
| let span = tracing::Span::current(); | ||
| if let Some(user) = &user { | ||
| span.record("user.id", user.id); | ||
|
|
||
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.
nit: maybe also test that non-admin users cannot impersonate