Skip to content

Commit 564382a

Browse files
committed
editoast: authz: add remove_roles to v2
Add a new function remove_roles to replaces revoke_user_roles and revoke_group_roles. Remove the old revoke_user_roles and revoke_group_roles functions. add tests for remove_roles. Signed-off-by: Simon <sim.gaubert.sg@gmail.com>
1 parent 7b20698 commit 564382a

File tree

4 files changed

+156
-55
lines changed

4 files changed

+156
-55
lines changed

editoast/authz/src/authorizer.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,15 @@ mod tests {
217217

218218
// remove role
219219
{
220-
regulator()
221-
.revoke_user_roles(&User(user_id), HashSet::from([Role::OperationalStudies]))
222-
.await
223-
.expect("roles should be stripped");
220+
v2::remove_roles(
221+
Subject::user(user_id),
222+
HashSet::from([Role::OperationalStudies]),
223+
)
224+
.authorize(&v2::test_authorizers::Authorize(regulator().openfga()))
225+
.await
226+
.expect("roles should be stripped")
227+
.unwrap_authorized()
228+
.await;
224229
}
225230

226231
assert!(

editoast/authz/src/regulator.rs

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -199,42 +199,6 @@ impl<S: StorageDriver> Regulator<S> {
199199
Ok(roles.into_iter().collect())
200200
}
201201

202-
#[tracing::instrument(skip_all, fields(user, ?roles), ret(level = Level::DEBUG), err)]
203-
pub async fn revoke_user_roles(
204-
&self,
205-
user: &User,
206-
roles: HashSet<Role>,
207-
) -> Result<(), Error<S::Error>> {
208-
if !self.user_exists(user.0).await? {
209-
return Err(Error::UnknownSubject(user.0));
210-
}
211-
let mut deletes = self.openfga.prepare_deletes();
212-
let existing_roles = self.user_roles(user).await?;
213-
for role in roles.intersection(&existing_roles) {
214-
deletes.push(&User::role().tuple(role, user));
215-
}
216-
deletes.execute().await?;
217-
Ok(())
218-
}
219-
220-
#[tracing::instrument(skip_all, fields(group, ?roles), ret(level = Level::DEBUG), err)]
221-
pub async fn revoke_group_roles(
222-
&self,
223-
group: &Group,
224-
roles: HashSet<Role>,
225-
) -> Result<(), Error<S::Error>> {
226-
if !self.group_exists(group.0).await? {
227-
return Err(Error::UnknownSubject(group.0));
228-
}
229-
let mut deletes = self.openfga.prepare_deletes();
230-
let existing_roles = self.group_roles(group).await?;
231-
for role in roles.intersection(&existing_roles) {
232-
deletes.push(&Group::role().tuple(role, group));
233-
}
234-
deletes.execute().await?;
235-
Ok(())
236-
}
237-
238202
#[tracing::instrument(skip(self), fields(user, ?roles), ret(level = Level::DEBUG), err)]
239203
pub async fn check_roles(
240204
&self,

editoast/authz/src/v2.rs

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@ pub fn remove_members(group: Group, members: HashSet<User>) -> Protected<'static
256256
writes.push(&User::group().tuple(&group, user));
257257
}
258258
writes.execute().await?;
259-
260259
Ok(())
261260
}
262261
.boxed()
@@ -266,6 +265,45 @@ pub fn remove_members(group: Group, members: HashSet<User>) -> Protected<'static
266265
.with_guardrail(Guardrail::IssuerHasRole(Role::Admin))
267266
}
268267

