@@ -15,7 +15,7 @@ use crate::{
1515 rbac:: {
1616 Users ,
1717 map:: { read_user_groups, roles, users} ,
18- role:: model:: { DefaultPrivilege , Role } ,
18+ role:: model:: { DefaultPrivilege , Role , RoleType } ,
1919 user:: { User , UserType } ,
2020 } ,
2121 utils:: { get_user_and_tenant_from_request, is_admin} ,
@@ -240,29 +240,43 @@ fn collect_tenant_api_keys(tenant_id: &Option<String>) -> Vec<User> {
240240 . collect ( )
241241}
242242
243- /// Validate that every role in the request exists in the tenant's role map .
243+ /// Validate that every requested role exists and can be assigned in this tenant.
244244/// Mirrors the check performed by the native-user CRUD endpoint.
245245fn validate_roles (
246246 role_names : & HashSet < String > ,
247247 tenant_id : & Option < String > ,
248248) -> Result < ( ) , ApiKeyError > {
249249 let tenant = tenant_or_default ( tenant_id) ;
250- let tenant_roles = roles ( ) ;
251- let missing: Vec < String > = role_names
252- . iter ( )
253- . filter ( |r| {
254- tenant_roles
255- . get ( tenant)
256- . map ( |m| !m. contains_key ( * r) )
257- . unwrap_or ( true )
250+ let missing: Vec < String > = {
251+ let tenant_roles = roles ( ) ;
252+ role_names
253+ . iter ( )
254+ . filter ( |r| {
255+ tenant_roles
256+ . get ( tenant)
257+ . map ( |m| !m. contains_key ( * r) )
258+ . unwrap_or ( true )
259+ } )
260+ . cloned ( )
261+ . collect ( )
262+ } ;
263+ if !missing. is_empty ( ) {
264+ return Err ( ApiKeyError :: Rbac ( RBACError :: RolesDoNotExist ( missing) ) ) ;
265+ }
266+ // Takes USERS, so no ROLES guard may be held here.
267+ if Users . any_role_held_by_protected_user ( role_names, tenant_id) {
268+ return Err ( ApiKeyError :: Rbac ( RBACError :: ProtectedRole ) ) ;
269+ }
270+ if let Some ( tenant_roles) = roles ( ) . get ( tenant)
271+ && role_names. iter ( ) . any ( |name| {
272+ tenant_roles. get ( name) . is_some_and ( |role| {
273+ role. role_type ( ) == & RoleType :: Internal || role. deny_super_admin ( )
274+ } )
258275 } )
259- . cloned ( )
260- . collect ( ) ;
261- if missing. is_empty ( ) {
262- Ok ( ( ) )
263- } else {
264- Err ( ApiKeyError :: Rbac ( RBACError :: RolesDoNotExist ( missing) ) )
276+ {
277+ return Err ( ApiKeyError :: Rbac ( RBACError :: ProtectedRole ) ) ;
265278 }
279+ Ok ( ( ) )
266280}
267281
268282/// POST /api/prism/v1/apikeys
@@ -276,8 +290,8 @@ pub async fn create_api_key(
276290
277291 let guard = UPDATE_LOCK . lock ( ) . await ;
278292
279- // Reject any role names that don't exist in this tenant while holding the
280- // update lock so a concurrent role update cannot invalidate the check.
293+ // Validate role assignments under the update lock so a concurrent role
294+ // update cannot invalidate the check.
281295 validate_roles ( & body. roles , & tenant_id) ?;
282296
283297 // Duplicate key-name detection inside the same tenant.
@@ -861,4 +875,161 @@ mod tests {
861875 assert_eq ! ( names( & unknown, None ) , [ "orphan-key" ] ) ;
862876 assert ! ( names( & unknown, reader( ) ) . is_empty( ) ) ;
863877 }
878+
879+ mod role_escalation {
880+ use actix_web:: { HttpMessage , ResponseError , test:: TestRequest } ;
881+ use chrono:: TimeDelta ;
882+
883+ use super :: * ;
884+ use crate :: rbac:: user:: Basic ;
885+
886+ const ADMIN_ROLE : & str = "tenant-admin" ;
887+ const READER_ROLE : & str = "reader" ;
888+ const INTERNAL_ROLE : & str = "internal" ;
889+ // Mirrors the internal role `rbac::map::init` adds to the default tenant.
890+ const SUPER_ADMIN_ROLE : & str = "super-admin" ;
891+ // A user-typed role holding SuperAdmin, e.g. from legacy metadata.
892+ const ELEVATED_ROLE : & str = "elevated" ;
893+
894+ /// A fresh tenant laid out like the default tenant after `map::init`.
895+ fn tenant ( ) -> String {
896+ let tenant = format ! ( "apikey-escalation-{}" , Ulid :: new( ) ) ;
897+ mut_roles ( ) . insert (
898+ tenant. clone ( ) ,
899+ HashMap :: from ( [
900+ (
901+ ADMIN_ROLE . to_owned ( ) ,
902+ Role :: create_user_role ( vec ! [ DefaultPrivilege :: Admin ] ) ,
903+ ) ,
904+ (
905+ READER_ROLE . to_owned ( ) ,
906+ Role :: create_user_role ( vec ! [ DefaultPrivilege :: Reader { resource: None } ] ) ,
907+ ) ,
908+ (
909+ INTERNAL_ROLE . to_owned ( ) ,
910+ Role :: create_internal_role ( vec ! [ DefaultPrivilege :: Admin ] ) ,
911+ ) ,
912+ (
913+ SUPER_ADMIN_ROLE . to_owned ( ) ,
914+ Role :: create_internal_role ( vec ! [ DefaultPrivilege :: SuperAdmin ] ) ,
915+ ) ,
916+ (
917+ ELEVATED_ROLE . to_owned ( ) ,
918+ Role :: create_user_role ( vec ! [ DefaultPrivilege :: SuperAdmin ] ) ,
919+ ) ,
920+ ] ) ,
921+ ) ;
922+ tenant
923+ }
924+
925+ fn role_set ( name : & str ) -> HashSet < String > {
926+ HashSet :: from ( [ name. to_owned ( ) ] )
927+ }
928+
929+ /// A request from a tenant admin (Admin, not SuperAdmin) with a live session.
930+ fn tenant_admin_request ( tenant : & str ) -> HttpRequest {
931+ let admin = User {
932+ ty : UserType :: Native ( Basic {
933+ username : format ! ( "admin-{}" , Ulid :: new( ) ) ,
934+ password_hash : String :: new ( ) ,
935+ email : None ,
936+ } ) ,
937+ roles : role_set ( ADMIN_ROLE ) ,
938+ user_groups : HashSet :: new ( ) ,
939+ tenant : Some ( tenant. to_owned ( ) ) ,
940+ protected : false ,
941+ } ;
942+ mut_users ( ) . insert ( admin. clone ( ) ) ;
943+ let session = SessionKey :: SessionId ( Ulid :: new ( ) ) ;
944+ Users . new_session ( & admin, session. clone ( ) , TimeDelta :: minutes ( 5 ) ) ;
945+ let req = TestRequest :: default ( ) . to_http_request ( ) ;
946+ req. extensions_mut ( ) . insert ( session) ;
947+ req
948+ }
949+
950+ #[ test]
951+ fn validate_roles_accepts_ordinary_role ( ) {
952+ let _serial = blocking_rbac_state ( ) ;
953+ let tenant = Some ( tenant ( ) ) ;
954+ assert ! ( validate_roles( & role_set( READER_ROLE ) , & tenant) . is_ok( ) ) ;
955+ }
956+
957+ #[ test]
958+ fn validate_roles_rejects_internal_role ( ) {
959+ let _serial = blocking_rbac_state ( ) ;
960+ let tenant = Some ( tenant ( ) ) ;
961+ let result = validate_roles ( & role_set ( INTERNAL_ROLE ) , & tenant) ;
962+ assert ! (
963+ matches!( result, Err ( ApiKeyError :: Rbac ( RBACError :: ProtectedRole ) ) ) ,
964+ "internal role must be rejected like the native-user path, got {result:?}"
965+ ) ;
966+ }
967+
968+ #[ test]
969+ fn validate_roles_rejects_super_admin_privilege_role ( ) {
970+ let _serial = blocking_rbac_state ( ) ;
971+ let tenant = Some ( tenant ( ) ) ;
972+ let result = validate_roles ( & role_set ( ELEVATED_ROLE ) , & tenant) ;
973+ assert ! (
974+ matches!( result, Err ( ApiKeyError :: Rbac ( RBACError :: ProtectedRole ) ) ) ,
975+ "role holding SuperAdmin must be rejected like the native-user path, got {result:?}"
976+ ) ;
977+ }
978+
979+ #[ test]
980+ fn validate_roles_rejects_role_held_by_protected_user ( ) {
981+ let _serial = blocking_rbac_state ( ) ;
982+ let tenant = tenant ( ) ;
983+ mut_users ( ) . insert ( User {
984+ ty : UserType :: Native ( Basic {
985+ username : format ! ( "protected-{}" , Ulid :: new( ) ) ,
986+ password_hash : String :: new ( ) ,
987+ email : None ,
988+ } ) ,
989+ roles : role_set ( READER_ROLE ) ,
990+ user_groups : HashSet :: new ( ) ,
991+ tenant : Some ( tenant. clone ( ) ) ,
992+ protected : true ,
993+ } ) ;
994+ let result = validate_roles ( & role_set ( READER_ROLE ) , & Some ( tenant) ) ;
995+ assert ! (
996+ matches!( result, Err ( ApiKeyError :: Rbac ( RBACError :: ProtectedRole ) ) ) ,
997+ "a protected user's role must be rejected like the native-user path, got {result:?}"
998+ ) ;
999+ }
1000+
1001+ #[ tokio:: test]
1002+ async fn tenant_admin_cannot_create_super_admin_api_key ( ) {
1003+ let _serial = rbac_state ( ) . await ;
1004+ let tenant = tenant ( ) ;
1005+ let req = tenant_admin_request ( & tenant) ;
1006+ // An existing key with the same name makes the vulnerable handler stop
1007+ // with 409 before it reaches storage, which tests cannot initialise.
1008+ let key_name = "escalation" . to_owned ( ) ;
1009+ mut_users ( ) . insert ( User :: new_api_key (
1010+ Ulid :: new ( ) ,
1011+ format ! ( "existing-{}" , Ulid :: new( ) ) ,
1012+ key_name. clone ( ) ,
1013+ HashSet :: new ( ) ,
1014+ "admin" . into ( ) ,
1015+ Some ( tenant. clone ( ) ) ,
1016+ ) ) ;
1017+
1018+ for role in [ SUPER_ADMIN_ROLE , ELEVATED_ROLE ] {
1019+ let body = CreateApiKeyRequest {
1020+ key_name : key_name. clone ( ) ,
1021+ roles : role_set ( role) ,
1022+ ttl_seconds : None ,
1023+ } ;
1024+ let Err ( err) = create_api_key ( req. clone ( ) , web:: Json ( body) ) . await else {
1025+ panic ! ( "API key with role {role} must be rejected" ) ;
1026+ } ;
1027+ assert ! (
1028+ matches!( err, ApiKeyError :: Rbac ( RBACError :: ProtectedRole ) ) ,
1029+ "role {role}: expected ProtectedRole, got {err:?}"
1030+ ) ;
1031+ assert_eq ! ( err. status_code( ) , StatusCode :: BAD_REQUEST , "role {role}" ) ;
1032+ }
1033+ }
1034+ }
8641035}
0 commit comments