From a72b0f1c55b63e4d69f05aaac0a3cd9ec52ba8c6 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Fri, 19 Sep 2025 00:38:26 -0500 Subject: [PATCH 1/3] Internal: Remove ADMIN vs SUPERADMIN redundancy - refs #6762 --- assets/vue/store/security.js | 10 +- assets/vue/store/securityStore.js | 18 +-- assets/vue/views/lp/LpList.vue | 2 +- config/packages/security.yaml | 15 +-- public/main/admin/user_list.php | 13 +- public/main/inc/ajax/user_manager.ajax.php | 31 ++--- public/main/inc/lib/api.lib.php | 38 ++---- public/main/inc/lib/usermanager.lib.php | 16 +-- .../DataFixtures/PermissionFixtures.php | 127 +++++++++--------- src/CoreBundle/Entity/User.php | 14 +- 10 files changed, 130 insertions(+), 154 deletions(-) diff --git a/assets/vue/store/security.js b/assets/vue/store/security.js index 1bb832102d4..38c73f46244 100644 --- a/assets/vue/store/security.js +++ b/assets/vue/store/security.js @@ -17,7 +17,10 @@ export default { return state.isAuthenticated }, isAdmin(state, getters) { - return getters.isAuthenticated && (getters.hasRole("ROLE_SUPER_ADMIN") || getters.hasRole("ROLE_ADMIN")) + return ( + getters.isAuthenticated && + (getters.hasRole("ROLE_ADMIN") || getters.hasRole("ROLE_GLOBAL_ADMIN")) + ) }, isCourseAdmin(state, getters) { if (getters.isAdmin) { @@ -34,8 +37,7 @@ export default { if (!getters.isAuthenticated) { return false } - - if (getters.hasRole("ROLE_SUPER_ADMIN") || getters.hasRole("ROLE_ADMIN")) { + if (getters.hasRole("ROLE_ADMIN") || getters.hasRole("ROLE_GLOBAL_ADMIN")) { return true } @@ -52,7 +54,7 @@ export default { }, hasRole(state) { return (role) => { - if (state.user.roles) { + if (state.user && state.user.roles) { return state.user.roles.indexOf(role) !== -1 } diff --git a/assets/vue/store/securityStore.js b/assets/vue/store/securityStore.js index c7a675b76d4..d35f539cd87 100644 --- a/assets/vue/store/securityStore.js +++ b/assets/vue/store/securityStore.js @@ -27,6 +27,7 @@ export const useSecurityStore = defineStore("security", () => { * @param {string} role */ const removeRole = (role) => { + if (!user.value || !user.value.roles) return const index = user.value.roles.indexOf(role) if (index > -1) { @@ -40,20 +41,21 @@ export const useSecurityStore = defineStore("security", () => { const isHRM = computed(() => hasRole.value("ROLE_HR")) - const isTeacher = computed(() => (isAdmin.value ? true : hasRole.value("ROLE_TEACHER"))) + const isAdmin = computed(() => hasRole.value("ROLE_ADMIN") || hasRole.value("ROLE_GLOBAL_ADMIN")) - const isCurrentTeacher = computed(() => (isAdmin.value ? true : hasRole.value("ROLE_CURRENT_COURSE_TEACHER"))) + const isTeacher = computed(() => isAdmin.value || hasRole.value("ROLE_TEACHER")) - const isCourseAdmin = computed(() => - isAdmin.value - ? true - : hasRole.value("ROLE_CURRENT_COURSE_SESSION_TEACHER") || hasRole.value("ROLE_CURRENT_COURSE_TEACHER"), + const isCurrentTeacher = computed(() => isAdmin.value || hasRole.value("ROLE_CURRENT_COURSE_TEACHER")) + + const isCourseAdmin = computed( + () => + isAdmin.value || + hasRole.value("ROLE_CURRENT_COURSE_SESSION_TEACHER") || + hasRole.value("ROLE_CURRENT_COURSE_TEACHER"), ) const isSessionAdmin = computed(() => hasRole.value("ROLE_SESSION_MANAGER")) - const isAdmin = computed(() => hasRole.value("ROLE_SUPER_ADMIN") || hasRole.value("ROLE_ADMIN")) - async function checkSession() { isLoading.value = true try { diff --git a/assets/vue/views/lp/LpList.vue b/assets/vue/views/lp/LpList.vue index becd6d56a3c..5f8fdf0b86e 100644 --- a/assets/vue/views/lp/LpList.vue +++ b/assets/vue/views/lp/LpList.vue @@ -333,7 +333,7 @@ const load = async () => { let allowed = await checkIsAllowedToEdit(true, true, true, false) const roles = securityStore.user?.roles ?? [] - if (!allowed && Array.isArray(roles) && (roles.includes("ROLE_ADMIN") || roles.includes("ROLE_SUPER_ADMIN"))) { + if (!allowed && Array.isArray(roles) && (roles.includes("ROLE_ADMIN") || roles.includes("ROLE_GLOBAL_ADMIN"))) { allowed = true } rawCanEdit.value = !!allowed diff --git a/config/packages/security.yaml b/config/packages/security.yaml index b541b577ed0..943bba02374 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -42,18 +42,17 @@ security: - ROLE_CURRENT_COURSE_TEACHER - ROLE_CURRENT_COURSE_SESSION_TEACHER - ROLE_CURRENT_COURSE_GROUP_TEACHER - ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] # Admin that can log in as another user. - ROLE_GLOBAL_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] # The user that installed the platform. + - ROLE_ALLOWED_TO_SWITCH + ROLE_GLOBAL_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] ROLE_TEACHER: [ROLE_STUDENT] ROLE_HR: [ROLE_TEACHER, ROLE_ALLOWED_TO_SWITCH] ROLE_QUESTION_MANAGER: [ROLE_STUDENT] ROLE_SESSION_MANAGER: [ROLE_STUDENT, ROLE_ALLOWED_TO_SWITCH] ROLE_STUDENT_BOSS: [ROLE_STUDENT] ROLE_INVITEE: [ROLE_STUDENT] - - ROLE_CURRENT_COURSE_STUDENT: [ROLE_CURRENT_COURSE_STUDENT] # Set in the CidReqListener - ROLE_CURRENT_COURSE_TEACHER: [ROLE_CURRENT_COURSE_TEACHER, ROLE_CURRENT_COURSE_STUDENT] # Set in the course listener - ROLE_CURRENT_COURSE_GROUP_STUDENT: [ROLE_CURRENT_COURSE_GROUP_STUDENT] # Set in the CidReqListener + ROLE_CURRENT_COURSE_STUDENT: [ROLE_CURRENT_COURSE_STUDENT] + ROLE_CURRENT_COURSE_TEACHER: [ROLE_CURRENT_COURSE_TEACHER, ROLE_CURRENT_COURSE_STUDENT] + ROLE_CURRENT_COURSE_GROUP_STUDENT: [ROLE_CURRENT_COURSE_GROUP_STUDENT] ROLE_CURRENT_COURSE_GROUP_TEACHER: [ROLE_CURRENT_COURSE_GROUP_TEACHER, ROLE_CURRENT_COURSE_GROUP_STUDENT] ROLE_CURRENT_COURSE_SESSION_STUDENT: [ROLE_CURRENT_COURSE_SESSION_STUDENT] ROLE_CURRENT_COURSE_SESSION_TEACHER: [ROLE_CURRENT_COURSE_SESSION_STUDENT, ROLE_CURRENT_COURSE_SESSION_TEACHER] @@ -124,5 +123,5 @@ security: access_control: - { path: ^/login/token/check, roles: PUBLIC_ACCESS } - - {path: ^/login, roles: PUBLIC_ACCESS} - - {path: ^/api/authentication_token, roles: PUBLIC_ACCESS} + - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/api/authentication_token, roles: PUBLIC_ACCESS } diff --git a/public/main/admin/user_list.php b/public/main/admin/user_list.php index 49c2063eef5..a1ee4499f8c 100644 --- a/public/main/admin/user_list.php +++ b/public/main/admin/user_list.php @@ -264,7 +264,7 @@ function prepare_user_sql_query(bool $getCount, bool $showDeletedUsers = false): } $mappedStatuses = array_values(array_unique($mappedStatuses)); - $adminVariants = ['ROLE_PLATFORM_ADMIN','PLATFORM_ADMIN','ROLE_SUPER_ADMIN','SUPER_ADMIN','ROLE_GLOBAL_ADMIN','GLOBAL_ADMIN','ROLE_ADMIN','ADMIN']; + $adminVariants = ['ROLE_PLATFORM_ADMIN','PLATFORM_ADMIN','ROLE_GLOBAL_ADMIN','GLOBAL_ADMIN','ROLE_ADMIN','ADMIN']; $needsAdminLeftJoin = (bool) array_intersect($roles, $adminVariants); if ($needsAdminLeftJoin) { $sql .= " LEFT JOIN $admin_table a ON (a.user_id = u.id) "; @@ -467,9 +467,7 @@ function get_user_data(int $from, int $number_of_items, int $column, string $dir title="'.api_get_person_name($user[2], $user[3]).'" />'; if (1 == $user[7] && !empty($user['exp'])) { - // check expiration date $expiration_time = api_strtotime($user['exp']); - // if expiration date is passed, store a special value for active field if ($expiration_time < $t) { $user[7] = '-1'; } @@ -477,15 +475,15 @@ function get_user_data(int $from, int $number_of_items, int $column, string $dir // forget about the expiration date field $users[] = [ - $user[0], // id + $user[0], $photo, $user[1], $user[2], $user[3], - $user[4], // username - $user[5], // email + $user[4], + $user[5], $user[0], - $user[7], // active + $user[7], api_get_local_time($user[8]), api_get_local_time($user[9], null, null, true), $user[0], @@ -598,7 +596,6 @@ function modify_filter($user_id, $url_params, $row): string $userRoles = $userEntity ? $userEntity->getRoles() : []; $isAdminByRole = in_array('ROLE_PLATFORM_ADMIN', $userRoles, true) - || in_array('ROLE_SUPER_ADMIN', $userRoles, true) || in_array('ROLE_GLOBAL_ADMIN', $userRoles, true) || in_array('ROLE_ADMIN', $userRoles, true); diff --git a/public/main/inc/ajax/user_manager.ajax.php b/public/main/inc/ajax/user_manager.ajax.php index 28ef56edaec..bf344f4db16 100644 --- a/public/main/inc/ajax/user_manager.ajax.php +++ b/public/main/inc/ajax/user_manager.ajax.php @@ -72,8 +72,6 @@ .$user_info['official_code']; if ($isAnonymous) { - // Only allow anonymous users to see user popup if the popup user - // is a teacher (which might be necessary to illustrate a course) if (COURSEMANAGER === (int) $user_info['status']) { if ($user_info['status'] === COURSEMANAGER) { echo $userData; @@ -218,22 +216,21 @@ ); $body = get_lang('Dear')." ".stripslashes($recipientName).",\n\n"; $body .= sprintf( - get_lang('Your account on %s has just been approved by one of our administrators.'), - api_get_setting('siteName') - )."\n"; + get_lang('Your account on %s has just been approved by one of our administrators.'), + api_get_setting('siteName') + )."\n"; $body .= sprintf( - get_lang('You can now login at %s using the login and the password you have provided.'), - api_get_path(WEB_PATH) - ).",\n\n"; + get_lang('You can now login at %s using the login and the password you have provided.'), + api_get_path(WEB_PATH) + ).",\n\n"; $body .= get_lang('Have fun,')."\n\n"; - //$body.=get_lang('In case of trouble, contact us.'). "\n\n". get_lang('Sincerely'); $body .= api_get_person_name( - api_get_setting('administratorName'), - api_get_setting('administratorSurname') - )."\n". - get_lang('Administrator')." ". - api_get_setting('siteName')."\nT. ".api_get_setting('administratorTelephone')."\n". - get_lang('E-mail')." : ".api_get_setting('emailAdministrator'); + api_get_setting('administratorName'), + api_get_setting('administratorSurname') + )."\n". + get_lang('Administrator')." ". + api_get_setting('siteName')."\nT. ".api_get_setting('administratorTelephone')."\n". + get_lang('E-mail')." : ".api_get_setting('emailAdministrator'); MessageManager::send_message_simple( $user_id, @@ -281,7 +278,7 @@ $urlId = api_get_current_access_url_id(); - $roleList = ['ROLE_TEACHER', 'ROLE_ADMIN', 'ROLE_SUPER_ADMIN']; + $roleList = ['ROLE_TEACHER', 'ROLE_ADMIN', 'ROLE_GLOBAL_ADMIN']; $users = Container::getUserRepository()->findByRoleList( $roleList, @@ -312,7 +309,7 @@ $urlId = api_get_current_access_url_id(); - $roleList = ['ROLE_STUDENT', 'ROLE_TEACHER', 'ROLE_ADMIN', 'ROLE_SUPER_ADMIN']; + $roleList = ['ROLE_STUDENT', 'ROLE_TEACHER', 'ROLE_ADMIN', 'ROLE_GLOBAL_ADMIN']; $users = Container::getUserRepository()->findByRoleList( $roleList, diff --git a/public/main/inc/lib/api.lib.php b/public/main/inc/lib/api.lib.php index 6efa8d82b98..be0a82872e4 100644 --- a/public/main/inc/lib/api.lib.php +++ b/public/main/inc/lib/api.lib.php @@ -6376,9 +6376,8 @@ function api_get_roles(): array $codes = Container::$container ->get(\Chamilo\CoreBundle\Helpers\PermissionHelper::class) - ->getUserRoles(); // list of role codes from DB + ->getUserRoles(); - // Built-in labels fallbacks. DB codes are used as keys. $labels = [ 'ROLE_STUDENT' => get_lang('Learner'), 'STUDENT' => get_lang('Learner'), @@ -6398,8 +6397,6 @@ function api_get_roles(): array 'ADMIN' => get_lang('Admin'), 'ROLE_PLATFORM_ADMIN' => get_lang('Administrator'), 'PLATFORM_ADMIN' => get_lang('Administrator'), - 'ROLE_SUPER_ADMIN' => get_lang('Super admin'), - 'SUPER_ADMIN' => get_lang('Super admin'), 'ROLE_GLOBAL_ADMIN' => get_lang('Global admin'), 'GLOBAL_ADMIN' => get_lang('Global admin'), 'ROLE_ANONYMOUS' => 'Anonymous', @@ -6776,7 +6773,6 @@ function api_drh_can_access_all_session_content() function api_can_login_as($loginAsUserId, $userId = null) { $loginAsUserId = (int) $loginAsUserId; - if (empty($loginAsUserId)) { return false; } @@ -6789,9 +6785,8 @@ function api_can_login_as($loginAsUserId, $userId = null) return false; } - // Check if the user to login is an admin + // If target is an admin, only global admins can login to admin accounts if (api_is_platform_admin_by_id($loginAsUserId)) { - // Only super admins can login to admin accounts if (!api_global_admin_can_edit_admin($loginAsUserId)) { return false; } @@ -6802,25 +6797,18 @@ function api_can_login_as($loginAsUserId, $userId = null) $isDrh = function () use ($loginAsUserId) { if (api_is_drh()) { if (api_drh_can_access_all_session_content()) { - $users = SessionManager::getAllUsersFromCoursesFromAllSessionFromStatus( - 'drh_all', - api_get_user_id() - ); - $userList = []; + $users = SessionManager::getAllUsersFromCoursesFromAllSessionFromStatus('drh_all', api_get_user_id()); + $userIds = []; if (is_array($users)) { foreach ($users as $user) { - $userList[] = $user['id']; + $userIds[] = $user['id']; } } - if (in_array($loginAsUserId, $userList)) { - return true; - } - } else { - if (api_is_drh() && - UserManager::is_user_followed_by_drh($loginAsUserId, api_get_user_id()) - ) { - return true; - } + return in_array($loginAsUserId, $userIds); + } + + if (UserManager::is_user_followed_by_drh($loginAsUserId, api_get_user_id())) { + return true; } } @@ -6833,9 +6821,9 @@ function api_can_login_as($loginAsUserId, $userId = null) $loginAsStatusForSessionAdmins[] = COURSEMANAGER; } - return api_is_platform_admin() || - (api_is_session_admin() && in_array($userInfo['status'], $loginAsStatusForSessionAdmins)) || - $isDrh(); + return api_is_platform_admin() // local admins can login as (except into other admins unless allowed above) + || (api_is_session_admin() && in_array($userInfo['status'], $loginAsStatusForSessionAdmins)) + || $isDrh(); } /** diff --git a/public/main/inc/lib/usermanager.lib.php b/public/main/inc/lib/usermanager.lib.php index 4c8b6194a6c..734e8acc747 100644 --- a/public/main/inc/lib/usermanager.lib.php +++ b/public/main/inc/lib/usermanager.lib.php @@ -5465,20 +5465,18 @@ public static function loginAsUser($userId, $checkIfUserCanLoginAs = true) } if ($userId) { - $logInfo = [ + Event::registerLog([ 'tool' => 'logout', 'tool_id' => 0, 'tool_id_detail' => 0, 'action' => '', 'info' => 'Change user (login as)', - ]; - Event::registerLog($logInfo); + ]); - // Logout the current user + // Logout current user self::loginDelete(api_get_user_id()); - return true; - + // Reset and set new session data Session::erase('_user'); Session::erase('is_platformAdmin'); Session::erase('is_allowedCreateCourse'); @@ -5501,13 +5499,13 @@ public static function loginAsUser($userId, $checkIfUserCanLoginAs = true) Session::write('is_allowedCreateCourse', 1 == $userInfo['status']); // will be useful later to know if the user is actually an admin or not (example reporting) Session::write('login_as', true); - $logInfo = [ + + Event::registerLog([ 'tool' => 'login', 'tool_id' => 0, 'tool_id_detail' => 0, 'info' => $userId, - ]; - Event::registerLog($logInfo); + ]); return true; } diff --git a/src/CoreBundle/DataFixtures/PermissionFixtures.php b/src/CoreBundle/DataFixtures/PermissionFixtures.php index 2b68ede42f5..ee5601c8182 100644 --- a/src/CoreBundle/DataFixtures/PermissionFixtures.php +++ b/src/CoreBundle/DataFixtures/PermissionFixtures.php @@ -175,7 +175,6 @@ public static function getRoles(): array 'ROLE_STUDENT' => 'STU', 'ROLE_TEACHER' => 'TEA', 'ROLE_ADMIN' => 'ADM', - 'ROLE_SUPER_ADMIN' => 'SUA', 'ROLE_GLOBAL_ADMIN' => 'GLO', 'ROLE_HR' => 'HRM', 'ROLE_QUESTION_MANAGER' => 'QBM', @@ -187,95 +186,95 @@ public static function getRoles(): array public static function getPermissionsMapping(): array { return [ - 'analytics:view' => ['INV', 'STU', 'TEA', 'ADM', 'SUA', 'GLO', 'HRM', 'QBM', 'SSM', 'STB'], - 'analytics:viewassigned' => ['TEA', 'ADM', 'SUA', 'GLO', 'HRM', 'SSM', 'STB'], - 'analytics:viewall' => ['ADM', 'SUA', 'GLO', 'SSM', 'STB'], + 'analytics:view' => ['INV', 'STU', 'TEA', 'ADM', 'GLO', 'HRM', 'QBM', 'SSM', 'STB'], + 'analytics:viewassigned' => ['TEA', 'ADM', 'GLO', 'HRM', 'SSM', 'STB'], + 'analytics:viewall' => ['ADM', 'GLO', 'SSM', 'STB'], 'assignment:create' => ['TEA'], - 'assignment:delete' => ['TEA', 'ADM', 'SUA', 'GLO'], - 'assignment:edit' => ['TEA', 'ADM', 'SUA', 'GLO'], + 'assignment:delete' => ['TEA', 'ADM', 'GLO'], + 'assignment:edit' => ['TEA', 'ADM', 'GLO'], 'assignment:grade' => ['TEA'], 'assignment:submit' => ['STU'], - 'assignment:view' => ['INV', 'STU', 'TEA', 'ADM', 'SUA', 'GLO', 'HRM', 'SSM', 'STB'], - 'backup:backup' => ['TEA', 'ADM', 'SUA', 'GLO', 'SSM'], - 'backup:copy' => ['TEA', 'ADM', 'SUA', 'GLO', 'SSM'], - 'backup:restore' => ['TEA', 'ADM', 'SUA', 'GLO', 'SSM'], - 'badge:configurecriteria' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'badge:create' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'badge:edit' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'badge:delete' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'badge:view' => ['INV', 'STU', 'TEA', 'ADM', 'SUA', 'GLO', 'SSM', 'STB'], - 'calendar:create' => ['ADM', 'SUA', 'GLO'], - 'calendar:edit' => ['ADM', 'SUA', 'GLO'], - 'calendar:delete' => ['ADM', 'SUA', 'GLO'], - 'catalogue:view' => ['INV', 'STU', 'TEA', 'ADM', 'SUA', 'GLO', 'HRM', 'QBM', 'SSM', 'STB'], + 'assignment:view' => ['INV', 'STU', 'TEA', 'ADM', 'GLO', 'HRM', 'SSM', 'STB'], + 'backup:backup' => ['TEA', 'ADM', 'GLO', 'SSM'], + 'backup:copy' => ['TEA', 'ADM', 'GLO', 'SSM'], + 'backup:restore' => ['TEA', 'ADM', 'GLO', 'SSM'], + 'badge:configurecriteria' => ['ADM', 'GLO', 'SSM'], + 'badge:create' => ['ADM', 'GLO', 'SSM'], + 'badge:edit' => ['ADM', 'GLO', 'SSM'], + 'badge:delete' => ['ADM', 'GLO', 'SSM'], + 'badge:view' => ['INV', 'STU', 'TEA', 'ADM', 'GLO', 'SSM', 'STB'], + 'calendar:create' => ['ADM', 'GLO'], + 'calendar:edit' => ['ADM', 'GLO'], + 'calendar:delete' => ['ADM', 'GLO'], + 'catalogue:view' => ['INV', 'STU', 'TEA', 'ADM', 'GLO', 'HRM', 'QBM', 'SSM', 'STB'], 'certificate:create' => ['TEA', 'SSM'], 'certificate:delete' => ['TEA', 'SSM'], 'certificate:edit' => ['TEA', 'SSM'], 'certificate:generate' => ['STU', 'TEA', 'SSM'], 'certificate:generateall' => ['TEA', 'HRM', 'SSM'], 'certificate:viewall' => ['TEA', 'HRM', 'SSM', 'STB'], - 'class:assigncourse' => ['TEA', 'ADM', 'SUA', 'GLO'], - 'class:assignsession' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'class:assignuser' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'class:create' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'class:delete' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'class:edit' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'class:view' => ['STU', 'TEA', 'ADM', 'SUA', 'GLO', 'SSM'], - 'cms:create' => ['ADM', 'SUA', 'GLO'], - 'cms:delete' => ['ADM', 'SUA', 'GLO'], - 'cms:edit' => ['ADM', 'SUA', 'GLO'], - 'course:create' => ['TEA', 'ADM', 'SUA', 'GLO', 'SSM'], - 'course:delete' => ['TEA', 'ADM', 'SUA', 'GLO'], - 'course:downloadcoursecontent' => ['TEA', 'ADM', 'SUA', 'GLO', 'SSM'], + 'class:assigncourse' => ['TEA', 'ADM', 'GLO'], + 'class:assignsession' => ['ADM', 'GLO', 'SSM'], + 'class:assignuser' => ['ADM', 'GLO', 'SSM'], + 'class:create' => ['ADM', 'GLO', 'SSM'], + 'class:delete' => ['ADM', 'GLO', 'SSM'], + 'class:edit' => ['ADM', 'GLO', 'SSM'], + 'class:view' => ['STU', 'TEA', 'ADM', 'GLO', 'SSM'], + 'cms:create' => ['ADM', 'GLO'], + 'cms:delete' => ['ADM', 'GLO'], + 'cms:edit' => ['ADM', 'GLO'], + 'course:create' => ['TEA', 'ADM', 'GLO', 'SSM'], + 'course:delete' => ['TEA', 'ADM', 'GLO'], + 'course:downloadcoursecontent' => ['TEA', 'ADM', 'GLO', 'SSM'], 'course:edit' => ['TEA', 'SSM'], - 'course:editall' => ['ADM', 'SUA', 'GLO'], - 'plugin:manage' => ['ADM', 'SUA', 'GLO'], + 'course:editall' => ['ADM', 'GLO'], + 'plugin:manage' => ['ADM', 'GLO'], 'quiz:create' => ['TEA', 'QBM'], 'quiz:delete' => ['TEA', 'QBM'], 'quiz:edit' => ['TEA', 'QBM'], 'quiz:grade' => ['TEA'], 'quiz:viewliveresults' => ['TEA', 'SSM'], - 'quiz:managequestionbank' => ['ADM', 'SUA', 'GLO', 'QBM'], - 'role:create' => ['ADM', 'SUA', 'GLO'], - 'role:managepermissions' => ['ADM', 'SUA', 'GLO'], - 'session:create' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'session:delete' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'session:edit' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'session:editall' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'session:assigncourse' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'site:editsettings' => ['ADM', 'SUA', 'GLO'], - 'site:maintenanceaccess' => ['ADM', 'SUA', 'GLO'], - 'skill:coursecompetencymanage' => ['TEA', 'ADM', 'SUA', 'GLO', 'HRM'], - 'skill:usercompetencyreview' => ['STU', 'TEA', 'ADM', 'SUA', 'GLO'], - 'skill:assign' => ['ADM', 'SUA', 'GLO'], + 'quiz:managequestionbank' => ['ADM', 'GLO', 'QBM'], + 'role:create' => ['ADM', 'GLO'], + 'role:managepermissions' => ['ADM', 'GLO'], + 'session:create' => ['ADM', 'GLO', 'SSM'], + 'session:delete' => ['ADM', 'GLO', 'SSM'], + 'session:edit' => ['ADM', 'GLO', 'SSM'], + 'session:editall' => ['ADM', 'GLO', 'SSM'], + 'session:assigncourse' => ['ADM', 'GLO', 'SSM'], + 'site:editsettings' => ['ADM', 'GLO'], + 'site:maintenanceaccess' => ['ADM', 'GLO'], + 'skill:coursecompetencymanage' => ['TEA', 'ADM', 'GLO', 'HRM'], + 'skill:usercompetencyreview' => ['STU', 'TEA', 'ADM', 'GLO'], + 'skill:assign' => ['ADM', 'GLO'], 'skill:create' => ['GLO'], 'skill:delete' => ['GLO'], 'skill:edit' => ['GLO'], - 'skill:view' => ['ADM', 'SUA', 'GLO', 'SSM', 'STB'], - 'skill:viewall' => ['ADM', 'SUA', 'GLO', 'SSM', 'STB'], + 'skill:view' => ['ADM', 'GLO', 'SSM', 'STB'], + 'skill:viewall' => ['ADM', 'GLO', 'SSM', 'STB'], 'survey:create' => ['TEA'], 'survey:delete' => ['TEA'], 'survey:edit' => ['TEA'], - 'survey:submit' => ['INV', 'STU', 'TEA', 'ADM', 'SUA', 'GLO', 'SSM', 'STB'], + 'survey:submit' => ['INV', 'STU', 'TEA', 'ADM', 'GLO', 'SSM', 'STB'], 'survey:viewresults' => ['TEA', 'HRM', 'SSM', 'STB'], - 'ticket:comment' => ['STU', 'TEA', 'ADM', 'SUA', 'GLO', 'HRM', 'QBM', 'SSM', 'STB'], - 'ticket:manage' => ['ADM', 'SUA', 'GLO'], - 'ticket:report' => ['STU', 'TEA', 'ADM', 'SUA', 'GLO', 'HRM', 'QBM', 'SSM', 'STB'], - 'ticket:seeissues' => ['STU', 'TEA', 'ADM', 'SUA', 'GLO', 'SSM', 'STB'], - 'ticket:viewallissues' => ['ADM', 'SUA', 'GLO', 'SSM', 'STB'], - 'tool:editvisibility' => ['TEA', 'ADM', 'SUA', 'GLO', 'SSM'], + 'ticket:comment' => ['STU', 'TEA', 'ADM', 'GLO', 'HRM', 'QBM', 'SSM', 'STB'], + 'ticket:manage' => ['ADM', 'GLO'], + 'ticket:report' => ['STU', 'TEA', 'ADM', 'GLO', 'HRM', 'QBM', 'SSM', 'STB'], + 'ticket:seeissues' => ['STU', 'TEA', 'ADM', 'GLO', 'SSM', 'STB'], + 'ticket:viewallissues' => ['ADM', 'GLO', 'SSM', 'STB'], + 'tool:editvisibility' => ['TEA', 'ADM', 'GLO', 'SSM'], 'url:manage' => ['GLO'], 'url:assignclass' => ['GLO'], 'url:assigncourse' => ['GLO'], 'url:assignuser' => ['GLO'], - 'user:assigncourse' => ['TEA', 'ADM', 'SUA', 'GLO'], - 'user:assignsession' => ['ADM', 'SUA', 'GLO', 'SSM'], - 'user:create' => ['ADM', 'SUA', 'GLO'], - 'user:delete' => ['ADM', 'SUA', 'GLO'], - 'user:edit' => ['ADM', 'SUA', 'GLO'], - 'user:editrole' => ['ADM', 'SUA', 'GLO'], - 'user:loginas' => ['SUA', 'GLO'], - 'course:editsettings' => ['TEA', 'ADM', 'SUA', 'GLO'], + 'user:assigncourse' => ['TEA', 'ADM', 'GLO'], + 'user:assignsession' => ['ADM', 'GLO', 'SSM'], + 'user:create' => ['ADM', 'GLO'], + 'user:delete' => ['ADM', 'GLO'], + 'user:edit' => ['ADM', 'GLO'], + 'user:editrole' => ['ADM', 'GLO'], + 'user:loginas' => ['ADM', 'GLO'], + 'course:editsettings' => ['TEA', 'ADM', 'GLO'], ]; } } diff --git a/src/CoreBundle/Entity/User.php b/src/CoreBundle/Entity/User.php index c81fb9f1e42..1867817f80a 100644 --- a/src/CoreBundle/Entity/User.php +++ b/src/CoreBundle/Entity/User.php @@ -155,13 +155,6 @@ class User implements UserInterface, EquatableInterface, ResourceInterface, Reso */ public const ROLE_FALLBACK = 99; - /*public const COURSE_MANAGER = 1; - public const TEACHER = 1; - public const SESSION_ADMIN = 3; - public const DRH = 4; - public const STUDENT = 5; - public const ANONYMOUS = 6;*/ - // User active field constants public const ACTIVE = 1; public const INACTIVE = 0; @@ -1251,7 +1244,7 @@ public function setConfirmationToken(string $confirmationToken): self public function isPasswordRequestNonExpired(int $ttl): bool { return $this->getPasswordRequestedAt() instanceof DateTime && $this->getPasswordRequestedAt()->getTimestamp( - ) + $ttl > time(); + ) + $ttl > time(); } public function getPasswordRequestedAt(): ?DateTime @@ -1669,7 +1662,8 @@ public function eraseCredentials(): void */ public function isSuperAdmin(): bool { - return $this->hasRole('ROLE_SUPER_ADMIN'); + // Treat "global admin" as super-admin + return $this->hasRole('ROLE_GLOBAL_ADMIN'); } public function hasRole(string $role): bool @@ -2357,7 +2351,7 @@ public function isFriendWithMeByRelationType(self $friend, int $relationType): b return $this ->getFriendsWithMeByRelationType($relationType) ->exists(fn (int $index, UserRelUser $userRelUser) => $userRelUser->getUser() === $friend) - ; + ; } /** From 4821a0e9e06ef1e4f0893c27bdf67b31af0931c5 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Fri, 19 Sep 2025 00:41:28 -0500 Subject: [PATCH 2/3] Internal: Add migration to remove ADMIN vs SUPERADMIN redundancy - refs #6762 --- .../Schema/V200/Version20250918163700.php | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 src/CoreBundle/Migrations/Schema/V200/Version20250918163700.php diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20250918163700.php b/src/CoreBundle/Migrations/Schema/V200/Version20250918163700.php new file mode 100644 index 00000000000..dc85608697d --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20250918163700.php @@ -0,0 +1,282 @@ +connection; + $conn->beginTransaction(); + + try { + $users = $conn->fetchAllAssociative("SELECT id, roles FROM `user`"); + $updateStmt = $conn->prepare("UPDATE `user` SET roles = :roles WHERE id = :id"); + + foreach ($users as $u) { + $id = (int) $u['id']; + $raw = $u['roles']; + + [$roles, $format] = $this->decodeRoles($raw); + if (empty($roles)) { + continue; + } + + $upper = array_values(array_unique(array_map( + fn ($r) => strtoupper(trim((string) $r)), + $roles + ))); + + $changed = false; + + if (in_array('ROLE_SUPER_ADMIN', $upper, true)) { + if (!in_array('ROLE_ADMIN', $upper, true)) { + $upper[] = 'ROLE_ADMIN'; + } + $upper = array_values(array_diff($upper, ['ROLE_SUPER_ADMIN'])); + $changed = true; + } + + if ($changed) { + $encoded = $this->encodeRoles($upper, $format); + $updateStmt->executeStatement(['roles' => $encoded, 'id' => $id]); + } + } + + $adminRoleId = $this->ensureAdminRoleId(); + $permId = $this->ensurePermissionExists('user:loginas', 'Login as user', 'Login as another user'); + $this->ensurePermissionRelRole($permId, $adminRoleId); + + $superAdminId = $this->getRoleIdByCodes(['SUPER_ADMIN', 'SUA']); + if ($superAdminId !== null) { + $conn->executeStatement( + "DELETE FROM permission_rel_role WHERE role_id = :rid", + ['rid' => $superAdminId] + ); + $conn->executeStatement("DELETE FROM role WHERE id = :rid", ['rid' => $superAdminId]); + } + + $conn->commit(); + } catch (\Throwable $e) { + $conn->rollBack(); + throw $e; + } + } + + public function down(Schema $schema): void + { + $conn = $this->connection; + $conn->beginTransaction(); + + try { + $superAdminId = $this->ensureSuperAdminRoleId(); + + $permId = $this->ensurePermissionExists('user:loginas', 'Login as user', 'Login as another user'); + $this->ensurePermissionRelRole($permId, $superAdminId); + + $adminRoleId = $this->getRoleIdByCodes(['ADMIN', 'ADM']); + if ($adminRoleId !== null) { + $conn->executeStatement( + "DELETE FROM permission_rel_role WHERE permission_id = :pid AND role_id = :rid", + ['pid' => $permId, 'rid' => $adminRoleId] + ); + } + + $users = $conn->fetchAllAssociative("SELECT id, roles FROM `user`"); + $updateStmt = $conn->prepare("UPDATE `user` SET roles = :roles WHERE id = :id"); + + foreach ($users as $u) { + $id = (int) $u['id']; + $raw = $u['roles']; + + [$roles, $format] = $this->decodeRoles($raw); + $upper = array_values(array_unique(array_map( + fn ($r) => strtoupper(trim((string) $r)), + $roles + ))); + + if (in_array('ROLE_ADMIN', $upper, true) && !in_array('ROLE_SUPER_ADMIN', $upper, true)) { + $upper[] = 'ROLE_SUPER_ADMIN'; + $encoded = $this->encodeRoles($upper, $format); + $updateStmt->executeStatement(['roles' => $encoded, 'id' => $id]); + } + } + + $conn->commit(); + } catch (\Throwable $e) { + $conn->rollBack(); + throw $e; + } + } + + private function decodeRoles($raw): array + { + if ($raw === null || $raw === '') { + return [[], 'empty']; + } + + if (is_array($raw)) { + return [$raw, 'array']; + } + + $s = (string) $raw; + + $json = json_decode($s, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($json)) { + return [$json, 'json']; + } + + $unser = @unserialize($s); + if ($unser !== false && is_array($unser)) { + return [$unser, 'php']; + } + + $parts = preg_split('/[,\s]+/', $s, -1, PREG_SPLIT_NO_EMPTY); + return [$parts ?: [], 'text']; + } + + private function encodeRoles(array $roles, string $format): string + { + return match ($format) { + 'json', 'array', 'empty' => json_encode(array_values($roles), JSON_UNESCAPED_UNICODE), + 'php' => serialize(array_values($roles)), + 'text' => implode(',', array_values($roles)), + default => json_encode(array_values($roles), JSON_UNESCAPED_UNICODE), + }; + } + + private function getRoleIdByCodes(array $codes): ?int + { + $placeholders = implode(',', array_fill(0, count($codes), '?')); + $row = $this->connection->fetchAssociative( + "SELECT id FROM role WHERE code IN ($placeholders) LIMIT 1", + $codes + ); + + return $row ? (int) $row['id'] : null; + } + + private function ensureAdminRoleId(): int + { + $id = $this->getRoleIdByCodes(['ADMIN', 'ADM']); + if ($id !== null) { + return $id; + } + return $this->createRoleRow('ADMIN', 'Administrator', 'Platform administrator'); + } + + private function ensureSuperAdminRoleId(): int + { + $id = $this->getRoleIdByCodes(['SUPER_ADMIN', 'SUA']); + if ($id !== null) { + return $id; + } + return $this->createRoleRow('SUPER_ADMIN', 'Super Administrator', 'Full platform administrator'); + } + + private function createRoleRow(string $code, string $title, ?string $description = null): int + { + $conn = $this->connection; + $sm = $conn->createSchemaManager(); + + $cols = []; + foreach ($sm->listTableColumns('role') as $c) { + $cols[strtoupper($c->getName())] = $c; + } + + $now = (new \DateTime())->format('Y-m-d H:i:s'); + + $data = [ + 'code' => $code, + 'title' => $title, + ]; + + if ($description !== null && isset($cols['DESCRIPTION'])) { + $data['description'] = $description; + } + + if (isset($cols['CONSTANT_VALUE'])) { + $type = strtolower($cols['CONSTANT_VALUE']->getType()->getName()); + $isNumeric = in_array($type, ['integer', 'smallint', 'bigint', 'decimal', 'float', 'boolean'], true); + $data['constant_value'] = $isNumeric ? 0 : ('ROLE_' . strtoupper($code)); + } + + if (isset($cols['SYSTEM_ROLE'])) { + $data['system_role'] = 1; + } + + if (isset($cols['CREATED_AT'])) { + $data['created_at'] = $now; + } + if (isset($cols['UPDATED_AT'])) { + $data['updated_at'] = $now; + } + + $fields = array_keys($data); + $placeholders = array_map(fn ($f) => ':' . $f, $fields); + $sql = 'INSERT INTO role (' . implode(',', $fields) . ') VALUES (' . implode(',', $placeholders) . ')'; + $conn->executeStatement($sql, $data); + + return (int) $conn->lastInsertId(); + } + + private function ensurePermissionExists(string $slug, string $title, string $desc): int + { + $conn = $this->connection; + + $row = $conn->fetchAssociative("SELECT id FROM permission WHERE slug = :s", ['s' => $slug]); + if ($row) { + return (int) $row['id']; + } + + $sm = $conn->createSchemaManager(); + $cols = array_change_key_case( + array_map(fn ($c) => $c->getName(), $sm->listTableColumns('permission')), + CASE_UPPER + ); + + $now = (new \DateTime())->format('Y-m-d H:i:s'); + + $data = ['slug' => $slug, 'title' => $title, 'description' => $desc]; + if (isset($cols['CREATED_AT'])) { + $data['created_at'] = $now; + } + if (isset($cols['UPDATED_AT'])) { + $data['updated_at'] = $now; + } + + $fields = array_keys($data); + $place = array_map(fn ($f) => ':' . $f, $fields); + $sql = 'INSERT INTO permission (' . implode(',', $fields) . ') VALUES (' . implode(',', $place) . ')'; + $conn->executeStatement($sql, $data); + + return (int) $conn->lastInsertId(); + } + + private function ensurePermissionRelRole(int $permissionId, int $roleId): void + { + $row = $this->connection->fetchAssociative( + "SELECT 1 FROM permission_rel_role WHERE permission_id = :p AND role_id = :r", + ['p' => $permissionId, 'r' => $roleId] + ); + if (!$row) { + $now = (new \DateTime())->format('Y-m-d H:i:s'); + $this->connection->executeStatement( + "INSERT INTO permission_rel_role (permission_id, role_id, changeable, updated_at) VALUES (:p, :r, 1, :u)", + ['p' => $permissionId, 'r' => $roleId, 'u' => $now] + ); + } + } +} From 4ef7fb9cc0354403dbbbe4f034c60f279ea08d75 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Mon, 22 Sep 2025 18:59:14 -0500 Subject: [PATCH 3/3] Internal: Minor, clean code --- public/main/inc/lib/usermanager.lib.php | 31 ------------------------- 1 file changed, 31 deletions(-) diff --git a/public/main/inc/lib/usermanager.lib.php b/public/main/inc/lib/usermanager.lib.php index e226b360989..19d63e6b168 100644 --- a/public/main/inc/lib/usermanager.lib.php +++ b/public/main/inc/lib/usermanager.lib.php @@ -5510,37 +5510,6 @@ public static function loginAsUser($userId, $checkIfUserCanLoginAs = true) // Logout current user self::loginDelete(api_get_user_id()); - // Reset and set new session data - Session::erase('_user'); - Session::erase('is_platformAdmin'); - Session::erase('is_allowedCreateCourse'); - Session::erase('_uid'); - - // Cleaning session variables - $_user['firstName'] = $userInfo['firstname']; - $_user['lastName'] = $userInfo['lastname']; - $_user['mail'] = $userInfo['email']; - $_user['official_code'] = $userInfo['official_code']; - $_user['picture_uri'] = $userInfo['picture_uri']; - $_user['user_id'] = $userId; - $_user['id'] = $userId; - $_user['status'] = $userInfo['status']; - - // Filling session variables with new data - Session::write('_uid', $userId); - Session::write('_user', $userInfo); - Session::write('is_platformAdmin', (bool) self::is_admin($userId)); - Session::write('is_allowedCreateCourse', 1 == $userInfo['status']); - // will be useful later to know if the user is actually an admin or not (example reporting) - Session::write('login_as', true); - - Event::registerLog([ - 'tool' => 'login', - 'tool_id' => 0, - 'tool_id_detail' => 0, - 'info' => $userId, - ]); - return true; }