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

Add role management endpoints #8984

Merged
merged 5 commits into from
Oct 1, 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
2 changes: 2 additions & 0 deletions editoast/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions editoast/editoast_authz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition.workspace = true
fixtures = []

[dependencies]
itertools.workspace = true
serde = { workspace = true, features = ["derive"] }
strum.workspace = true
thiserror.workspace = true
Expand All @@ -16,6 +17,7 @@ toml_edit = { version = "0.22.16", default-features = false, features = [
"serde",
] }
tracing.workspace = true
utoipa.workspace = true

[dev-dependencies]
pretty_assertions.workspace = true
Expand Down
114 changes: 110 additions & 4 deletions editoast/editoast_authz/src/authorizer.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
use itertools::Itertools as _;
use std::{collections::HashSet, future::Future, sync::Arc};

use tracing::debug;
use tracing::Level;

use crate::roles::{BuiltinRoleSet, RoleConfig};
use crate::roles::{BuiltinRoleSet, RoleConfig, RoleIdentifier};

pub type UserIdentity = String;
pub type UserName = String;

#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct UserInfo {
pub identity: UserIdentity,
pub name: UserName,
}

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

pub trait StorageDriver {
pub trait StorageDriver: Clone {
type BuiltinRole: BuiltinRoleSet + std::fmt::Debug;
type Error: std::error::Error;

fn get_user_id(
&self,
user_info: &UserInfo,
) -> impl Future<Output = Result<Option<i64>, Self::Error>> + Send;

fn get_user_info(
&self,
user_id: i64,
) -> impl Future<Output = Result<Option<UserInfo>, Self::Error>> + Send;

fn ensure_user(&self, user: &UserInfo)
-> impl Future<Output = Result<i64, Self::Error>> + Send;

Expand Down Expand Up @@ -89,10 +101,23 @@ impl<S: StorageDriver> Authorizer<S> {
}
}

pub fn user_id(&self) -> i64 {
self.user_id
}

pub fn is_superuser(&self) -> bool {
self.roles_config.is_superuser()
}

/// Returns whether a user with some id exists
#[tracing::instrument(skip_all, fields(user_id = %user_id), ret(level = Level::DEBUG), err)]
pub async fn user_exists(&self, user_id: i64) -> Result<bool, S::Error> {
self.storage
.get_user_info(user_id)
.await
.map(|x| x.is_some())
}

/// 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(level = Level::DEBUG), err)]
pub async fn check_roles(
Expand All @@ -106,6 +131,71 @@ 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?;
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)]
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())
.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)]
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())
.await?;
tracing::debug!(?removed_roles, "removed roles");
self.user_roles.retain(|r| !roles.contains(r));
Ok(())
}
}

