Skip to content

Commit 6c32f1a

Browse files
committed
editoast: add role management endpoints
Signed-off-by: Leo Valais <[email protected]>
1 parent e17cc40 commit 6c32f1a

File tree

13 files changed

+755
-34
lines changed

13 files changed

+755
-34
lines changed

editoast/Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editoast/editoast_authz/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ edition.workspace = true
88
fixtures = []
99

1010
[dependencies]
11+
itertools.workspace = true
1112
serde = { workspace = true, features = ["derive"] }
1213
strum.workspace = true
1314
thiserror.workspace = true

editoast/editoast_authz/src/authorizer.rs

+104-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
use itertools::Itertools as _;
12
use std::{collections::HashSet, future::Future, sync::Arc};
23

34
use tracing::debug;
45
use tracing::Level;
56

6-
use crate::roles::{BuiltinRoleSet, RoleConfig};
7+
use crate::roles::{BuiltinRoleSet, RoleConfig, RoleIdentifier};
78

89
pub type UserIdentity = String;
910
pub type UserName = String;
@@ -47,8 +48,8 @@ pub trait StorageDriver: Clone {
4748
roles_config: &RoleConfig<Self::BuiltinRole>,
4849
) -> impl Future<Output = Result<HashSet<Self::BuiltinRole>, Self::Error>> + Send;
4950

50-
fn ensure_subject_roles(
51-
&self,
51+
fn ensure_subject_roles<'a>(
52+
&'a self,
5253
subject_id: i64,
5354
roles_config: &RoleConfig<Self::BuiltinRole>,
5455
roles: HashSet<Self::BuiltinRole>,
@@ -100,23 +101,101 @@ impl<S: StorageDriver> Authorizer<S> {
100101
}
101102
}
102103

104+
pub fn user_id(&self) -> i64 {
105+
self.user_id
106+
}
107+
103108
pub fn is_superuser(&self) -> bool {
104109
self.roles_config.is_superuser()
105110
}
106111

112+
/// Returns whether a user with some id exists
113+
#[tracing::instrument(skip_all, fields(user_id = %user_id), ret(level = Level::DEBUG), err)]
114+
pub async fn user_exists(&self, user_id: i64) -> Result<bool, S::Error> {
115+
self.storage
116+
.get_user_info(user_id)
117+
.await
118+
.map(|x| x.is_some())
119+
}
120+
107121
/// Check that the user has all the required builting roles
108122
#[tracing::instrument(skip_all, fields(user = %self.user, user_roles = ?self.user_roles, ?required_roles), ret(level = Level::DEBUG), err)]
109123
pub async fn check_roles(
110124
&self,
111125
required_roles: HashSet<S::BuiltinRole>,
112126
) -> Result<bool, S::Error> {
113127
if self.is_superuser() {
114-
tracing::debug!("role checking skipped for superuser");
128+
tracing::warn!("role checking skipped for superuser");
115129
return Ok(true);
116130
}
117131

118132
Ok(required_roles.is_subset(&self.user_roles))
119133
}
134+
135+
#[tracing::instrument(skip_all, fields(user_id, auth_user = %self.user, user_roles = ?self.user_roles), ret(level = Level::DEBUG), err)]
136+
pub async fn infer_application_roles(
137+
&self,
138+
user_id: i64,
139+
) -> Result<Vec<RoleIdentifier>, S::Error> {
140+
if self.is_superuser() {
141+
return Ok(self.roles_config.application_roles().cloned().collect_vec());
142+
}
143+
144+
let resolved_roles = &self.roles_config.resolved_roles;
145+
let user_roles = self
146+
.storage
147+
.fetch_subject_roles(user_id, &self.roles_config)
148+
.await?;
149+
150+
let app_roles = resolved_roles
151+
.iter()
152+
.filter(|(_, builtins)| user_roles.is_superset(builtins))
153+
.map(|(app_role, _)| app_role)
154+
.cloned()
155+
.collect_vec();
156+
157+
Ok(app_roles)
158+
}
159+
160+
#[tracing::instrument(skip_all, fields(user_id, auth_user = %self.user, user_roles = ?self.user_roles), ret(level = Level::DEBUG), err)]
161+
pub async fn user_builtin_roles(
162+
&self,
163+
user_id: i64,
164+
) -> Result<HashSet<S::BuiltinRole>, S::Error> {
165+
let user_roles = self
166+
.storage
167+
.fetch_subject_roles(user_id, &self.roles_config)
168+
.await?;
169+
Ok(user_roles.clone())
170+
}
171+
172+
#[tracing::instrument(skip_all, fields(user_id, auth_user = %self.user, ?roles, role_config = ?self.roles_config), ret(level = Level::DEBUG), err)]
173+
pub async fn grant_roles(
174+
&mut self,
175+
user_id: i64,
176+
roles: HashSet<S::BuiltinRole>,
177+
) -> Result<(), S::Error> {
178+
self.storage
179+
.ensure_subject_roles(user_id, &self.roles_config, roles.clone())
180+
.await?;
181+
self.user_roles.extend(roles);
182+
Ok(())
183+
}
184+
185+
#[tracing::instrument(skip_all, fields(user_id, auth_user = %self.user, ?roles, role_config = ?self.roles_config), ret(level = Level::DEBUG), err)]
186+
pub async fn strip_roles(
187+
&mut self,
188+
user_id: i64,
189+
roles: HashSet<S::BuiltinRole>,
190+
) -> Result<(), S::Error> {
191+
let removed_roles = self
192+
.storage
193+
.remove_subject_roles(user_id, &self.roles_config, roles.clone())
194+
.await?;
195+
tracing::debug!(?removed_roles, "removed roles");
196+
self.user_roles.retain(|r| !roles.contains(r));
197+
Ok(())
198+
}
120199
}
121200

