Skip to content

Commit

Permalink
editoast: implement Authorizer creation middleware
Browse files Browse the repository at this point in the history
Co-Authored-By: Younes Khoudli <[email protected]>
  • Loading branch information
leovalais and Khoyo committed Aug 2, 2024
1 parent 9d45d8c commit 0ed793d
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 53 deletions.
55 changes: 20 additions & 35 deletions editoast/editoast_authz/src/authorizer.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{collections::HashSet, future::Future};
use std::{collections::HashSet, future::Future, sync::Arc};

use tracing::debug;

Expand All @@ -13,11 +13,12 @@ pub struct UserInfo {
pub name: UserName,
}

pub struct Authorizer<'config, S: StorageDriver> {
pub struct Authorizer<S: StorageDriver> {
user: UserInfo,
user_id: i64,
roles_config: &'config RoleConfig<S::BuiltinRole>,
user_roles: Option<HashSet<S::BuiltinRole>>,
roles_config: Arc<RoleConfig<S::BuiltinRole>>,
user_roles: HashSet<S::BuiltinRole>,
#[allow(unused)] // will be used soon
storage: S,
}

Expand Down Expand Up @@ -49,27 +50,28 @@ pub trait StorageDriver {
) -> impl Future<Output = Result<HashSet<Self::BuiltinRole>, Self::Error>> + Send;
}