impl<S: StorageDriver> std::fmt::Debug for Authorizer<S> {
Expand Down Expand Up @@ -277,5 +367,21 @@ mod tests {
.collect();
Ok(removed_roles)
}

async fn get_user_id(&self, user_info: &UserInfo) -> Result<Option<i64>, Self::Error> {
Ok(self.users.lock().unwrap().get(&user_info.identity).copied())
}

async fn get_user_info(&self, user_id: i64) -> Result<Option<UserInfo>, Self::Error> {
let users = self.users.lock().unwrap();
let user_info = users
.iter()
.find(|(_, id)| **id == user_id)
.map(|(identity, _)| UserInfo {
identity: identity.clone(),
name: "Mocked User".to_owned(),
});
Ok(user_info)
}
}
}
42 changes: 17 additions & 25 deletions editoast/editoast_authz/src/builtin_role.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
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, EnumString, AsRefStr, Display)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, EnumString, AsRefStr, Display, ToSchema,
)]
#[strum(serialize_all = "snake_case")]
pub enum BuiltinRole {
#[strum(serialize = "operational_studies:write")]
Expand Down Expand Up @@ -32,6 +36,8 @@ pub enum BuiltinRole {

#[strum(serialize = "stdcm")]
Stdcm,
#[strum(serialize = "stdcm:admin")]
StdcmAdmin,

#[strum(serialize = "timetable:read")]
TimetableRead,
Expand All @@ -43,29 +49,15 @@ pub enum BuiltinRole {
#[strum(serialize = "document:write")]
DocumentWrite,

#[strum(serialize = "admin")]
Admin,
}
#[strum(serialize = "subject:read")]
SubjectRead,
#[strum(serialize = "subject:write")]
SubjectWrite,

impl BuiltinRoleSet for BuiltinRole {
fn implies_iter(&self) -> impl IntoIterator<Item = Self> {
use BuiltinRole::*;
match self {
OpsRead => vec![],
OpsWrite => vec![OpsRead],
InfraRead => vec![],
InfraWrite => vec![InfraRead],
RollingStockCollectionRead => vec![],
RollingStockCollectionWrite => vec![RollingStockCollectionRead],
WorkScheduleWrite => vec![WorkScheduleRead],
WorkScheduleRead => vec![],
MapRead => vec![],
Stdcm => vec![MapRead],
TimetableRead => vec![],
TimetableWrite => vec![TimetableRead],
DocumentRead => vec![],
DocumentWrite => vec![DocumentRead],
Admin => vec![],
}
}
#[strum(serialize = "role:read")]
RoleRead,
#[strum(serialize = "role:write")]
RoleWrite,
}

impl BuiltinRoleSet for BuiltinRole {}
12 changes: 1 addition & 11 deletions editoast/editoast_authz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,7 @@ pub mod fixtures {
UserBan,
}

impl roles::BuiltinRoleSet for TestBuiltinRole {
fn implies_iter(&self) -> impl IntoIterator<Item = Self> {
match self {
Self::DocRead => vec![],
Self::DocEdit => vec![Self::DocRead],
Self::DocDelete => vec![Self::DocEdit],
Self::UserAdd => vec![],
Self::UserBan => vec![],
}
}
}
impl roles::BuiltinRoleSet for TestBuiltinRole {}

pub fn default_test_config() -> roles::RoleConfig<TestBuiltinRole> {
const SOURCE: &str = r#"
Expand Down
15 changes: 8 additions & 7 deletions editoast/editoast_authz/src/roles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ use std::{
pub trait BuiltinRoleSet:
FromStr + AsRef<str> + Sized + Clone + std::hash::Hash + std::cmp::Eq + std::fmt::Debug
{
fn implies_iter(&self) -> impl IntoIterator<Item = Self>;

fn as_str(&self) -> &str {
self.as_ref()
}
Expand Down Expand Up @@ -41,14 +39,13 @@ impl<B: BuiltinRoleSet> RoleConfig<B> {
self.superuser
}

pub fn resolve<'r>(
&self,
app_roles: impl Iterator<Item = &'r RoleIdentifier>,
) -> Result<HashSet<B>, &'r RoleIdentifier> {
pub fn resolve<'r>(&self, roles: impl Iterator<Item = &'r str>) -> Result<HashSet<B>, &'r str> {
let mut resolved = HashSet::new();
for role in app_roles {
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);
}
Expand Down Expand Up @@ -128,6 +125,10 @@ impl<B: BuiltinRoleSet> RoleConfig<B> {
}
Ok(config)
}

pub fn application_roles(&self) -> impl Iterator<Item = &RoleIdentifier> {
self.resolved_roles.keys()
}
}

#[derive(Debug, thiserror::Error)]
Expand Down
2 changes: 1 addition & 1 deletion editoast/editoast_models/src/db_connection_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ impl DbConnection {
//
// :WARNING: If you ever need to modify this function, please take a look at the
// original `diesel` function, they probably do it right more than us.
pub async fn transaction<'a, R, E, F>(self, callback: F) -> std::result::Result<R, E>
pub async fn transaction<'a, R, E, F>(&self, callback: F) -> std::result::Result<R, E>
where
F: FnOnce(Self) -> ScopedBoxFuture<'a, 'a, std::result::Result<R, E>> + Send + 'a,
E: From<diesel::result::Error> + Send + 'a,
Expand Down
Loading
Loading