Skip to content

Commit

Permalink
editoast: add role management endpoints
Browse files Browse the repository at this point in the history
Signed-off-by: Leo Valais <leo.valais97@gmail.com>
  • Loading branch information
leovalais committed Sep 24, 2024
1 parent e17cc40 commit 6c32f1a
Show file tree
Hide file tree
Showing 13 changed files with 755 additions and 34 deletions.
1 change: 1 addition & 0 deletions editoast/Cargo.lock

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

1 change: 1 addition & 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 Down
108 changes: 104 additions & 4 deletions editoast/editoast_authz/src/authorizer.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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;
Expand Down Expand Up @@ -47,8 +48,8 @@ pub trait StorageDriver: Clone {
roles_config: &RoleConfig<Self::BuiltinRole>,
) -> impl Future<Output = Result<HashSet<Self::BuiltinRole>, Self::Error>> + Send;

fn ensure_subject_roles(
&self,
fn ensure_subject_roles<'a>(
&'a self,
subject_id: i64,
roles_config: &RoleConfig<Self::BuiltinRole>,
roles: HashSet<Self::BuiltinRole>,
Expand Down Expand Up @@ -100,23 +101,101 @@ 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(
&self,
required_roles: HashSet<S::BuiltinRole>,
) -> Result<bool, S::Error> {
if self.is_superuser() {
tracing::debug!("role checking skipped for superuser");
tracing::warn!("role checking skipped for superuser");
return Ok(true);
}

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 @@ -288,5 +367,26 @@ mod tests {
.collect();
Ok(removed_roles)
}

async fn get_user_id(&self, user_info: &UserInfo) -> Result<Option<i64>, Self::Error> {
self.users
.lock()
.unwrap()
.get(&user_info.identity)
.map(|id| Ok(Some(*id)))
.unwrap_or_else(|| Ok(None))
}

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(),
});
async move { Ok(user_info) }
}
}
}
11 changes: 9 additions & 2 deletions editoast/editoast_authz/src/builtin_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,15 @@ pub enum BuiltinRole {
#[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 {}
13 changes: 8 additions & 5 deletions editoast/editoast_authz/src/roles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,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 @@ -126,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
Loading

0 comments on commit 6c32f1a

Please sign in to comment.