122201
impl<S: StorageDriver> std::fmt::Debug for Authorizer<S> {
@@ -288,5 +367,26 @@ mod tests {
288367
.collect();
289368
Ok(removed_roles)
290369
}
370+
371+
async fn get_user_id(&self, user_info: &UserInfo) -> Result<Option<i64>, Self::Error> {
372+
self.users
373+
.lock()
374+
.unwrap()
375+
.get(&user_info.identity)
376+
.map(|id| Ok(Some(*id)))
377+
.unwrap_or_else(|| Ok(None))
378+
}
379+
380+
async fn get_user_info(&self, user_id: i64) -> Result<Option<UserInfo>, Self::Error> {
381+
let users = self.users.lock().unwrap();
382+
let user_info = users
383+
.iter()
384+
.find(|(_, id)| **id == user_id)
385+
.map(|(identity, _)| UserInfo {
386+
identity: identity.clone(),
387+
name: "Mocked User".to_owned(),
388+
});
389+
async move { Ok(user_info) }
390+
}
291391
}
292392
}

editoast/editoast_authz/src/builtin_role.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,15 @@ pub enum BuiltinRole {
4545
#[strum(serialize = "document:write")]
4646
DocumentWrite,
4747

48-
}
49-
48+
#[strum(serialize = "subject:read")]
49+
SubjectRead,
50+
#[strum(serialize = "subject:write")]
51+
SubjectWrite,
52+
53+
#[strum(serialize = "role:read")]
54+
RoleRead,
55+
#[strum(serialize = "role:write")]
56+
RoleWrite,
5057
}
5158

5259
impl BuiltinRoleSet for BuiltinRole {}

editoast/editoast_authz/src/roles.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,13 @@ impl<B: BuiltinRoleSet> RoleConfig<B> {
3939
self.superuser
4040
}
4141

42-
pub fn resolve<'r>(
43-
&self,
44-
app_roles: impl Iterator<Item = &'r RoleIdentifier>,
45-
) -> Result<HashSet<B>, &'r RoleIdentifier> {
42+
pub fn resolve<'r>(&self, roles: impl Iterator<Item = &'r str>) -> Result<HashSet<B>, &'r str> {
4643
let mut resolved = HashSet::new();
47-
for role in app_roles {
44+
for role in roles {
4845
if let Some(role) = self.resolved_roles.get(role) {
4946
resolved.extend(role.iter().cloned());
47+
} else if let Ok(builtin) = B::from_str(role) {
48+
resolved.insert(builtin);
5049
} else {
5150
return Err(role);
5251
}
@@ -126,6 +125,10 @@ impl<B: BuiltinRoleSet> RoleConfig<B> {
126125
}
127126
Ok(config)
128127
}
128+
129+
pub fn application_roles(&self) -> impl Iterator<Item = &RoleIdentifier> {
130+
self.resolved_roles.keys()
131+
}
129132
}
130133

131134
#[derive(Debug, thiserror::Error)]

0 commit comments

Comments
 (0)