impl<'config, S: StorageDriver> Authorizer<'config, S> {
impl<S: StorageDriver> Authorizer<S> {
#[tracing::instrument(skip_all, fields(%user, roles_config = %roles_config.as_ref()), err)]
pub async fn try_initialize(
user: UserInfo,
roles_config: &'config RoleConfig<S::BuiltinRole>,
roles_config: Arc<RoleConfig<S::BuiltinRole>>,
storage_driver: S,
) -> Result<Self, S::Error> {
let user_id = storage_driver.ensure_user(&user).await?;
debug!(%user, %user_id, "user authenticated");
let user_roles = storage_driver
.fetch_subject_roles(user_id, roles_config.as_ref())
.await?;
Ok(Self {
user,
user_id,
roles_config,
user_roles: None,
user_roles,
storage: storage_driver,
})
}

pub fn new_superuser(
roles_config: &'config RoleConfig<S::BuiltinRole>,
storage_driver: S,
) -> Self {
pub fn new_superuser(roles_config: Arc<RoleConfig<S::BuiltinRole>>, storage_driver: S) -> Self {
debug_assert!(
roles_config.is_superuser(),
"Authorizer::new_superuser requires a superuser role config"
Expand All @@ -81,7 +83,7 @@ impl<'config, S: StorageDriver> Authorizer<'config, S> {
},
user_id: -1,
roles_config,
user_roles: None,
user_roles: Default::default(),
storage: storage_driver,
}
}
Expand All @@ -93,35 +95,19 @@ impl<'config, S: StorageDriver> Authorizer<'config, S> {
/// Check that the user has all the required builting roles
#[tracing::instrument(skip_all, fields(user = %self.user, user_roles = ?self.user_roles, ?required_roles), ret, err)]
pub async fn check_roles(
&mut self,
&self,
required_roles: HashSet<S::BuiltinRole>,
) -> Result<bool, S::Error> {
if self.is_superuser() {
tracing::debug!("role checking skipped for superuser");
return Ok(true);
}

let user_roles = self.get_user_roles().await?;
Ok(required_roles.is_subset(user_roles))
}

async fn get_user_roles(&mut self) -> Result<&HashSet<S::BuiltinRole>, S::Error> {
match self.user_roles {
None => {
tracing::info!("fetching user roles");
let user_roles = self
.storage
.fetch_subject_roles(self.user_id, self.roles_config)
.await?;
tracing::debug!(roles = ?user_roles, "caching user roles");
Ok(self.user_roles.insert(user_roles))
}
Some(ref user_roles) => Ok(user_roles),
}
Ok(required_roles.is_subset(&self.user_roles))
}
}

impl<'config, S: StorageDriver> std::fmt::Debug for Authorizer<'config, S> {
impl<S: StorageDriver> std::fmt::Debug for Authorizer<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Authorizer")
.field("user", &self.user)
Expand All @@ -144,7 +130,6 @@ mod tests {
use crate::fixtures::*;
use pretty_assertions::assert_eq;
use std::{
borrow::BorrowMut,
collections::HashMap,
convert::Infallible,
sync::{Arc, Mutex},
Expand All @@ -160,7 +145,7 @@ mod tests {
async fn superuser() {
let config = RoleConfig::new_superuser();
let storage = MockStorageDriver::default();
let mut authorizer = Authorizer::new_superuser(&config, storage);
let authorizer = Authorizer::new_superuser(config.into(), storage);
assert!(authorizer.is_superuser());
// Check that the superuser has any role even if not explicitely granted
assert_eq!(
Expand All @@ -175,12 +160,12 @@ mod tests {
async fn check_roles() {
let config = default_test_config();
let storage = MockStorageDriver::default();
let mut authorizer = Authorizer::try_initialize(
let authorizer = Authorizer::try_initialize(
UserInfo {
identity: "toto".to_owned(),
name: "Sir Toto, the One and Only".to_owned(),
},
&config,
config.into(),
storage,
)
.await
Expand Down
6 changes: 5 additions & 1 deletion editoast/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _
use validator::ValidationErrorsKind;
use views::infra::InfraApiError;
use views::search::SearchConfigFinder;
use views::Roles;
use views::{authorizer_middleware, Roles};

/// The mode editoast is running in
///
Expand Down Expand Up @@ -430,6 +430,10 @@ async fn runserver(
// Configure the axum router
let router: Router<()> = axum::Router::<AppState>::new()
.merge(views::router())
.route_layer(axum::middleware::from_fn_with_state(
app_state.clone(),
authorizer_middleware,
))
.layer(OtelInResponseLayer)
.layer(OtelAxumLayer::default())
.layer(request_payload_limit)
Expand Down
5 changes: 1 addition & 4 deletions editoast/src/tests/test_role_config.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
name = "Test role configuration"
version = "0.1"

[application_roles.operational_studies]
[roles.operational_studies]
implies = ["operational_studies_write"]
27 changes: 23 additions & 4 deletions editoast/src/views/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ use std::ops::DerefMut as _;
use std::sync::Arc;
use std::time::Duration;

use axum::extract::Request;
use axum::middleware::Next;
use axum::response::Response;
use editoast_authz::authorizer::Authorizer;
use editoast_authz::authorizer::UserInfo;
use editoast_authz::BuiltinRole;
Expand Down Expand Up @@ -118,13 +121,13 @@ editoast_common::schemas! {
}

pub type Roles = editoast_authz::roles::RoleConfig<BuiltinRole>;
pub type AuthorizerExt = axum::extract::Extension<Arc<Authorizer<PgAuthDriver<BuiltinRole>>>>;

// This function will become a middleware once we switch to axum
async fn make_authorizer<'config>(
async fn make_authorizer(
headers: &axum::http::HeaderMap,
roles: &'config Roles,
roles: Arc<Roles>,
db_pool: Arc<DbConnectionPoolV2>,
) -> Result<Authorizer<'config, PgAuthDriver<BuiltinRole>>, AuthorizationError> {
) -> Result<Authorizer<PgAuthDriver<BuiltinRole>>, AuthorizationError> {
if roles.is_superuser() {
return Ok(Authorizer::new_superuser(
roles,
Expand All @@ -151,6 +154,22 @@ async fn make_authorizer<'config>(
Ok(authorizer)
}

pub async fn authorizer_middleware(
State(AppState {
db_pool_v2: db_pool,
role_config,
..
}): State<AppState>,
mut req: Request,
next: Next,
) -> Result<Response> {
let headers = req.headers();
let authorizer = make_authorizer(headers, role_config.clone(), db_pool).await?;
let authorizer = Arc::new(authorizer);
req.extensions_mut().insert(authorizer);
Ok(next.run(req).await)
}

#[derive(Debug, Error, EditoastError)]
#[editoast_error(base_id = "authorization")]
pub enum AuthorizationError {
Expand Down
12 changes: 4 additions & 8 deletions editoast/src/views/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use axum::extract::Path;
use axum::extract::Query;
use axum::extract::State;
use axum::response::IntoResponse;
use axum::Extension;
use chrono::Utc;
use derivative::Derivative;
use editoast_authz::BuiltinRole;
Expand All @@ -20,6 +21,7 @@ use super::operational_studies::OperationalStudiesOrderingParam;
use super::pagination::PaginatedList;
use super::pagination::PaginationStats;
use super::study;
use super::AuthorizerExt;
use crate::error::Result;
use crate::modelsv2::projects::Tags;
use crate::modelsv2::Changeset;
Expand All @@ -28,7 +30,6 @@ use crate::modelsv2::Document;
use crate::modelsv2::Model;
use crate::modelsv2::Project;
use crate::modelsv2::Retrieve;
use crate::views::make_authorizer;
use crate::views::pagination::PaginationQueryParam;
use crate::views::AuthorizationError;
use crate::AppState;
Expand Down Expand Up @@ -148,15 +149,10 @@ impl ProjectWithStudyCount {
)
)]
async fn create(
headers: axum::http::HeaderMap,
State(AppState {
db_pool_v2: db_pool,
role_config,
..
}): State<AppState>,
State(db_pool): State<DbConnectionPoolV2>,
Extension(authorizer): AuthorizerExt,
Json(project_create_form): Json<ProjectCreateForm>,
) -> Result<Json<ProjectWithStudyCount>> {
let mut authorizer = make_authorizer(&headers, role_config.as_ref(), db_pool.clone()).await?;
let authorized = authorizer
.check_roles([BuiltinRole::OpsWrite].into())
.await
Expand Down
6 changes: 5 additions & 1 deletion editoast/src/views/test_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::{
use axum_test::TestRequest;
use axum_test::TestServer;

use super::Roles;
use super::{authorizer_middleware, Roles};

/// A builder interface for [TestApp]
///
Expand Down Expand Up @@ -144,6 +144,10 @@ impl TestAppBuilder {
// Configure the axum router
let router: Router<()> = axum::Router::<AppState>::new()
.merge(crate::views::router())
.route_layer(axum::middleware::from_fn_with_state(
app_state.clone(),
authorizer_middleware,
))
.layer(OtelInResponseLayer)
.layer(OtelAxumLayer::default())
.layer(TraceLayer::new_for_http())
Expand Down

0 comments on commit 0ed793d

Please sign in to comment.