268+
// TODO: move somewhere more appropriate
269+
/// Removes the specified roles from the subject
270+
///
271+
/// Idempotent but not atomic due to the lack of transactions in OpenFGA.
272+
pub fn remove_roles(subject: Subject, roles: HashSet<Role>) -> Protected<'static, ()> {
273+
Protected::new(move |openfga| {
274+
async move {
275+
let existing_roles = match &subject {
276+
Subject::User(user) => Role::list_roles(openfga, User::role(), user).await?,
277+
Subject::Group(group) => Role::list_roles(openfga, Group::role(), group).await?,
278+
};
279+
280+
let existing_roles = HashSet::from_iter(existing_roles);
281+
let removed_roles = roles.intersection(&existing_roles);
282+
let mut writes = openfga.prepare_deletes();
283+
match subject {
284+
Subject::User(user) => {
285+
for role in removed_roles {
286+
writes.push(&User::role().tuple(role, &user));
287+
}
288+
}
289+
Subject::Group(group) => {
290+
for role in removed_roles {
291+
writes.push(&Group::role().tuple(role, &group));
292+
}
293+
}
294+
}
295+
writes.execute().await?;
296+
Ok(())
297+
}
298+
.boxed()
299+
})
300+
.with_guardrail(Guardrail::IssuerHasRole(Role::Admin))
301+
.with_check(match &subject {
302+
Subject::User(user) => SanityCheck::UserExists(*user),
303+
Subject::Group(group) => SanityCheck::GroupExists(*group),
304+
})
305+
}
306+
269307
pub mod test_authorizers {
270308
use std::convert::Infallible;
271309

@@ -539,4 +577,100 @@ mod tests {
539577
HashSet::from_iter([Role::Admin, Role::Stdcm, Role::OperationalStudies])
540578
);
541579
}
580+
581+
#[tokio::test]
582+
async fn remove_roles_idempotent() {
583+
let openfga = crate::authz_client!();
584+
let authorize = Authorize(&openfga);
585+
586+
add_roles(
587+
Subject::user(1),
588+
HashSet::from_iter([Role::Admin, Role::Stdcm]),
589+
)
590+
.authorize(&authorize)
591+
.await
592+
.unwrap()
593+
.unwrap_authorized()
594+
.await;
595+
assert_eq!(
596+
openfga.subject_roles(&Subject::user(1)).await,
597+
HashSet::from_iter([Role::Admin, Role::Stdcm])
598+
);
599+
600+
remove_roles(
601+
Subject::user(1),
602+
HashSet::from_iter([Role::Admin, Role::Stdcm]),
603+
)
604+
.authorize(&authorize)
605+
.await
606+
.unwrap()
607+
.unwrap_authorized()
608+
.await;
609+
assert_eq!(
610+
openfga.subject_roles(&Subject::user(1)).await,
611+
HashSet::from_iter([])
612+
);
613+
614+
remove_roles(
615+
Subject::user(1),
616+
HashSet::from_iter([Role::Admin, Role::Stdcm]),
617+
)
618+
.authorize(&authorize)
619+
.await
620+
.unwrap()
621+
.unwrap_authorized()
622+
.await;
623+
assert_eq!(
624+
openfga.subject_roles(&Subject::user(1)).await,
625+
HashSet::from_iter([])
626+
);
627+
}
628+
629+
#[tokio::test]
630+
async fn remove_roles_intersecting_calls() {
631+
let openfga = crate::authz_client!();
632+
let authorize = Authorize(&openfga);
633+
634+
add_roles(
635+
Subject::user(1),
636+
HashSet::from_iter([Role::Admin, Role::Stdcm, Role::OperationalStudies]),
637+
)
638+
.authorize(&authorize)
639+
.await
640+
.unwrap()
641+
.unwrap_authorized()
642+
.await;
643+
assert_eq!(
644+
openfga.subject_roles(&Subject::user(1)).await,
645+
HashSet::from_iter([Role::Admin, Role::Stdcm, Role::OperationalStudies])
646+
);
647+
648+
remove_roles(
649+
Subject::user(1),
650+
HashSet::from_iter([Role::Admin, Role::Stdcm]),
651+
)
652+
.authorize(&authorize)
653+
.await
654+
.unwrap()
655+
.unwrap_authorized()
656+
.await;
657+
assert_eq!(
658+
openfga.subject_roles(&Subject::user(1)).await,
659+
HashSet::from_iter([Role::OperationalStudies])
660+
);
661+
662+
remove_roles(
663+
Subject::user(1),
664+
HashSet::from_iter([Role::Admin, Role::OperationalStudies]),
665+
)
666+
.authorize(&authorize)
667+
.await
668+
.unwrap()
669+
.unwrap_authorized()
670+
.await;
671+
assert_eq!(
672+
openfga.subject_roles(&Subject::user(1)).await,
673+
HashSet::from_iter([])
674+
);
675+
}
542676
}

editoast/src/client/roles.rs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,12 @@ pub async fn remove_roles(
206206
pool: Arc<DbConnectionPoolV2>,
207207
openfga_config: OpenfgaConfig,
208208
) -> anyhow::Result<()> {
209-
let regulator = openfga_config.into_regulator(pool).await?;
209+
let openfga = &openfga_config.into_client().await?;
210+
let system = SystemAuthorizer {
211+
openfga,
212+
conn: pool.get().await?,
213+
};
214+
210215
let roles = roles
211216
.iter()
212217
.map(String::as_str)
@@ -220,19 +225,12 @@ pub async fn remove_roles(
220225
.collect_vec()
221226
.join(", "),
222227
);
223-
match parse_and_fetch_subject(&subject, regulator.driver()).await? {
224-
Subject {
225-
id,
226-
info: SubjectInfo::User(_),
227-
} => regulator.revoke_user_roles(&authz::User(id), roles).await?,
228-
Subject {
229-
id,
230-
info: SubjectInfo::Group(_),
231-
} => {
232-
regulator
233-
.revoke_group_roles(&authz::Group(id), roles)
234-
.await?
228+
let subject = parse_and_fetch_subject(&subject, &PgAuthDriver::new(pool)).await?;
229+
let remove_roles = authz::v2::remove_roles(subject.into_authz(), roles);
230+
match system.authorize(remove_roles).await?.access().await? {
231+
Ok(()) => Ok(()),
232+
Err(Rejection::NoSuchUser(_)) | Err(Rejection::NoSuchGroup(_)) => {
233+
unreachable!("checked by parse_and_fetch_subject")
235234
}
236235
}
237-
Ok(())
238236
}

0 commit comments

Comments
 (0)