From 8713f842d0d1e5f6c2b4642de30793eb55892309 Mon Sep 17 00:00:00 2001 From: hamz2a Date: Thu, 26 Dec 2024 11:13:51 +0100 Subject: [PATCH] editoast: add configurable authorization to TestAppBuilder Signed-off-by: hamz2a --- editoast/src/views/mod.rs | 10 ++---- editoast/src/views/projects.rs | 32 +++++++++++++++++ editoast/src/views/test_app.rs | 64 ++++++++++++++++++++++++++++++++-- 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/editoast/src/views/mod.rs b/editoast/src/views/mod.rs index 4d81b52277e..e82231dd5bb 100644 --- a/editoast/src/views/mod.rs +++ b/editoast/src/views/mod.rs @@ -225,15 +225,13 @@ async fn authenticate( async fn authentication_middleware( State(AppState { - db_pool, - disable_authorization, - .. + db_pool, config, .. }): State, mut req: Request, next: Next, ) -> Result { let headers = req.headers(); - let authorizer = authenticate(disable_authorization, headers, db_pool).await?; + let authorizer = authenticate(config.disable_authorization, headers, db_pool).await?; req.extensions_mut().insert(authorizer); Ok(next.run(req).await) } @@ -365,7 +363,6 @@ pub struct ServerConfig { pub health_check_timeout: Duration, pub map_layers_max_zoom: u8, pub disable_authorization: bool, - pub postgres_config: PostgresConfig, pub osrdyne_config: OsrdyneConfig, pub valkey_config: ValkeyConfig, @@ -382,13 +379,11 @@ pub struct Server { #[derive(Clone)] pub struct AppState { pub config: Arc, - pub db_pool: Arc, pub valkey: Arc, pub infra_caches: Arc>, pub map_layers: Arc, pub speed_limit_tag_ids: Arc, - pub disable_authorization: bool, pub core_client: Arc, pub osrdyne_client: Arc, pub health_check_timeout: Duration, @@ -458,7 +453,6 @@ impl AppState { osrdyne_client, map_layers: Arc::new(MapLayers::default()), speed_limit_tag_ids, - disable_authorization: config.disable_authorization, health_check_timeout: config.health_check_timeout, config: Arc::new(config), }) diff --git a/editoast/src/views/projects.rs b/editoast/src/views/projects.rs index 5d658b79c47..601b8041295 100644 --- a/editoast/src/views/projects.rs +++ b/editoast/src/views/projects.rs @@ -359,15 +359,21 @@ async fn patch( #[cfg(test)] pub mod tests { + use std::collections::HashSet; + use axum::http::StatusCode; + use editoast_authz::authorizer::UserInfo; use pretty_assertions::assert_eq; use rstest::rstest; use serde_json::json; use super::*; + use crate::core::mocking::MockingClient; + use crate::core::CoreClient; use crate::models::fixtures::create_project; use crate::models::prelude::*; use crate::views::test_app::TestAppBuilder; + use crate::views::test_app::TestRequestExt; #[rstest] async fn project_post() { @@ -394,6 +400,32 @@ pub mod tests { assert_eq!(project.name, project_name); } + #[rstest] + async fn project_post_should_fail_when_authorization_is_enabled() { + let pool = DbConnectionPoolV2::for_tests(); + let user = UserInfo { + identity: "user_identity".to_string(), + name: "user_name".to_string(), + }; + let app = TestAppBuilder::new() + .db_pool(pool) + .core_client(CoreClient::Mocked(MockingClient::default())) + .enable_authorization(true) + .user(user.clone()) + .roles(HashSet::from([BuiltinRole::OpsRead])) + .build(); + + let request = app.post("/projects").by_user(user).json(&json!({ + "name": "test_project_failed", + "description": "", + "objectives": "", + "funders": "", + })); + + // OpsWrite is required to complete this request successfully. + app.fetch(request).assert_status(StatusCode::FORBIDDEN); + } + #[rstest] async fn project_list() { let app = TestAppBuilder::default_app(); diff --git a/editoast/src/views/test_app.rs b/editoast/src/views/test_app.rs index b91455394d2..eba92675b2c 100644 --- a/editoast/src/views/test_app.rs +++ b/editoast/src/views/test_app.rs @@ -2,17 +2,22 @@ //! test actix server, database connection pool, and different mocking //! components. +use std::collections::HashSet; use std::sync::Arc; use axum::Router; use axum_tracing_opentelemetry::middleware::OtelAxumLayer; use dashmap::DashMap; +use editoast_authz::authorizer::StorageDriver; +use editoast_authz::authorizer::UserInfo; +use editoast_authz::BuiltinRole; use editoast_common::tracing::create_tracing_subscriber; use editoast_common::tracing::Stream; use editoast_common::tracing::Telemetry; use editoast_common::tracing::TracingConfig; use editoast_models::DbConnectionPoolV2; use editoast_osrdyne_client::OsrdyneClient; +use futures::executor::block_on; use futures::future::BoxFuture; use opentelemetry_sdk::export::trace::ExportResult; use opentelemetry_sdk::export::trace::SpanData; @@ -26,6 +31,7 @@ use crate::{ generated_data::speed_limit_tags_config::SpeedLimitTagIds, infra_cache::InfraCache, map::MapLayers, + models::auth::PgAuthDriver, valkey_utils::ValkeyConfig, AppState, ValkeyClient, }; @@ -56,6 +62,9 @@ pub(crate) struct TestAppBuilder { db_pool: Option, core_client: Option, osrdyne_client: Option, + enable_authorization: bool, + user: Option, + roles: HashSet, } impl TestAppBuilder { @@ -64,6 +73,9 @@ impl TestAppBuilder { db_pool: None, core_client: None, osrdyne_client: None, + enable_authorization: false, + user: None, + roles: HashSet::new(), } } @@ -85,6 +97,23 @@ impl TestAppBuilder { self } + pub fn enable_authorization(mut self, enable_authorization: bool) -> Self { + self.enable_authorization = enable_authorization; + self + } + + pub fn user(mut self, user: UserInfo) -> Self { + assert!(self.user.is_none()); + self.user = Some(user); + self + } + + pub fn roles(mut self, roles: HashSet) -> Self { + assert!(self.roles.is_empty()); + self.roles = roles; + self + } + pub fn default_app() -> TestApp { let pool = DbConnectionPoolV2::for_tests(); let core_client = CoreClient::Mocked(MockingClient::default()); @@ -100,7 +129,7 @@ impl TestAppBuilder { port: 0, address: String::default(), health_check_timeout: chrono::Duration::milliseconds(500), - disable_authorization: true, + disable_authorization: !self.enable_authorization, map_layers_max_zoom: 18, postgres_config: PostgresConfig { database_url: Url::parse("postgres://osrd:password@localhost:5432/osrd").unwrap(), @@ -168,7 +197,6 @@ impl TestAppBuilder { infra_caches, map_layers: Arc::new(MapLayers::default()), speed_limit_tag_ids, - disable_authorization: true, health_check_timeout: config.health_check_timeout, config: Arc::new(config), }; @@ -187,6 +215,23 @@ impl TestAppBuilder { // Run server let server = TestServer::new(router).expect("test server should build properly"); + // Setup user and roles + let driver = PgAuthDriver::::new(db_pool_v2.clone()); + if let Some(ref user) = self.user { + let uid = block_on(async { + driver + .ensure_user(user) + .await + .expect("User should be created successfully") + }); + block_on(async { + driver + .ensure_subject_roles(uid, self.roles) + .await + .expect("Roles should be updated successfully") + }); + }; + TestApp { server, db_pool: db_pool_v2, @@ -229,20 +274,35 @@ impl TestApp { pub fn get(&self, path: &str) -> TestRequest { self.server.get(&trim_path(path)) } + pub fn post(&self, path: &str) -> TestRequest { self.server.post(&trim_path(path)) } + pub fn put(&self, path: &str) -> TestRequest { self.server.put(&trim_path(path)) } + pub fn patch(&self, path: &str) -> TestRequest { self.server.patch(&trim_path(path)) } + pub fn delete(&self, path: &str) -> TestRequest { self.server.delete(&trim_path(path)) } } +pub trait TestRequestExt { + fn by_user(self, user: UserInfo) -> Self; +} + +impl TestRequestExt for TestRequest { + fn by_user(self, user: UserInfo) -> Self { + self.add_header("x-remote-user-identity", user.identity) + .add_header("x-remote-user-name", user.name) + } +} + // For technical reasons, we had a hard time trying to configure the normalizing layer // in the test server. Since we have control over the paths configured in our unit tests, // doing this manually is probably a good enough solution for now.