Skip to content
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

Remove application roles #9157

Merged
merged 4 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 15 additions & 67 deletions editoast/editoast_authz/src/authorizer.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,7 +18,6 @@ pub struct UserInfo {
pub struct Authorizer<S: StorageDriver> {
user: UserInfo,
user_id: i64,
pub roles_config: Arc<RoleConfig<S::BuiltinRole>>,
user_roles: HashSet<S::BuiltinRole>,
#[allow(unused)] // will be used soon
storage: S,
Expand All @@ -45,58 +43,43 @@ pub trait StorageDriver: Clone {
fn fetch_subject_roles(
&self,
subject_id: i64,
roles_config: &RoleConfig<Self::BuiltinRole>,
) -> impl Future<Output = Result<HashSet<Self::BuiltinRole>, Self::Error>> + Send;

fn ensure_subject_roles(
&self,
subject_id: i64,
roles_config: &RoleConfig<Self::BuiltinRole>,
roles: HashSet<Self::BuiltinRole>,
) -> impl Future<Output = Result<(), Self::Error>> + Send;

fn remove_subject_roles(
&self,
subject_id: i64,
roles_config: &RoleConfig<Self::BuiltinRole>,
roles: HashSet<Self::BuiltinRole>,
) -> impl Future<Output = Result<HashSet<Self::BuiltinRole>, Self::Error>> + Send;
}

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: Arc<RoleConfig<S::BuiltinRole>>,
storage_driver: S,
) -> Result<Self, S::Error> {
#[tracing::instrument(skip_all, fields(%user), err)]
pub async fn try_initialize(user: UserInfo, 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?;
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<RoleConfig<S::BuiltinRole>>, 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,
}
}
Expand All @@ -106,7 +89,7 @@ impl<S: StorageDriver> Authorizer<S> {
}

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
Expand All @@ -132,65 +115,37 @@ impl<S: StorageDriver> Authorizer<S> {
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<Vec<RoleIdentifier>, 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<HashSet<S::BuiltinRole>, 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<S::BuiltinRole>,
) -> 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,
roles: HashSet<S::BuiltinRole>,
) -> 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));
Expand All @@ -203,7 +158,6 @@ impl<S: StorageDriver> std::fmt::Debug for Authorizer<S> {
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()
}
Expand Down Expand Up @@ -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!(
Expand All @@ -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
Expand All @@ -270,7 +222,6 @@ mod tests {
identity: "toto".to_owned(),
name: "Sir Toto, the One and Only".to_owned(),
},
config.into(),
storage,
)
.await
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -334,7 +285,6 @@ mod tests {
async fn fetch_subject_roles(
&self,
subject_id: i64,
_roles_config: &RoleConfig<Self::BuiltinRole>,
) -> Result<HashSet<Self::BuiltinRole>, Self::Error> {
let user_roles = self.user_roles.lock().unwrap();
let roles = user_roles.get(&subject_id).cloned().expect("no user");
Expand All @@ -344,7 +294,6 @@ mod tests {
async fn ensure_subject_roles(
&self,
subject_id: i64,
_roles_config: &RoleConfig<Self::BuiltinRole>,
roles: HashSet<Self::BuiltinRole>,
) -> Result<(), Self::Error> {
let mut user_roles = self.user_roles.lock().unwrap();
Expand All @@ -355,7 +304,6 @@ mod tests {
async fn remove_subject_roles(
&self,
subject_id: i64,
_roles_config: &RoleConfig<Self::BuiltinRole>,
roles: HashSet<Self::BuiltinRole>,
) -> Result<HashSet<Self::BuiltinRole>, Self::Error> {
let mut user_roles = self.user_roles.lock().unwrap();
Expand Down
37 changes: 14 additions & 23 deletions editoast/editoast_authz/src/builtin_role.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 6 additions & 18 deletions editoast/editoast_authz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -18,24 +18,12 @@ pub mod fixtures {
DocDelete,
UserAdd,
UserBan,
Superuser,
}

impl roles::BuiltinRoleSet for TestBuiltinRole {}

pub fn default_test_config() -> roles::RoleConfig<TestBuiltinRole> {
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
}
}
}
Loading
Loading