diff --git a/editoast/editoast_authz/src/authorizer.rs b/editoast/editoast_authz/src/authorizer.rs index 3949a8682c2..997528b3433 100644 --- a/editoast/editoast_authz/src/authorizer.rs +++ b/editoast/editoast_authz/src/authorizer.rs @@ -1,10 +1,9 @@ -use itertools::Itertools as _; -use std::{collections::HashSet, future::Future, sync::Arc}; +use std::{collections::HashSet, future::Future}; use tracing::debug; use tracing::Level; -use crate::roles::{BuiltinRoleSet, RoleConfig, RoleIdentifier}; +use crate::roles::BuiltinRoleSet; pub type UserIdentity = String; pub type UserName = String; @@ -19,7 +18,6 @@ pub struct UserInfo { pub struct Authorizer { user: UserInfo, user_id: i64, - pub roles_config: Arc>, user_roles: HashSet, #[allow(unused)] // will be used soon storage: S, @@ -45,58 +43,43 @@ pub trait StorageDriver: Clone { fn fetch_subject_roles( &self, subject_id: i64, - roles_config: &RoleConfig, ) -> impl Future, Self::Error>> + Send; fn ensure_subject_roles( &self, subject_id: i64, - roles_config: &RoleConfig, roles: HashSet, ) -> impl Future> + Send; fn remove_subject_roles( &self, subject_id: i64, - roles_config: &RoleConfig, roles: HashSet, ) -> impl Future, Self::Error>> + Send; } impl Authorizer { - #[tracing::instrument(skip_all, fields(%user, roles_config = %roles_config.as_ref()), err)] - pub async fn try_initialize( - user: UserInfo, - roles_config: Arc>, - storage_driver: S, - ) -> Result { + #[tracing::instrument(skip_all, fields(%user), err)] + pub async fn try_initialize(user: UserInfo, storage_driver: S) -> Result { 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?; + let user_roles = storage_driver.fetch_subject_roles(user_id).await?; Ok(Self { user, user_id, - roles_config, user_roles, storage: storage_driver, }) } - pub fn new_superuser(roles_config: Arc>, storage_driver: S) -> Self { - debug_assert!( - roles_config.is_superuser(), - "Authorizer::new_superuser requires a superuser role config" - ); + pub fn new_superuser(storage_driver: S) -> Self { Self { user: UserInfo { identity: "superuser".to_string(), name: "Super User".to_string(), }, user_id: -1, - roles_config, - user_roles: Default::default(), + user_roles: HashSet::from([S::BuiltinRole::superuser()]), storage: storage_driver, } } @@ -106,7 +89,7 @@ impl Authorizer { } pub fn is_superuser(&self) -> bool { - self.roles_config.is_superuser() + self.user_roles.contains(&S::BuiltinRole::superuser()) } /// Returns whether a user with some id exists @@ -132,57 +115,29 @@ impl Authorizer { Ok(required_roles.is_subset(&self.user_roles)) } - #[tracing::instrument(skip_all, fields(user_id, auth_user = %self.user, user_roles = ?self.user_roles), ret(level = Level::DEBUG), err)] - pub async fn infer_application_roles( - &self, - user_id: i64, - ) -> Result, S::Error> { - if self.is_superuser() { - return Ok(self.roles_config.application_roles().cloned().collect_vec()); - } - - let resolved_roles = &self.roles_config.resolved_roles; - let user_roles = self - .storage - .fetch_subject_roles(user_id, &self.roles_config) - .await?; - - let app_roles = resolved_roles - .iter() - .filter(|(_, builtins)| user_roles.is_superset(builtins)) - .map(|(app_role, _)| app_role) - .cloned() - .collect_vec(); - - Ok(app_roles) - } - #[tracing::instrument(skip_all, fields(user_id, auth_user = %self.user, user_roles = ?self.user_roles), ret(level = Level::DEBUG), err)] pub async fn user_builtin_roles( &self, user_id: i64, ) -> Result, S::Error> { - let user_roles = self - .storage - .fetch_subject_roles(user_id, &self.roles_config) - .await?; + let user_roles = self.storage.fetch_subject_roles(user_id).await?; Ok(user_roles.clone()) } - #[tracing::instrument(skip_all, fields(user_id, auth_user = %self.user, ?roles, role_config = ?self.roles_config), ret(level = Level::DEBUG), err)] + #[tracing::instrument(skip_all, fields(user_id, auth_user = %self.user, ?roles), ret(level = Level::DEBUG), err)] pub async fn grant_roles( &mut self, user_id: i64, roles: HashSet, ) -> Result<(), S::Error> { self.storage - .ensure_subject_roles(user_id, &self.roles_config, roles.clone()) + .ensure_subject_roles(user_id, roles.clone()) .await?; self.user_roles.extend(roles); Ok(()) } - #[tracing::instrument(skip_all, fields(user_id, auth_user = %self.user, ?roles, role_config = ?self.roles_config), ret(level = Level::DEBUG), err)] + #[tracing::instrument(skip_all, fields(user_id, auth_user = %self.user, ?roles), ret(level = Level::DEBUG), err)] pub async fn strip_roles( &mut self, user_id: i64, @@ -190,7 +145,7 @@ impl Authorizer { ) -> Result<(), S::Error> { let removed_roles = self .storage - .remove_subject_roles(user_id, &self.roles_config, roles.clone()) + .remove_subject_roles(user_id, roles.clone()) .await?; tracing::debug!(?removed_roles, "removed roles"); self.user_roles.retain(|r| !roles.contains(r)); @@ -203,7 +158,6 @@ impl std::fmt::Debug for Authorizer { f.debug_struct("Authorizer") .field("user", &self.user) .field("user_id", &self.user_id) - .field("roles_config", &self.roles_config) .field("user_roles", &self.user_roles) .finish() } @@ -234,9 +188,8 @@ mod tests { #[tokio::test] async fn superuser() { - let config = RoleConfig::new_superuser(); let storage = MockStorageDriver::default(); - let authorizer = Authorizer::new_superuser(config.into(), storage); + let authorizer = Authorizer::new_superuser(storage); assert!(authorizer.is_superuser()); // Check that the superuser has any role even if not explicitely granted assert_eq!( @@ -249,7 +202,6 @@ mod tests { #[tokio::test] async fn check_roles() { - let config = default_test_config(); let storage = MockStorageDriver::default(); // insert some mocked roles @@ -270,7 +222,6 @@ mod tests { identity: "toto".to_owned(), name: "Sir Toto, the One and Only".to_owned(), }, - config.into(), storage, ) .await @@ -306,7 +257,7 @@ mod tests { .unwrap()); assert!(!authorizer - .check_roles(HashSet::from([TestBuiltinRole::DocEdit,])) + .check_roles(HashSet::from([TestBuiltinRole::DocEdit])) .await .unwrap()); assert!(!authorizer @@ -334,7 +285,6 @@ mod tests { async fn fetch_subject_roles( &self, subject_id: i64, - _roles_config: &RoleConfig, ) -> Result, Self::Error> { let user_roles = self.user_roles.lock().unwrap(); let roles = user_roles.get(&subject_id).cloned().expect("no user"); @@ -344,7 +294,6 @@ mod tests { async fn ensure_subject_roles( &self, subject_id: i64, - _roles_config: &RoleConfig, roles: HashSet, ) -> Result<(), Self::Error> { let mut user_roles = self.user_roles.lock().unwrap(); @@ -355,7 +304,6 @@ mod tests { async fn remove_subject_roles( &self, subject_id: i64, - _roles_config: &RoleConfig, roles: HashSet, ) -> Result, Self::Error> { let mut user_roles = self.user_roles.lock().unwrap(); diff --git a/editoast/editoast_authz/src/builtin_role.rs b/editoast/editoast_authz/src/builtin_role.rs index 6da055a2d6d..b7a425e6e6e 100644 --- a/editoast/editoast_authz/src/builtin_role.rs +++ b/editoast/editoast_authz/src/builtin_role.rs @@ -1,63 +1,54 @@ +use serde::Deserialize; use serde::Serialize; use strum::AsRefStr; -use strum::Display; use strum::EnumString; use utoipa::ToSchema; use crate::roles::BuiltinRoleSet; #[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, EnumString, AsRefStr, Display, ToSchema, + Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumString, AsRefStr, ToSchema, )] -#[strum(serialize_all = "snake_case")] pub enum BuiltinRole { - #[strum(serialize = "operational_studies:write")] + /// A user with this role short-circuits all role and permission checks + /// + /// Alternatively, especially for development, the `EDITOAST_DISABLE_AUTHORIZATION` environment variable can be set + /// when no user identity header is present. (This is the case when editoast is queried directly and + /// not through the gateway.) + Superuser, + OpsWrite, - #[strum(serialize = "operational_studies:read")] OpsRead, - #[strum(serialize = "infra:read")] InfraRead, - #[strum(serialize = "infra:write")] InfraWrite, - #[strum(serialize = "rolling_stock_collection:read")] RollingStockCollectionRead, - #[strum(serialize = "rolling_stock_collection:write")] RollingStockCollectionWrite, - #[strum(serialize = "work_schedule:write")] WorkScheduleWrite, - #[strum(serialize = "work_schedule:read")] WorkScheduleRead, - #[strum(serialize = "map:read")] MapRead, - #[strum(serialize = "stdcm")] Stdcm, - #[strum(serialize = "stdcm:admin")] StdcmAdmin, - #[strum(serialize = "timetable:read")] TimetableRead, - #[strum(serialize = "timetable:write")] TimetableWrite, - #[strum(serialize = "document:read")] DocumentRead, - #[strum(serialize = "document:write")] DocumentWrite, - #[strum(serialize = "subject:read")] SubjectRead, - #[strum(serialize = "subject:write")] SubjectWrite, - #[strum(serialize = "role:read")] RoleRead, - #[strum(serialize = "role:write")] RoleWrite, } -impl BuiltinRoleSet for BuiltinRole {} +impl BuiltinRoleSet for BuiltinRole { + fn superuser() -> Self { + Self::Superuser + } +} diff --git a/editoast/editoast_authz/src/lib.rs b/editoast/editoast_authz/src/lib.rs index 9fc5b9c8312..b01af2cd8e8 100644 --- a/editoast/editoast_authz/src/lib.rs +++ b/editoast/editoast_authz/src/lib.rs @@ -8,7 +8,7 @@ pub use builtin_role::BuiltinRole; pub mod fixtures { use strum::{AsRefStr, EnumString}; - use crate::roles::{self, RoleConfig}; + use crate::roles::BuiltinRoleSet; #[derive(Debug, Clone, PartialEq, Eq, Hash, AsRefStr, EnumString)] #[strum(serialize_all = "snake_case")] @@ -18,24 +18,12 @@ pub mod fixtures { DocDelete, UserAdd, UserBan, + Superuser, } - impl roles::BuiltinRoleSet for TestBuiltinRole {} - - pub fn default_test_config() -> roles::RoleConfig { - const SOURCE: &str = r#" - [roles.doc_reader] - implies = ["doc_read"] - - [roles.doc_provider] - implies = ["doc_delete", "doc_edit", "doc_read"] - - [roles.admin] - implies = ["user_add", "user_ban"] - - [roles.dev] - implies = ["admin", "doc_provider"] - "#; - RoleConfig::load(SOURCE).expect("should parse successfully") + impl BuiltinRoleSet for TestBuiltinRole { + fn superuser() -> Self { + Self::Superuser + } } } diff --git a/editoast/editoast_authz/src/roles.rs b/editoast/editoast_authz/src/roles.rs index b78d9651389..8def298987a 100644 --- a/editoast/editoast_authz/src/roles.rs +++ b/editoast/editoast_authz/src/roles.rs @@ -1,253 +1,12 @@ -use std::{ - collections::{HashMap, HashSet}, - str::FromStr, -}; +use std::str::FromStr; pub trait BuiltinRoleSet: FromStr + AsRef + Sized + Clone + std::hash::Hash + std::cmp::Eq + std::fmt::Debug { + /// Returns the builtin role that short-circuits all role and permission checks. + fn superuser() -> Self; + fn as_str(&self) -> &str { self.as_ref() } } - -pub type RoleIdentifier = String; - -#[derive(Debug)] -pub struct RoleConfig { - #[allow(unused)] - pub(crate) superuser: bool, - pub(crate) resolved_roles: HashMap>, -} - -impl RoleConfig { - pub fn new_superuser() -> Self { - Self { - superuser: true, - resolved_roles: Default::default(), - } - } - - pub fn new(resolved_roles: HashMap>) -> Self { - Self { - superuser: false, - resolved_roles, - } - } - - pub fn is_superuser(&self) -> bool { - self.superuser - } - - pub fn resolve<'r>(&self, roles: impl Iterator) -> Result, &'r str> { - let mut resolved = HashSet::new(); - for role in roles { - if let Some(role) = self.resolved_roles.get(role) { - resolved.extend(role.iter().cloned()); - } else if let Ok(builtin) = B::from_str(role) { - resolved.insert(builtin); - } else { - return Err(role); - } - } - Ok(resolved) - } - - #[tracing::instrument(err)] - pub fn load(source: &str) -> Result { - #[derive(serde::Deserialize)] - struct ApplicationRole { - implies: Vec, - } - - #[derive(serde::Deserialize)] - struct Config { - roles: HashMap, - } - - impl Config { - fn validate(&self) -> Result<(), RoleConfigParsingError> { - for role in self.roles.keys() { - if !(1..=255).contains(&role.len()) { - return Err(RoleConfigParsingError::InvalidRoleIdentifier(role.clone())); - } - } - Ok(()) - } - } - - let raw: Config = toml_edit::de::from_str(source)?; - raw.validate()?; - - let Config { roles } = raw; - let mut config = Self { - superuser: false, - resolved_roles: Default::default(), - }; - - fn resolve_role( - role: &str, - roles: &HashMap, - resolved_roles: &HashMap>, - ) -> Result, RoleConfigParsingError> { - // 1. Is the role already resolved? - if let Some(role) = resolved_roles.get(role) { - return Ok(role.clone()); - } - - // 2. Is it a built-in role? - if let Ok(builtin_role) = B::from_str(role) { - // If so it maps to itself. - return Ok(std::iter::once(builtin_role).collect()); - } - - // 3. Then it has to be an application role. - let ApplicationRole { implies } = roles.get(role).ok_or_else(|| { - RoleConfigParsingError::UndeclaredRoleIdentifier(role.to_string()) - })?; - let mut resolved = HashSet::new(); - for implied_role in implies { - // Recursively resolve implied roles and flatten the built-in roles set. - let implied = resolve_role(implied_role, roles, resolved_roles)?; - resolved.extend(implied); - } - Ok(resolved) - } - - for role in roles.keys() { - if B::from_str(role).is_ok() { - return Err(RoleConfigParsingError::ReservedRoleIdentifier(role.clone())); - } - let resolved = resolve_role(role, &roles, &config.resolved_roles)?; - - // a role cannot be duplicated in the config (TOML hashmap) - config.resolved_roles.insert(role.to_owned(), resolved); - } - Ok(config) - } - - pub fn application_roles(&self) -> impl Iterator { - self.resolved_roles.keys() - } -} - -#[derive(Debug, thiserror::Error)] -pub enum RoleConfigParsingError { - #[error(transparent)] - ParsingError(#[from] toml_edit::de::Error), - #[error("Invalid role identifier: '{0}'")] - InvalidRoleIdentifier(String), - #[error("Role identifier '{0}' is neither a builtin role or a declared application role")] - UndeclaredRoleIdentifier(RoleIdentifier), - #[error("Role identifier '{0}' is a reserved builtin role")] - ReservedRoleIdentifier(RoleIdentifier), -} - -impl std::fmt::Display for RoleConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.superuser { - write!(f, "RoleConfig(superuser)") - } else { - write!(f, "RoleConfig({} roles))", self.resolved_roles.len()) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::fixtures::*; - use pretty_assertions::assert_eq; - - #[test] - fn parsing_ok() { - let source = r#" - [roles.anyone] - implies = ["doc_read"] - - [roles.doc_provider] - implies = ["doc_delete", "doc_edit", "doc_read"] - - [roles.admin] - implies = ["user_add", "user_ban"] - - [roles.dev] - implies = ["admin", "doc_provider"] - "#; - let config = - RoleConfig::::load(source).expect("should parse successfully"); - assert_eq!(config.resolved_roles.len(), 4); - - impl RoleConfig { - fn assert_roles(&self, role: &str, builtin_roles: &[TestBuiltinRole]) { - let builtin_roles: HashSet<_> = builtin_roles.iter().cloned().collect(); - assert_eq!( - self.resolved_roles.get(role).expect("role should exist"), - &builtin_roles - ); - } - } - use TestBuiltinRole::*; - config.assert_roles("anyone", &[DocRead]); - config.assert_roles("doc_provider", &[DocDelete, DocEdit, DocRead]); - config.assert_roles("admin", &[UserAdd, UserBan]); - config.assert_roles("dev", &[UserAdd, UserBan, DocDelete, DocEdit, DocRead]); - } - - #[test] - fn reserved_role_identifier() { - let source = r#" - [roles.doc_read] - implies = ["doc_read"] - "#; - let err = RoleConfig::::load(source).unwrap_err(); - assert!(matches!( - err, - RoleConfigParsingError::ReservedRoleIdentifier(_) - )); - } - - #[test] - fn undeclared_role_identifier() { - let source = r#" - [roles.foo] - implies = ["undeclared"] - "#; - let err = RoleConfig::::load(source).unwrap_err(); - assert!(matches!( - err, - RoleConfigParsingError::UndeclaredRoleIdentifier(_) - )); - } - - #[test] - fn not_toml() { - let source = "not toml"; - let err = RoleConfig::::load(source).unwrap_err(); - assert!(matches!(err, RoleConfigParsingError::ParsingError(_))); - } - - #[test] - fn invalid_application_role_identifier() { - let source = r#" - roles = { "" = { implies = [] } } - "#; - let err = RoleConfig::::load(source).unwrap_err(); - assert!(matches!( - err, - RoleConfigParsingError::InvalidRoleIdentifier(_) - )); - - let source = format!( - r#" - roles = {{ "{} batman!" = {{ implies = [] }} }} - "#, - "na".repeat(256) - ); - let err = RoleConfig::::load(&source).unwrap_err(); - assert!(matches!( - err, - RoleConfigParsingError::InvalidRoleIdentifier(_) - )); - } -} diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index eea1bd75d19..da8bdf401c2 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -7,22 +7,6 @@ info: url: https://www.gnu.org/licenses/lgpl-3.0.html version: 0.1.0 paths: - /authz/list_roles: - get: - tags: - - authz - responses: - '200': - description: List of supported application roles - content: - application/json: - schema: - type: array - items: - type: string - example: - - admin - - dev /authz/roles/me: get: tags: @@ -91,7 +75,7 @@ paths: roles: type: array items: - type: string + $ref: '#/components/schemas/BuiltinRole' required: true responses: '204': @@ -118,7 +102,7 @@ paths: roles: type: array items: - type: string + $ref: '#/components/schemas/BuiltinRole' required: true responses: '204': @@ -3139,6 +3123,7 @@ components: BuiltinRole: type: string enum: + - Superuser - OpsWrite - OpsRead - InfraRead @@ -4097,7 +4082,6 @@ components: - $ref: '#/components/schemas/EditoastInfraApiErrorNotFound' - $ref: '#/components/schemas/EditoastInfraCacheEditoastErrorObjectNotFound' - $ref: '#/components/schemas/EditoastInfraStateErrorFetchError' - - $ref: '#/components/schemas/EditoastInvalidRoleTagInvalid' - $ref: '#/components/schemas/EditoastLayersErrorLayerNotFound' - $ref: '#/components/schemas/EditoastLayersErrorViewNotFound' - $ref: '#/components/schemas/EditoastLinesErrorsLineNotFound' @@ -4384,25 +4368,6 @@ components: type: string enum: - editoast:infra_state:FetchError - EditoastInvalidRoleTagInvalid: - type: object - required: - - type - - status - - message - properties: - context: - type: object - message: - type: string - status: - type: integer - enum: - - 400 - type: - type: string - enum: - - editoast:authz:role:Invalid EditoastLayersErrorLayerNotFound: type: object required: diff --git a/editoast/src/client/mod.rs b/editoast/src/client/mod.rs index 320fd20e137..db15e31dec2 100644 --- a/editoast/src/client/mod.rs +++ b/editoast/src/client/mod.rs @@ -79,8 +79,7 @@ pub enum TimetablesCommands { Export(ExportTimetableArgs), } -#[derive(Args, Debug, Derivative)] -#[derivative(Default)] +#[derive(Args, Debug)] #[command(about, long_about = "Import a train schedule given a JSON file")] pub struct ImportTimetableArgs { /// The timetable id on which attach the trains to @@ -90,8 +89,7 @@ pub struct ImportTimetableArgs { pub path: PathBuf, } -#[derive(Args, Debug, Derivative)] -#[derivative(Default)] +#[derive(Args, Debug)] #[command(about, long_about = "Export the train schedules of a given timetable")] pub struct ExportTimetableArgs { /// The timetable id on which get the train schedules from @@ -134,38 +132,33 @@ pub struct MapLayersConfig { pub max_tiles: u64, } -#[derive(Args, Debug, Derivative)] -#[derivative(Default)] +#[derive(Args, Debug)] #[command(about, long_about = "Launch the server")] pub struct RunserverArgs { #[command(flatten)] pub map_layers_config: MapLayersConfig, - #[derivative(Default(value = "8090"))] #[arg(long, env = "EDITOAST_PORT", default_value_t = 8090)] pub port: u16, - #[derivative(Default(value = r#""0.0.0.0".into()"#))] #[arg(long, env = "EDITOAST_ADDRESS", default_value_t = String::from("0.0.0.0"))] pub address: String, - #[derivative(Default(value = r#""amqp://osrd:password@127.0.0.1:5672/%2f".into()"#))] #[clap(long, env = "OSRD_MQ_URL", default_value_t = String::from("amqp://osrd:password@127.0.0.1:5672/%2f"))] pub mq_url: String, - #[derivative(Default(value = "180"))] #[clap(long, env = "EDITOAST_CORE_TIMEOUT", default_value_t = 180)] pub core_timeout: u64, - #[derivative(Default(value = r#""".into()"#))] #[clap(long, env = "ROOT_PATH", default_value_t = String::new())] pub root_path: String, #[clap(long)] pub workers: Option, - /// An application roles configuration file to apply. If none is provided (default behavior), - /// the superuser role is granted to all users. - #[clap(long, env = "EDITOAST_ROLES_CONFIG")] - pub roles_config: Option, - #[derivative(Default(value = r#""http://127.0.0.1:4242/".into()"#))] + /// If this option is set, any role and permission check will be bypassed. Even if no user is + /// provided by the request headers of if the provided user doesn't have the required privileges. + // TODO: once the whole role system will be deployed, the default value of this option should + // be set to false. It's currently set to true in order to pass integration tests, which otherwise + // only recieve 401 responses. + #[clap(long, env = "EDITOAST_DISABLE_AUTHORIZATION", default_value_t = true)] + pub disable_authorization: bool, #[clap(long, env = "OSRDYNE_API_URL", default_value_t = String::from("http://127.0.0.1:4242/"))] pub osrdyne_api_url: String, /// The timeout to use when performing the healthcheck, in milliseconds - #[derivative(Default(value = "500"))] #[clap(long, env = "EDITOAST_HEALTH_CHECK_TIMEOUT_MS", default_value_t = 500)] pub health_check_timeout_ms: u64, } diff --git a/editoast/src/main.rs b/editoast/src/main.rs index b653f78200d..9de59abc096 100644 --- a/editoast/src/main.rs +++ b/editoast/src/main.rs @@ -65,18 +65,17 @@ pub use redis_utils::{RedisClient, RedisConnection}; use std::error::Error; use std::fs::File; use std::io::{BufReader, IsTerminal}; -use std::path::PathBuf; use std::process::exit; use std::sync::Arc; use std::time::Duration; use std::{env, fs}; use thiserror::Error; -use tracing::{debug, error, info, warn}; +use tracing::{error, info, warn}; use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, Layer as _}; use validator::ValidationErrorsKind; +use views::authorizer_middleware; use views::infra::InfraApiError; use views::search::SearchConfigFinder; -use views::{authorizer_middleware, Roles}; /// The mode editoast is running in /// @@ -344,7 +343,7 @@ pub struct AppState { pub map_layers: Arc, pub map_layers_config: Arc, pub speed_limit_tag_ids: Arc, - pub role_config: Arc, + pub disable_authorization: bool, pub core_client: Arc, pub osrdyne_client: Arc, pub health_check_timeout: Duration, @@ -374,8 +373,9 @@ impl AppState { // Static list of configured speed-limit tag ids let speed_limit_tag_ids = Arc::new(SpeedLimitTagIds::load()); - // Roles configuration - let role_config = Arc::new(load_roles_config(args.roles_config.as_ref())?); + if args.disable_authorization { + warn!("authorization disabled — all role and permission checks are bypassed"); + } // Build Core client let core_client = CoreClient::new_mq(args.mq_url.clone(), "core".into(), args.core_timeout) @@ -396,7 +396,7 @@ impl AppState { map_layers: Arc::new(MapLayers::parse()), map_layers_config: Arc::new(args.map_layers_config.clone()), speed_limit_tag_ids, - role_config, + disable_authorization: args.disable_authorization, health_check_timeout, }) } @@ -462,23 +462,6 @@ async fn runserver( Ok(()) } -fn load_roles_config(path: Option<&PathBuf>) -> Result> { - let Some(path) = path else { - warn!("No roles configuration provided, superuser mode enabled"); - return Ok(Roles::new_superuser()); - }; - info!(file = %path.display(), "Loading roles configuration"); - let content = fs::read_to_string(path).map_err(|e| { - Box::new(CliError::new( - 1, - format!("Cannot read roles configuration file: {e}"), - )) - })?; - let roles_config = Roles::load(&content)?; - debug!("Roles configuration loaded"); - Ok(roles_config) -} - async fn build_redis_pool_and_invalidate_all_cache( redis_config: RedisConfig, infra_id: i64, diff --git a/editoast/src/models/auth.rs b/editoast/src/models/auth.rs index 6fdb41069bf..b212434f28b 100644 --- a/editoast/src/models/auth.rs +++ b/editoast/src/models/auth.rs @@ -5,7 +5,7 @@ use diesel::{dsl, prelude::*}; use diesel_async::{scoped_futures::ScopedFutureExt as _, RunQueryDsl}; use editoast_authz::{ authorizer::{StorageDriver, UserInfo}, - roles::{BuiltinRoleSet, RoleConfig}, + roles::BuiltinRoleSet, }; use editoast_models::DbConnection; @@ -105,11 +105,10 @@ impl StorageDriver for PgAuthDriver { .await } - #[tracing::instrument(skip_all, fields(%subject_id, %roles_config), ret(level = Level::DEBUG), err)] + #[tracing::instrument(skip_all, fields(%subject_id), ret(level = Level::DEBUG), err)] async fn fetch_subject_roles( &self, subject_id: i64, - roles_config: &RoleConfig, ) -> Result, Self::Error> { let roles = authz_role::table .select(authz_role::role) @@ -131,11 +130,10 @@ impl StorageDriver for PgAuthDriver { Ok(roles) } - #[tracing::instrument(skip_all, fields(%subject_id, %roles_config, ?roles), err)] + #[tracing::instrument(skip_all, fields(%subject_id, ?roles), err)] async fn ensure_subject_roles( &self, subject_id: i64, - roles_config: &RoleConfig, roles: HashSet, ) -> Result<(), Self::Error> { dsl::insert_into(authz_role::table) @@ -158,11 +156,10 @@ impl StorageDriver for PgAuthDriver { Ok(()) } - #[tracing::instrument(skip_all, fields(%subject_id, %roles_config, ?roles), ret(level = Level::DEBUG), err)] + #[tracing::instrument(skip_all, fields(%subject_id, ?roles), ret(level = Level::DEBUG), err)] async fn remove_subject_roles( &self, subject_id: i64, - roles_config: &RoleConfig, roles: HashSet, ) -> Result, Self::Error> { let deleted_roles = dsl::delete( @@ -200,11 +197,10 @@ mod tests { async fn assert_roles( driver: &mut PgAuthDriver, uid: i64, - config: &RoleConfig, roles: &[TestBuiltinRole], ) { let fetched_roles = driver - .fetch_subject_roles(uid, config) + .fetch_subject_roles(uid) .await .expect("roles should be fetched successfully"); let expected_roles = roles.iter().cloned().collect(); @@ -215,7 +211,6 @@ mod tests { async fn test_auth_driver() { let pool = DbConnectionPoolV2::for_tests(); let mut driver = PgAuthDriver::::new(pool.get_ok()); - let config = default_test_config(); let uid = driver .ensure_user(&UserInfo { @@ -224,19 +219,19 @@ mod tests { }) .await .expect("toto should be created successfully"); - assert_roles(&mut driver, uid, &config, Default::default()).await; + assert_roles(&mut driver, uid, Default::default()).await; driver - .ensure_subject_roles(uid, &config, HashSet::from([DocRead, DocEdit])) + .ensure_subject_roles(uid, HashSet::from([DocRead, DocEdit])) .await .expect("roles should be updated successfully"); - assert_roles(&mut driver, uid, &config, &[DocRead, DocEdit]).await; + assert_roles(&mut driver, uid, &[DocRead, DocEdit]).await; let deleted = driver - .remove_subject_roles(uid, &config, HashSet::from([DocEdit, UserBan])) + .remove_subject_roles(uid, HashSet::from([DocEdit, UserBan])) .await .expect("roles should be deleted successfully"); assert_eq!(deleted, HashSet::from([DocEdit])); - assert_roles(&mut driver, uid, &config, &[DocRead]).await; + assert_roles(&mut driver, uid, &[DocRead]).await; } } diff --git a/editoast/src/views/authz.rs b/editoast/src/views/authz.rs index be99231afbf..633696b8564 100644 --- a/editoast/src/views/authz.rs +++ b/editoast/src/views/authz.rs @@ -2,8 +2,7 @@ use std::collections::HashSet; use crate::error::Result; use crate::models::auth::{AuthDriverError, PgAuthDriver}; -use crate::AppState; -use axum::extract::{Path, State}; +use axum::extract::Path; use axum::response::Json; use axum::Extension; use editoast_authz::authorizer::Authorizer; @@ -13,10 +12,9 @@ use editoast_derive::EditoastError; use super::{AuthorizationError, AuthorizerExt}; crate::routes! { - "/authz" => { - "/list_roles" => list_roles, - "/roles/me" => list_current_roles, - "/roles/{user_id}" => { + "/authz/roles" => { + "/me" => list_current_roles, + "/{user_id}" => { list_user_roles, grant_roles, strip_roles, @@ -52,18 +50,6 @@ enum NoSuchUserError { NoSuchUser { user_id: i64 }, } -#[utoipa::path( - get, path = "", - tag = "authz", - responses( - (status = 200, description = "List of supported application roles", body = Vec, example = json!(["admin", "dev"])), - ), -)] -async fn list_roles(State(AppState { role_config, .. }): State) -> Json> { - let roles = role_config.application_roles().cloned().collect(); - Json(roles) -} - #[derive(serde::Serialize, utoipa::ToSchema)] struct Roles { builtin: HashSet, @@ -133,15 +119,7 @@ async fn list_user_roles( #[derive(serde::Deserialize, utoipa::ToSchema)] struct RoleListBody { - roles: Vec, -} - -#[derive(Debug, thiserror::Error, EditoastError)] -#[editoast_error(base_id = "authz:role")] -enum InvalidRoleTag { - #[error("Invalid role tag: {0}")] - #[editoast_error(status = 400)] - Invalid(String), + roles: Vec, } #[utoipa::path( @@ -168,12 +146,8 @@ async fn grant_roles( check_user_exists(user_id, &authorizer).await?; - let roles = authorizer - .roles_config - .resolve(roles.iter().map(String::as_str)) - .map_err(|unknown| InvalidRoleTag::Invalid(unknown.to_owned()))?; authorizer - .grant_roles(user_id, roles) + .grant_roles(user_id, HashSet::from_iter(roles)) .await .map_err(AuthzError::from)?; Ok(axum::http::StatusCode::NO_CONTENT) @@ -203,12 +177,8 @@ async fn strip_roles( check_user_exists(user_id, &authorizer).await?; - let roles = authorizer - .roles_config - .resolve(roles.iter().map(String::as_str)) - .map_err(|unknown| InvalidRoleTag::Invalid(unknown.to_owned()))?; authorizer - .strip_roles(user_id, roles) + .strip_roles(user_id, HashSet::from_iter(roles)) .await .map_err(AuthzError::from)?; Ok(axum::http::StatusCode::NO_CONTENT) diff --git a/editoast/src/views/mod.rs b/editoast/src/views/mod.rs index 0c24bdaef5d..42b55a60a50 100644 --- a/editoast/src/views/mod.rs +++ b/editoast/src/views/mod.rs @@ -114,19 +114,17 @@ editoast_common::schemas! { scenario::schemas(), } -pub type Roles = editoast_authz::roles::RoleConfig; pub type AuthorizerExt = axum::extract::Extension>>; async fn make_authorizer( + disable_authorization: bool, headers: &axum::http::HeaderMap, - roles: Arc, db_pool: Arc, ) -> Result>, AuthorizationError> { - if roles.is_superuser() { - return Ok(Authorizer::new_superuser( - roles, - PgAuthDriver::::new(db_pool.get().await?), - )); + if disable_authorization { + return Ok(Authorizer::new_superuser(PgAuthDriver::::new( + db_pool.get().await?, + ))); } let Some(header) = headers.get("x-remote-user") else { return Err(AuthorizationError::Unauthenticated); @@ -141,7 +139,6 @@ async fn make_authorizer( identity: identity.to_owned(), name: name.to_owned(), }, - roles, PgAuthDriver::::new(db_pool.get().await?), ) .await?; @@ -151,14 +148,14 @@ async fn make_authorizer( pub async fn authorizer_middleware( State(AppState { db_pool_v2: db_pool, - role_config, + disable_authorization, .. }): State, mut req: Request, next: Next, ) -> Result { let headers = req.headers(); - let authorizer = make_authorizer(headers, role_config.clone(), db_pool).await?; + let authorizer = make_authorizer(disable_authorization, headers, db_pool).await?; req.extensions_mut().insert(authorizer); Ok(next.run(req).await) } diff --git a/editoast/src/views/projects.rs b/editoast/src/views/projects.rs index bc6104e88c2..11cca5edd0e 100644 --- a/editoast/src/views/projects.rs +++ b/editoast/src/views/projects.rs @@ -358,8 +358,6 @@ async fn patch( #[cfg(test)] pub mod test { - use std::collections::HashMap; - use std::collections::HashSet; use axum::http::StatusCode; use pretty_assertions::assert_eq; @@ -367,11 +365,9 @@ pub mod test { use serde_json::json; use super::*; - use crate::core::CoreClient; use crate::models::fixtures::create_project; use crate::models::prelude::*; use crate::views::test_app::TestAppBuilder; - use crate::views::Roles; #[rstest] async fn project_post() { @@ -398,29 +394,6 @@ pub mod test { assert_eq!(project.name, project_name); } - #[rstest] - async fn project_post_returns_unauthenticated() { - let pool = DbConnectionPoolV2::for_tests(); - let core_client = CoreClient::default(); - let resolved_roles: HashMap> = - HashMap::from([("Unauthenticated".to_string(), HashSet::new())]); - let roles = Roles::new(resolved_roles); - let app = TestAppBuilder::new() - .db_pool(pool) - .core_client(core_client) - .roles(roles) - .build(); - - let request = app.post("/projects").json(&json!({ - "name": "test_project", - "description": "", - "objectives": "", - "funders": "", - })); - - app.fetch(request).assert_status(StatusCode::UNAUTHORIZED); - } - #[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 d56cfa46ce2..8de34714eb0 100644 --- a/editoast/src/views/test_app.rs +++ b/editoast/src/views/test_app.rs @@ -24,7 +24,7 @@ use crate::{ use axum_test::TestRequest; use axum_test::TestServer; -use super::{authorizer_middleware, Roles}; +use super::authorizer_middleware; /// A builder interface for [TestApp] /// @@ -38,7 +38,6 @@ use super::{authorizer_middleware, Roles}; pub(crate) struct TestAppBuilder { db_pool: Option, core_client: Option, - roles: Option, osrdyne_client: Option, db_pool_v1: bool, } @@ -48,7 +47,6 @@ impl TestAppBuilder { Self { db_pool: None, core_client: None, - roles: None, osrdyne_client: None, db_pool_v1: false, } @@ -73,20 +71,12 @@ impl TestAppBuilder { self } - pub fn roles(mut self, roles: Roles) -> Self { - assert!(self.roles.is_none()); - self.roles = Some(roles); - self - } - pub fn default_app() -> TestApp { let pool = DbConnectionPoolV2::for_tests(); let core_client = CoreClient::default(); - let roles = Roles::new_superuser(); TestAppBuilder::new() .db_pool(pool) .core_client(core_client) - .roles(roles) .build() } @@ -130,9 +120,6 @@ impl TestAppBuilder { // Load speed limit tag config let speed_limit_tag_ids = Arc::new(SpeedLimitTagIds::load()); - // Role configuration - let role_config = self.roles.unwrap_or_else(Roles::new_superuser).into(); - // Build Core client let core_client = Arc::new(self.core_client.expect( "No core client provided to TestAppBuilder, use Default or provide a core client", @@ -154,7 +141,7 @@ impl TestAppBuilder { map_layers: MapLayers::parse().into(), map_layers_config: MapLayersConfig::default().into(), speed_limit_tag_ids, - role_config, + disable_authorization: true, health_check_timeout: Duration::from_millis(500), }; diff --git a/front/public/locales/en/errors.json b/front/public/locales/en/errors.json index 80395225c1c..9d9e2b08684 100644 --- a/front/public/locales/en/errors.json +++ b/front/public/locales/en/errors.json @@ -23,10 +23,7 @@ "Driver": "Authentication/authorization internal error, try again or contact support", "NoSuchUser": "Unknown user", "Unauthenticated": "Unauthenticated user", - "Unauthorized": "Access denied", - "role": { - "Invalid": "Invalid role tag" - } + "Unauthorized": "Access denied" }, "auto_fixes": { "ConflictingFixesOnSameObject": "Conflicting fixes for the same object on the same fix-iteration", diff --git a/front/public/locales/fr/errors.json b/front/public/locales/fr/errors.json index 080798a89d8..1be0679afc8 100644 --- a/front/public/locales/fr/errors.json +++ b/front/public/locales/fr/errors.json @@ -23,10 +23,7 @@ "Driver": "Erreur interne d'authentification ou d'autorisation, veuillez réessayer ou contacter le support", "NoSuchUser": "Utilisateur inconnu", "Unauthenticated": "Utilisateur non authentifié", - "Unauthorized": "Accès refusé", - "role": { - "Invalid": "Libellé de rôle inconnu" - } + "Unauthorized": "Accès refusé" }, "auto_fixes": { "ConflictingFixesOnSameObject": "Correctifs conflictuels pour le même objet sur la même itération de correctif", diff --git a/front/src/common/api/generatedEditoastApi.ts b/front/src/common/api/generatedEditoastApi.ts index b3eb5d5a7ad..143f571deb3 100644 --- a/front/src/common/api/generatedEditoastApi.ts +++ b/front/src/common/api/generatedEditoastApi.ts @@ -29,10 +29,6 @@ const injectedRtkApi = api }) .injectEndpoints({ endpoints: (build) => ({ - getAuthzListRoles: build.query({ - query: () => ({ url: `/authz/list_roles` }), - providesTags: ['authz'], - }), getAuthzRolesMe: build.query({ query: () => ({ url: `/authz/roles/me` }), providesTags: ['authz'], @@ -876,9 +872,6 @@ const injectedRtkApi = api overrideExisting: false, }); export { injectedRtkApi as generatedEditoastApi }; -export type GetAuthzListRolesApiResponse = - /** status 200 List of supported application roles */ string[]; -export type GetAuthzListRolesApiArg = void; export type GetAuthzRolesMeApiResponse = /** status 200 List the roles of the issuer of the request */ { builtin: BuiltinRole[]; @@ -896,7 +889,7 @@ export type PostAuthzRolesByUserIdApiArg = { /** A user ID (not to be mistaken for its identity, cf. editoast user model documentation) */ userId: number; body: { - roles: string[]; + roles: BuiltinRole[]; }; }; export type DeleteAuthzRolesByUserIdApiResponse = unknown; @@ -904,7 +897,7 @@ export type DeleteAuthzRolesByUserIdApiArg = { /** A user ID (not to be mistaken for its identity, cf. editoast user model documentation) */ userId: number; body: { - roles: string[]; + roles: BuiltinRole[]; }; }; export type PostDocumentsApiResponse = @@ -1607,6 +1600,7 @@ export type PostWorkSchedulesProjectPathApiArg = { }; }; export type BuiltinRole = + | 'Superuser' | 'OpsWrite' | 'OpsRead' | 'InfraRead'