Skip to content

Commit 016cefe

Browse files
younesschrifiSh099078
authored andcommitted
editoast: list rolling_stocks privileges
Signed-off-by: Youness CHRIFI ALAOUI <youness.chrifi@gmail.com>
1 parent 34956fb commit 016cefe

4 files changed

Lines changed: 250 additions & 7 deletions

File tree

editoast/authz/src/model.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ pub enum InfraGrant {
138138
Eq,
139139
Hash,
140140
)]
141+
#[cfg_attr(test, derive(PartialOrd, Ord))]
141142
#[fga(name = "rolling_stock")]
142143
pub struct RollingStock(pub i64);
143144

editoast/authz/src/v2/rolling_stock.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use crate::RollingStockPrivilege;
1414
use crate::Subject;
1515
use crate::User;
1616
use crate::v2::Actor;
17+
use crate::v2::ResourcesList;
18+
use crate::v2::subject_roles;
1719

1820
pub fn rolling_stock_privileges(
1921
user: User,
@@ -229,6 +231,58 @@ pub fn rolling_stock_granted_subjects(
229231
.with_check(Check::RollingStockExists(rolling_stock))
230232
}
231233

234+
pub fn rolling_stock_list(
235+
user: User,
236+
privilege: RollingStockPrivilege,
237+
) -> Protected<ResourcesList<RollingStock>> {
238+
subject_roles(Subject::user(user)).map(move |openfga, roles| {
239+
async move {
240+
if roles.contains(&Role::Admin) {
241+
return Ok(ResourcesList::All);
242+
}
243+
let authorized_rolling_stocks = match privilege {
244+
RollingStockPrivilege::CanRead => {
245+
openfga
246+
.list_objects(RollingStock::can_read().query_objects(&user))
247+
.await?
248+
}
249+
RollingStockPrivilege::CanShareRead => {
250+
openfga
251+
.list_objects(RollingStock::can_share_read().query_objects(&user))
252+
.await?
253+
}
254+
RollingStockPrivilege::CanWrite => {
255+
openfga
256+
.list_objects(RollingStock::can_write().query_objects(&user))
257+
.await?
258+
}
259+
RollingStockPrivilege::CanShareWrite => {
260+
openfga
261+
.list_objects(RollingStock::can_share_write().query_objects(&user))
262+
.await?
263+
}
264+
RollingStockPrivilege::CanDelete => {
265+
openfga
266+
.list_objects(RollingStock::can_delete().query_objects(&user))
267+
.await?
268+
}
269+
RollingStockPrivilege::CanShareOwnership => {
270+
openfga
271+
.list_objects(RollingStock::can_share_ownership().query_objects(&user))
272+
.await?
273+
}
274+
RollingStockPrivilege::CanRevoke => {
275+
openfga
276+
.list_objects(RollingStock::can_revoke().query_objects(&user))
277+
.await?
278+
}
279+
};
280+
Ok(ResourcesList::Privileged(authorized_rolling_stocks))
281+
}
282+
.boxed()
283+
})
284+
}
285+
232286
#[cfg(test)]
233287
mod tests {
234288
use rstest::rstest;
@@ -376,6 +430,79 @@ mod tests {
376430
assert_eq!(response, expected);
377431
}
378432

433+
#[tokio::test]
434+
async fn check_rolling_stock_list_no_rights_and_admin() {
435+
let openfga = crate::authz_client!();
436+
openfga
437+
.prepare_writes()
438+
.write(&RollingStock::reader().tuple(&User(1), &RollingStock(1)))
439+
.write(&RollingStock::reader().tuple(&User(1), &RollingStock(3)))
440+
.write(&RollingStock::writer().tuple(&User(2), &RollingStock(2)))
441+
.write(&User::role().tuple(&Role::Admin, &User(3)))
442+
.execute()
443+
.await
444+
.unwrap();
445+
let rolling_stocks_1 = openfga
446+
.rolling_stock_list(User(1), RollingStockPrivilege::CanRead)
447+
.await
448+
.unwrap_privileged()
449+
.into_iter();
450+
let rolling_stocks_2 = openfga
451+
.rolling_stock_list(User(2), RollingStockPrivilege::CanRead)
452+
.await
453+
.unwrap_privileged()
454+
.into_iter();
455+
let rolling_stocks_no_rights = openfga
456+
.rolling_stock_list(User(4), RollingStockPrivilege::CanRead)
457+
.await
458+
.unwrap_privileged();
459+
let rolling_stocks_admin = openfga
460+
.rolling_stock_list(User(3), RollingStockPrivilege::CanRead)
461+
.await;
462+
assert_eq!(
463+
rolling_stocks_1.sorted().collect_vec(),
464+
vec![RollingStock(1), RollingStock(3)]
465+
);
466+
assert_eq!(
467+
rolling_stocks_2.sorted().collect_vec(),
468+
vec![RollingStock(2)]
469+
);
470+
assert_eq!(rolling_stocks_no_rights, vec![]);
471+
assert!(matches!(rolling_stocks_admin, ResourcesList::All));
472+
}
473+
474+
#[tokio::test]
475+
async fn rolling_stock_list_only_returns_resources_with_the_queried_privilege() {
476+
let openfga = crate::authz_client!();
477+
openfga
478+
.prepare_writes()
479+
.write(&RollingStock::reader().tuple(&User(1), &RollingStock(1)))
480+
.write(&RollingStock::writer().tuple(&User(2), &RollingStock(2)))
481+
.execute()
482+
.await
483+
.unwrap();
484+
485+
// A reader grant grants read access and not write access
486+
let reader_can_read = openfga
487+
.rolling_stock_list(User(1), RollingStockPrivilege::CanRead)
488+
.await
489+
.unwrap_privileged();
490+
assert_eq!(reader_can_read, vec![RollingStock(1)]);
491+
492+
let reader_can_write = openfga
493+
.rolling_stock_list(User(1), RollingStockPrivilege::CanWrite)
494+
.await
495+
.unwrap_privileged();
496+
assert_eq!(reader_can_write, vec![]);
497+
498+
// A writer grant grants both read and write access
499+
let writer_can_write = openfga
500+
.rolling_stock_list(User(2), RollingStockPrivilege::CanWrite)
501+
.await
502+
.unwrap_privileged();
503+
assert_eq!(writer_can_write, vec![RollingStock(2)]);
504+
}
505+
379506
#[rstest]
380507
#[case::rolling_stock_privileges(
381508
rolling_stock_privileges(User(1), RollingStock(1)).checks,
@@ -402,6 +529,13 @@ mod tests {
402529
Check::RollingStockExists(RollingStock(1))
403530
]
404531
)]
532+
#[rstest]
533+
#[case::rolling_stock_list(
534+
rolling_stock_list(User(1), RollingStockPrivilege::CanRead).checks,
535+
&[
536+
Check::SubjectExists(Subject::user(1)),
537+
]
538+
)]
405539
fn protected_contains_expected_checks(
406540
#[case] protected_checks: HashSet<Check>,
407541
#[case] expected_checks: &[Check],

editoast/authz/src/v2/test_client_ext.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ pub trait TestClientExt {
6363
subject: Subject,
6464
grant: RollingStockGrant,
6565
);
66+
async fn rolling_stock_list(
67+
&self,
68+
user: User,
69+
privilege: RollingStockPrivilege,
70+
) -> ResourcesList<RollingStock>;
6671
}
6772

