@@ -394,10 +394,23 @@ pub(in crate::views) struct DeleteRollingStockQueryParams {
394394 )
395395) ]
396396pub ( in crate :: views) async fn delete (
397- State ( db_pool) : State < Arc < DbConnectionPoolV2 > > ,
397+ State ( AppState {
398+ regulator, db_pool, ..
399+ } ) : State < AppState > ,
400+ Extension ( authn_state) : Extension < crate :: authentication:: State > ,
398401 Path ( rolling_stock_id) : Path < i64 > ,
399402 Query ( DeleteRollingStockQueryParams { force } ) : Query < DeleteRollingStockQueryParams > ,
400403) -> Result < impl IntoResponse > {
404+ if let Some ( user) = authn_state. regular_user ( ) {
405+ let authorizer = authn_state. authorizer ( regulator. openfga ( ) , db_pool. get ( ) . await ?) ;
406+ crate :: authorizers:: require (
407+ & authorizer,
408+ rolling_stock_privileges ( user, authz:: RollingStock ( rolling_stock_id) ) ,
409+ & RollingStockPrivilege :: CanDelete ,
410+ )
411+ . await ?;
412+ }
413+
401414 let conn = & mut db_pool. get ( ) . await ?;
402415
403416 let rolling_stock = RollingStock :: retrieve_or_fail ( conn. clone ( ) , rolling_stock_id, || {
@@ -1619,7 +1632,7 @@ pub mod tests {
16191632 #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
16201633 async fn delete_locked_rolling_stock_fails ( ) {
16211634 // GIVEN
1622- let app = test_app ! ( ) . skip_authz ( ) . build ( ) ;
1635+ let app = test_app ! ( ) . build ( ) ;
16231636 let db_pool = app. db_pool ( ) ;
16241637
16251638 let locked_rs_name = "locked_fast_rolling_stock_name" ;
@@ -1633,9 +1646,16 @@ pub mod tests {
16331646 . await
16341647 . expect ( "Failed to create rolling stock" ) ;
16351648
1649+ let user = app
1650+ . user ( "owner" , "Owner" )
1651+ . with_roles ( [ Role :: OperationalStudies ] )
1652+ . with_rolling_stock_grant ( locked_fast_rolling_stock. id , RollingStockGrant :: Owner )
1653+ . create ( )
1654+ . await ;
16361655 // WHEN
16371656 let raw_response = app
16381657 . delete ( format ! ( "/rolling_stock/{}" , locked_fast_rolling_stock. id) . as_str ( ) )
1658+ . by_user ( & user. info )
16391659 . await ;
16401660
16411661 // THEN
@@ -1654,16 +1674,24 @@ pub mod tests {
16541674 #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
16551675 async fn delete_unlocked_unused_rolling_stock_succeeds ( ) {
16561676 // GIVEN
1657- let app = test_app ! ( ) . skip_authz ( ) . build ( ) ;
1677+ let app = test_app ! ( ) . build ( ) ;
16581678 let db_pool = app. db_pool ( ) ;
16591679
16601680 let rs_name = "fast_rolling_stock_name" ;
16611681 let fast_rolling_stock = create_fast_rolling_stock ( & mut db_pool. get_ok ( ) , rs_name) . await ;
16621682 assert ! ( !fast_rolling_stock. locked) ;
16631683
1684+ let user = app
1685+ . user ( "owner" , "Owner" )
1686+ . with_roles ( [ Role :: OperationalStudies ] )
1687+ . with_rolling_stock_grant ( fast_rolling_stock. id , RollingStockGrant :: Owner )
1688+ . create ( )
1689+ . await ;
1690+
16641691 // WHEN
16651692 let raw_response = app
16661693 . delete ( format ! ( "/rolling_stock/{}" , fast_rolling_stock. id) . as_str ( ) )
1694+ . by_user ( & user. info )
16671695 . await ;
16681696
16691697 // THEN
@@ -1676,6 +1704,124 @@ pub mod tests {
16761704 assert ! ( !rolling_stock_exists) ;
16771705 }
16781706
1707+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
1708+ async fn delete_rolling_stock_without_authorization ( ) {
1709+ // GIVEN
1710+ let app = test_app ! ( ) . build ( ) ;
1711+ let db_pool = app. db_pool ( ) ;
1712+
1713+ let fast_rolling_stock =
1714+ create_fast_rolling_stock ( & mut db_pool. get_ok ( ) , "unauthorized_rolling_stock" ) . await ;
1715+
1716+ // A user with the OperationalStudies role but only a write grant on the rolling stock
1717+ // (delete requires an owner/delete grant)
1718+ let user = app
1719+ . user ( "writer" , "Writer" )
1720+ . with_roles ( [ Role :: OperationalStudies ] )
1721+ . with_rolling_stock_grant ( fast_rolling_stock. id , RollingStockGrant :: Writer )
1722+ . create ( )
1723+ . await ;
1724+
1725+ // WHEN
1726+ let raw_response = app
1727+ . delete ( format ! ( "/rolling_stock/{}" , fast_rolling_stock. id) . as_str ( ) )
1728+ . by_user ( user. as_ref ( ) )
1729+ . await ;
1730+
1731+ // THEN
1732+ raw_response. assert_status_forbidden ( ) ;
1733+
1734+ // The rolling stock should still exist
1735+ let rolling_stock_exists =
1736+ RollingStock :: exists ( & mut db_pool. get_ok ( ) , fast_rolling_stock. id )
1737+ . await
1738+ . expect ( "Failed to check if rolling stock exists" ) ;
1739+ assert ! ( rolling_stock_exists) ;
1740+ }
1741+
1742+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
1743+ async fn delete_rolling_stock_as_admin_without_grant ( ) {
1744+ // GIVEN
1745+ let app = test_app ! ( ) . build ( ) ;
1746+ let db_pool = app. db_pool ( ) ;
1747+
1748+ let fast_rolling_stock =
1749+ create_fast_rolling_stock ( & mut db_pool. get_ok ( ) , "admin_deleted_rolling_stock" ) . await ;
1750+
1751+ // An admin holds no grant on the rolling stock but can still delete it
1752+ let admin = app
1753+ . user ( "admin" , "Admin" )
1754+ . with_roles ( [ Role :: Admin ] )
1755+ . create ( )
1756+ . await ;
1757+
1758+ // WHEN
1759+ app. delete ( format ! ( "/rolling_stock/{}" , fast_rolling_stock. id) . as_str ( ) )
1760+ . by_user ( admin. as_ref ( ) )
1761+ . await
1762+ . assert_status_no_content ( ) ;
1763+
1764+ // THEN
1765+ let rolling_stock_exists =
1766+ RollingStock :: exists ( & mut db_pool. get_ok ( ) , fast_rolling_stock. id )
1767+ . await
1768+ . expect ( "Failed to check if rolling stock exists" ) ;
1769+ assert ! ( !rolling_stock_exists) ;
1770+ }
1771+
1772+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
1773+ async fn delete_rolling_stock_with_skip_authz_without_user ( ) {
1774+ // GIVEN
1775+ let app = test_app ! ( ) . skip_authz ( ) . build ( ) ;
1776+ let db_pool = app. db_pool ( ) ;
1777+
1778+ let fast_rolling_stock =
1779+ create_fast_rolling_stock ( & mut db_pool. get_ok ( ) , "skip_authz_deleted_rolling_stock" )
1780+ . await ;
1781+
1782+ // WHEN (no grant set up, authorization is skipped)
1783+ app. delete ( format ! ( "/rolling_stock/{}" , fast_rolling_stock. id) . as_str ( ) )
1784+ . await
1785+ . assert_status_no_content ( ) ;
1786+
1787+ // THEN
1788+ let rolling_stock_exists =
1789+ RollingStock :: exists ( & mut db_pool. get_ok ( ) , fast_rolling_stock. id )
1790+ . await
1791+ . expect ( "Failed to check if rolling stock exists" ) ;
1792+ assert ! ( !rolling_stock_exists) ;
1793+ }
1794+
1795+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
1796+ async fn delete_rolling_stock_without_operational_studies_role ( ) {
1797+ // GIVEN
1798+ let app = test_app ! ( ) . build ( ) ;
1799+ let db_pool = app. db_pool ( ) ;
1800+
1801+ let fast_rolling_stock =
1802+ create_fast_rolling_stock ( & mut db_pool. get_ok ( ) , "missing_role_rolling_stock" ) . await ;
1803+
1804+ // A user with an owner grant but lacking the OperationalStudies role
1805+ let user = app
1806+ . user ( "owner" , "Owner" )
1807+ . with_rolling_stock_grant ( fast_rolling_stock. id , RollingStockGrant :: Owner )
1808+ . create ( )
1809+ . await ;
1810+
1811+ // WHEN
1812+ app. delete ( format ! ( "/rolling_stock/{}" , fast_rolling_stock. id) . as_str ( ) )
1813+ . by_user ( user. as_ref ( ) )
1814+ . await
1815+ . assert_status_forbidden ( ) ;
1816+
1817+ // THEN the rolling stock should still exist
1818+ let rolling_stock_exists =
1819+ RollingStock :: exists ( & mut db_pool. get_ok ( ) , fast_rolling_stock. id )
1820+ . await
1821+ . expect ( "Failed to check if rolling stock exists" ) ;
1822+ assert ! ( rolling_stock_exists) ;
1823+ }
1824+
16791825 #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
16801826 async fn delete_unlocked_used_rolling_stock_requires_force_flag ( ) {
16811827 // GIVEN
0 commit comments