6873
impl TestClientExt for fga::Client {
@@ -258,4 +263,18 @@ impl TestClientExt for fga::Client {
258263
.await
259264
.unwrap()
260265
}
266+
267+
async fn rolling_stock_list(
268+
&self,
269+
user: User,
270+
privilege: RollingStockPrivilege,
271+
) -> ResourcesList<RollingStock> {
272+
let authorize = special_authorizers::Authorize(self);
273+
authorize
274+
.access_value(crate::v2::rolling_stock::rolling_stock_list(
275+
user, privilege,
276+
))
277+
.await
278+
.unwrap()
279+
}
261280
}

editoast/src/views/rolling_stock/light.rs

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use authz::RollingStockPrivilege;
2+
use authz::v2::Authorizer;
3+
use authz::v2::Check;
24
use authz::v2::rolling_stock_privileges;
35
use axum::Extension;
46
use axum::extract::Json;
@@ -8,7 +10,6 @@ use axum::extract::State;
810
use common::units;
911
use common::units::quantities::Length;
1012
use database::DbConnection;
11-
use database::DbConnectionPoolV2;
1213
use editoast_models::prelude::*;
1314
use editoast_models::rolling_stock::RollingStock;
1415
use editoast_models::rolling_stock::TrainMainCategory;
@@ -22,7 +23,6 @@ use schemas::rolling_stock::SupportedSignalingSystem;
2223
use serde::Serialize;
2324
use std::collections::HashMap;
2425
use std::collections::HashSet;
25-
use std::sync::Arc;
2626
use uom::si::f64::Mass;
2727
use uom::si::f64::Velocity;
2828
use utoipa::ToSchema;
@@ -32,7 +32,9 @@ use super::RollingStockIdParam;
3232
use super::RollingStockKey;
3333
use super::RollingStockNameParam;
3434
use crate::AppState;
35+
use crate::authorizers::impossible;
3536
use crate::error::Result;
37+
use crate::views::AuthorizationError;
3638
use crate::views::pagination::PaginatedList;
3739
use crate::views::pagination::PaginationQueryParams;
3840
use crate::views::pagination::PaginationStats;
@@ -88,14 +90,48 @@ pub(in crate::views) struct LightRollingStockWithLiveriesCountList {
8890
)
8991
)]
9092
pub(in crate::views) async fn list(
91-
State(db_pool): State<Arc<DbConnectionPoolV2>>,
93+
State(AppState {
94+
db_pool, regulator, ..
95+
}): State<AppState>,
96+
97+
Extension(authn_state): Extension<crate::authentication::State>,
9298
Query(page_settings): Query<PaginationQueryParams<1000>>,
9399
) -> Result<Json<LightRollingStockWithLiveriesCountList>> {
94-
let settings = page_settings
95-
.into_selection_settings()
96-
.order_by(|| RollingStock::ID.asc());
100+
let conn = &mut db_pool.get().await?;
101+
let default_settings = page_settings.into_selection_settings();
102+
let settings = if let Some(user) = authn_state.regular_user() {
103+
let authorizer = authn_state.authorizer(regulator.openfga(), db_pool.get().await?);
104+
let authorized_rolling_stocks = authorizer
105+
.authorize(authz::v2::rolling_stock_list(
106+
user,
107+
RollingStockPrivilege::CanRead,
108+
))
109+
.await?
110+
.access()
111+
.await?
112+
.map_err(|err| match err {
113+
Check::SubjectExists(_) => AuthorizationError::Forbidden,
114+
check => impossible!(check),
115+
})?;
116+
match authorized_rolling_stocks {
117+
authz::v2::ResourcesList::All => default_settings,
118+
authz::v2::ResourcesList::Privileged(authorized_rolling_stocks) => default_settings
119+
.filter(move || {
120+
RollingStock::ID.eq_any(
121+
authorized_rolling_stocks
122+
.iter()
123+
.map(|rolling_stock| rolling_stock.0)
124+
.collect(),
125+
)
126+
}),
127+
}
128+
} else {
129+
default_settings
130+
};
131+
97132
let (rolling_stocks, stats) =
98-
RollingStock::list_paginated(&mut db_pool.get().await?, settings).await?;
133+
RollingStock::list_paginated(conn, settings.order_by(move || RollingStock::ID.asc()))
134+
.await?;
99135

100136
let results = rolling_stocks.into_iter().zip(db_pool.iter_conn()).map(
101137
|(rolling_stock, conn)| async move {
@@ -302,6 +338,8 @@ impl From<ModeEffortCurves> for LightModeEffortCurves {
302338

303339
#[cfg(test)]
304340
mod tests {
341+
use authz::Role;
342+
use authz::RollingStockGrant;
305343
use pretty_assertions::assert_eq;
306344
use std::collections::HashSet;
307345

@@ -329,6 +367,57 @@ mod tests {
329367
app.get("/light_rolling_stock").await.assert_status_ok();
330368
}
331369

370+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
371+
async fn rolling_stock_list_filters_authorized_rolling_stocks() {
372+
let app = test_app!().build();
373+
let db_pool = app.db_pool();
374+
let rolling_stock_grant =
375+
create_fast_rolling_stock(&mut db_pool.get_ok(), "fast_rolling_stock_grant").await;
376+
let rolling_stock_no_grant =
377+
create_fast_rolling_stock(&mut db_pool.get_ok(), "fast_rolling_stock_no_grant").await;
378+
// Regular user with the correct roles should see only the rolling stock he is associated with:
379+
let user = app
380+
.user("user_identity", "user_name")
381+
.with_rolling_stock_grant(rolling_stock_grant.id, RollingStockGrant::Reader)
382+
.create()
383+
.await;
384+
let response: LightRollingStockWithLiveriesCountList = app
385+
.get("/light_rolling_stock")
386+
.by_user(user.as_ref())
387+
.await
388+
.assert_status_ok()
389+
.json();
390+
assert_eq!(
391+
response
392+
.results
393+
.iter()
394+
.map(|rolling_stock| rolling_stock.rolling_stock.id)
395+
.collect::<Vec<_>>(),
396+
vec![rolling_stock_grant.id]
397+
);
398+
// TODO: separate in two tests (admins_can_list_all_rolling_stocks and user_can_list_their_rolling_stocks)
399+
// An admin should see all the rolling stocks:
400+
let admin = app
401+
.user("admin", "admin")
402+
.with_roles([Role::Admin])
403+
.create()
404+
.await;
405+
let response: LightRollingStockWithLiveriesCountList = app
406+
.get("/light_rolling_stock/")
407+
.by_user(admin.as_ref())
408+
.await
409+
.assert_status_ok()
410+
.json();
411+
assert_eq!(
412+
response
413+
.results
414+
.iter()
415+
.map(|rolling_stock| rolling_stock.rolling_stock.id)
416+
.collect::<Vec<_>>(),
417+
vec![rolling_stock_grant.id, rolling_stock_no_grant.id]
418+
);
419+
}
420+
332421
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
333422
async fn get_light_rolling_stock() {
334423
// GIVEN

0 commit comments

Comments
 (0)