From c492a14086f6dec4db19194973ec40ce1bfb970c Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 28 Aug 2024 18:12:36 -0500 Subject: [PATCH 01/22] Plugin: Azure: Allows the user to be verified based on the internal Azure ID - refs BT#21930 --- main/auth/external_login/login.azure.php | 10 ++++++++++ plugin/azure_active_directory/lang/dutch.php | 1 + .../azure_active_directory/lang/english.php | 1 + plugin/azure_active_directory/lang/french.php | 1 + .../azure_active_directory/lang/spanish.php | 1 + .../src/AzureActiveDirectory.php | 7 +++++++ .../azure_active_directory/src/callback.php | 20 +++++++++++++++++-- 7 files changed, 39 insertions(+), 2 deletions(-) diff --git a/main/auth/external_login/login.azure.php b/main/auth/external_login/login.azure.php index 95acea4530..e00da56c87 100644 --- a/main/auth/external_login/login.azure.php +++ b/main/auth/external_login/login.azure.php @@ -12,6 +12,16 @@ if ($uData['auth_source'] === 'azure') { api_not_allowed(true); } + $uidField = new ExtraFieldValue('user'); + $uidValue = $uidField->get_values_by_handler_and_field_variable( + $uData['user_id'], + AzureActiveDirectory::EXTRA_FIELD_AZURE_UID + ); + + if (empty($uidValue) || empty($uidValue['value'])) { + api_not_allowed(true); + } + $azureIdField = new ExtraFieldValue('user'); $azureIdValue = $azureIdField->get_values_by_handler_and_field_variable( $uData['user_id'], diff --git a/plugin/azure_active_directory/lang/dutch.php b/plugin/azure_active_directory/lang/dutch.php index 7f3e03e563..2e2dbb6d77 100644 --- a/plugin/azure_active_directory/lang/dutch.php +++ b/plugin/azure_active_directory/lang/dutch.php @@ -24,6 +24,7 @@ $strings['management_login_name'] = 'Naam voor de beheeraanmelding'; $strings['management_login_name_help'] = 'De standaardinstelling is "Beheer login".'; $strings['OrganisationEmail'] = 'Organisatie e-mail'; $strings['AzureId'] = 'Azure ID (mailNickname)'; +$strings['AzureUid'] = 'Azure UID (internal ID)'; $strings['ManagementLogin'] = 'Beheer Login'; $strings['InvalidId'] = 'Deze identificatie is niet geldig (verkeerde log-in of wachtwoord). Errocode: AZMNF'; $strings['provisioning'] = 'Geautomatiseerde inrichting'; diff --git a/plugin/azure_active_directory/lang/english.php b/plugin/azure_active_directory/lang/english.php index 5faf822a74..61c4bf9b75 100644 --- a/plugin/azure_active_directory/lang/english.php +++ b/plugin/azure_active_directory/lang/english.php @@ -24,6 +24,7 @@ $strings['management_login_name'] = 'Name for the management login'; $strings['management_login_name_help'] = 'The default is "Management Login".'; $strings['OrganisationEmail'] = 'Organisation e-mail'; $strings['AzureId'] = 'Azure ID (mailNickname)'; +$strings['AzureUid'] = 'Azure UID (internal ID)'; $strings['ManagementLogin'] = 'Management Login'; $strings['InvalidId'] = 'Login failed - incorrect login or password. Errocode: AZMNF'; $strings['provisioning'] = 'Automated provisioning'; diff --git a/plugin/azure_active_directory/lang/french.php b/plugin/azure_active_directory/lang/french.php index 2fdb548083..96887c70f3 100644 --- a/plugin/azure_active_directory/lang/french.php +++ b/plugin/azure_active_directory/lang/french.php @@ -24,6 +24,7 @@ $strings['management_login_name'] = 'Nom du login de gestion'; $strings['management_login_name_help'] = 'Le nom par défaut est "Login de gestion".'; $strings['OrganisationEmail'] = 'E-mail professionnel'; $strings['AzureId'] = 'ID Azure (mailNickname)'; +$strings['AzureUid'] = 'Azure UID (internal ID)'; $strings['ManagementLogin'] = 'Login de gestion'; $strings['InvalidId'] = 'Échec du login - nom d\'utilisateur ou mot de passe incorrect. Errocode: AZMNF'; $strings['provisioning'] = 'Création automatisée'; diff --git a/plugin/azure_active_directory/lang/spanish.php b/plugin/azure_active_directory/lang/spanish.php index b6aef5cd36..ef1bc16db8 100644 --- a/plugin/azure_active_directory/lang/spanish.php +++ b/plugin/azure_active_directory/lang/spanish.php @@ -24,6 +24,7 @@ $strings['management_login_name'] = 'Nombre del bloque de login de gestión'; $strings['management_login_name_help'] = 'El nombre por defecto es "Login de gestión".'; $strings['OrganisationEmail'] = 'E-mail profesional'; $strings['AzureId'] = 'ID Azure (mailNickname)'; +$strings['AzureUid'] = 'UID Azure (ID interno)'; $strings['ManagementLogin'] = 'Login de gestión'; $strings['InvalidId'] = 'Problema en el login - nombre de usuario o contraseña incorrecto. Errocode: AZMNF'; $strings['provisioning'] = 'Creación automatizada'; diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 8e0f33f3d3..279e2b694e 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -29,6 +29,7 @@ class AzureActiveDirectory extends Plugin public const EXTRA_FIELD_ORGANISATION_EMAIL = 'organisationemail'; public const EXTRA_FIELD_AZURE_ID = 'azure_id'; + public const EXTRA_FIELD_AZURE_UID = 'azure_uid'; /** * AzureActiveDirectory constructor. @@ -123,5 +124,11 @@ class AzureActiveDirectory extends Plugin $this->get_lang('AzureId'), '' ); + UserManager::create_extra_field( + self::EXTRA_FIELD_AZURE_UID, + ExtraField::FIELD_TYPE_TEXT, + $this->get_lang('AzureUid'), + '' + ); } } diff --git a/plugin/azure_active_directory/src/callback.php b/plugin/azure_active_directory/src/callback.php index 08e140676e..036b0d70a9 100644 --- a/plugin/azure_active_directory/src/callback.php +++ b/plugin/azure_active_directory/src/callback.php @@ -79,7 +79,10 @@ try { throw new Exception('The mail field is empty in Azure AD and is needed to set the organisation email for this user.'); } if (empty($me['mailNickname'])) { - throw new Exception('The mailNickname field is empty in Azure AD and is needed to set the unique Azure ID for this user.'); + throw new Exception('The mailNickname field is empty in Azure AD and is needed to set the unique username for this user.'); + } + if (empty($me['objectId'])) { + throw new Exception('The id field is empty in Azure AD and is needed to set the unique Azure ID for this user.'); } $extraFieldValue = new ExtraFieldValue('user'); @@ -91,6 +94,10 @@ try { AzureActiveDirectory::EXTRA_FIELD_AZURE_ID, $me['mailNickname'] ); + $uidValue = $extraFieldValue->get_item_id_from_field_variable_and_field_value( + AzureActiveDirectory::EXTRA_FIELD_AZURE_UID, + $me['objectId'] + ); $userId = null; // Get the user ID (if any) from the EXTRA_FIELD_ORGANISATION_EMAIL extra @@ -107,6 +114,14 @@ try { } } + if (empty($userId)) { + // If the previous step didn't work, get the user ID from + // EXTRA_FIELD_AZURE_UID + if (!empty($uidValue) && isset($uidValue['item_id'])) { + $userId = $uidValue['item_id']; + } + } + if (empty($userId)) { // If we didn't find the user if ($plugin->get(AzureActiveDirectory::SETTING_PROVISION_USERS) === 'true') { @@ -155,6 +170,7 @@ try { [ 'extra_'.AzureActiveDirectory::EXTRA_FIELD_ORGANISATION_EMAIL => $me['mail'], 'extra_'.AzureActiveDirectory::EXTRA_FIELD_AZURE_ID => $me['mailNickname'], + 'extra_'.AzureActiveDirectory::EXTRA_FIELD_AZURE_UID => $me['id'], ], null, null, @@ -164,7 +180,7 @@ try { throw new Exception(get_lang('UserNotAdded').' '.$me['mailNickname']); } } else { - throw new Exception('User not found when checking the extra fields from '.$me['mail'].' or '.$me['mailNickname'].'.'); + throw new Exception('User not found when checking the extra fields from '.$me['mail'].' or '.$me['mailNickname'].' or '.$me['id'].'.'); } } From 994244bb02ad5ffd861fefc42cd8d53d84bc49d6 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Thu, 29 Aug 2024 09:02:33 -0500 Subject: [PATCH 02/22] Plugin: Azure: Add option to set the verification order por existing user - refs BT#21930 --- plugin/azure_active_directory/lang/dutch.php | 4 ++ .../azure_active_directory/lang/english.php | 4 ++ plugin/azure_active_directory/lang/french.php | 4 ++ .../azure_active_directory/lang/spanish.php | 4 ++ .../src/AzureActiveDirectory.php | 57 +++++++++++++++++++ .../azure_active_directory/src/callback.php | 37 +----------- 6 files changed, 74 insertions(+), 36 deletions(-) diff --git a/plugin/azure_active_directory/lang/dutch.php b/plugin/azure_active_directory/lang/dutch.php index 2e2dbb6d77..7cafd778dc 100644 --- a/plugin/azure_active_directory/lang/dutch.php +++ b/plugin/azure_active_directory/lang/dutch.php @@ -22,6 +22,10 @@ $strings['management_login_enable_help'] = 'Schakel de chamilo-login uit en scha .'U zult moeten kopiëren de /plugin/azure_active_directory/layout/login_form.tpl bestand in het /main/template/overrides/layout/ dossier.'; $strings['management_login_name'] = 'Naam voor de beheeraanmelding'; $strings['management_login_name_help'] = 'De standaardinstelling is "Beheer login".'; +$strings['existing_user_verification_order'] = 'Existing user verification order'; +$strings['existing_user_verification_order_help'] = 'This value indicates the order in which the user will be searched in Chamilo to verify its existence. ' + .'By default is 1, 2, 3.' + .'
  1. EXTRA_FIELD_ORGANISATION_EMAIL (mail)
  2. EXTRA_FIELD_AZURE_ID (mailNickname)
  3. EXTRA_FIELD_AZURE_UID (id of objectId)
'; $strings['OrganisationEmail'] = 'Organisatie e-mail'; $strings['AzureId'] = 'Azure ID (mailNickname)'; $strings['AzureUid'] = 'Azure UID (internal ID)'; diff --git a/plugin/azure_active_directory/lang/english.php b/plugin/azure_active_directory/lang/english.php index 61c4bf9b75..2dae53b180 100644 --- a/plugin/azure_active_directory/lang/english.php +++ b/plugin/azure_active_directory/lang/english.php @@ -22,6 +22,10 @@ $strings['management_login_enable_help'] = 'Disable the chamilo login and enable .'You will need to copy the /plugin/azure_active_directory/layout/login_form.tpl file to /main/template/overrides/layout/ directory.'; $strings['management_login_name'] = 'Name for the management login'; $strings['management_login_name_help'] = 'The default is "Management Login".'; +$strings['existing_user_verification_order'] = 'Existing user verification order'; +$strings['existing_user_verification_order_help'] = 'This value indicates the order in which the user will be searched in Chamilo to verify its existence. ' + .'By default is 1, 2, 3.' + .'
  1. EXTRA_FIELD_ORGANISATION_EMAIL (mail)
  2. EXTRA_FIELD_AZURE_ID (mailNickname)
  3. EXTRA_FIELD_AZURE_UID (id or objectId)
'; $strings['OrganisationEmail'] = 'Organisation e-mail'; $strings['AzureId'] = 'Azure ID (mailNickname)'; $strings['AzureUid'] = 'Azure UID (internal ID)'; diff --git a/plugin/azure_active_directory/lang/french.php b/plugin/azure_active_directory/lang/french.php index 96887c70f3..0446518c43 100644 --- a/plugin/azure_active_directory/lang/french.php +++ b/plugin/azure_active_directory/lang/french.php @@ -22,6 +22,10 @@ $strings['management_login_enable_help'] = 'Désactiver le login de Chamilo et p .'Vous devez, pour cela, copier le fichier /plugin/azure_active_directory/layout/login_form.tpl dans le répertoire /main/template/overrides/layout/.'; $strings['management_login_name'] = 'Nom du login de gestion'; $strings['management_login_name_help'] = 'Le nom par défaut est "Login de gestion".'; +$strings['existing_user_verification_order'] = 'Existing user verification order'; +$strings['existing_user_verification_order_help'] = 'This value indicates the order in which the user will be searched in Chamilo to verify its existence. ' + .'By default is 1, 2, 3.' + .'
  1. EXTRA_FIELD_ORGANISATION_EMAIL (mail)
  2. EXTRA_FIELD_AZURE_ID (mailNickname)
  3. EXTRA_FIELD_AZURE_UID (id ou objectId)
'; $strings['OrganisationEmail'] = 'E-mail professionnel'; $strings['AzureId'] = 'ID Azure (mailNickname)'; $strings['AzureUid'] = 'Azure UID (internal ID)'; diff --git a/plugin/azure_active_directory/lang/spanish.php b/plugin/azure_active_directory/lang/spanish.php index ef1bc16db8..e82a1775a4 100644 --- a/plugin/azure_active_directory/lang/spanish.php +++ b/plugin/azure_active_directory/lang/spanish.php @@ -22,6 +22,10 @@ $strings['management_login_enable_help'] = 'Desactivar el login de Chamilo y act .'Para ello, tendrá que copiar el archivo /plugin/azure_active_directory/layout/login_form.tpl en la carpeta /main/template/overrides/layout/.'; $strings['management_login_name'] = 'Nombre del bloque de login de gestión'; $strings['management_login_name_help'] = 'El nombre por defecto es "Login de gestión".'; +$strings['existing_user_verification_order'] = 'Orden de verificación de usuario existente'; +$strings['existing_user_verification_order_help'] = 'Este valor indica el orden en que el usuario serña buscado en Chamilo para verificar su existencia. ' + .'Por defecto es 1, 2, 3.' + .'
  1. EXTRA_FIELD_ORGANISATION_EMAIL (mail)
  2. EXTRA_FIELD_AZURE_ID (mailNickname)
  3. EXTRA_FIELD_AZURE_UID (id o objectId)
'; $strings['OrganisationEmail'] = 'E-mail profesional'; $strings['AzureId'] = 'ID Azure (mailNickname)'; $strings['AzureUid'] = 'UID Azure (ID interno)'; diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 279e2b694e..75fd178206 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -23,6 +23,7 @@ class AzureActiveDirectory extends Plugin public const SETTING_GROUP_ID_ADMIN = 'group_id_admin'; public const SETTING_GROUP_ID_SESSION_ADMIN = 'group_id_session_admin'; public const SETTING_GROUP_ID_TEACHER = 'group_id_teacher'; + public const SETTING_EXISTING_USER_VERIFICATION_ORDER = 'existing_user_verification_order'; public const URL_TYPE_AUTHORIZE = 'login'; public const URL_TYPE_LOGOUT = 'logout'; @@ -48,6 +49,7 @@ class AzureActiveDirectory extends Plugin self::SETTING_GROUP_ID_ADMIN => 'text', self::SETTING_GROUP_ID_SESSION_ADMIN => 'text', self::SETTING_GROUP_ID_TEACHER => 'text', + self::SETTING_EXISTING_USER_VERIFICATION_ORDER => 'text', ]; parent::__construct('2.3', 'Angel Fernando Quiroz Campos, Yannick Warnier', $settings); @@ -131,4 +133,59 @@ class AzureActiveDirectory extends Plugin '' ); } + + public function getExistingUserVerificationOrder(): array + { + $defaultOrder = [1, 2, 3]; + + $settingValue = $this->get(self::SETTING_EXISTING_USER_VERIFICATION_ORDER); + $selectedOrder = array_filter( + array_map( + 'trim', + explode(',', $settingValue) + ) + ); + $selectedOrder = array_map('intval', $selectedOrder); + $selectedOrder = array_filter( + $selectedOrder, + function ($position) use ($defaultOrder): bool { + return in_array($position, $defaultOrder); + } + ); + + if ($selectedOrder) { + return $selectedOrder; + } + + return $defaultOrder; + } + + public function getUserIdByVerificationOrder(array $azureUserData): ?int + { + $selectedOrder = $this->getExistingUserVerificationOrder(); + + $extraFieldValue = new ExtraFieldValue('user'); + $positionsAndFields = [ + 1 => $extraFieldValue->get_item_id_from_field_variable_and_field_value( + AzureActiveDirectory::EXTRA_FIELD_ORGANISATION_EMAIL, + $azureUserData['mail'] + ), + 2 => $extraFieldValue->get_item_id_from_field_variable_and_field_value( + AzureActiveDirectory::EXTRA_FIELD_AZURE_ID, + $azureUserData['mailNickname'] + ), + 3 => $extraFieldValue->get_item_id_from_field_variable_and_field_value( + AzureActiveDirectory::EXTRA_FIELD_AZURE_UID, + $azureUserData['objectId'] + ), + ]; + + foreach ($selectedOrder as $position) { + if (!empty($positionsAndFields[$position]) && isset($positionsAndFields[$position]['item_id'])) { + return (int) $positionsAndFields[$position]['item_id']; + } + } + + return null; + } } diff --git a/plugin/azure_active_directory/src/callback.php b/plugin/azure_active_directory/src/callback.php index 036b0d70a9..cb76aca6b1 100644 --- a/plugin/azure_active_directory/src/callback.php +++ b/plugin/azure_active_directory/src/callback.php @@ -85,42 +85,7 @@ try { throw new Exception('The id field is empty in Azure AD and is needed to set the unique Azure ID for this user.'); } - $extraFieldValue = new ExtraFieldValue('user'); - $organisationValue = $extraFieldValue->get_item_id_from_field_variable_and_field_value( - AzureActiveDirectory::EXTRA_FIELD_ORGANISATION_EMAIL, - $me['mail'] - ); - $azureValue = $extraFieldValue->get_item_id_from_field_variable_and_field_value( - AzureActiveDirectory::EXTRA_FIELD_AZURE_ID, - $me['mailNickname'] - ); - $uidValue = $extraFieldValue->get_item_id_from_field_variable_and_field_value( - AzureActiveDirectory::EXTRA_FIELD_AZURE_UID, - $me['objectId'] - ); - - $userId = null; - // Get the user ID (if any) from the EXTRA_FIELD_ORGANISATION_EMAIL extra - // field - if (!empty($organisationValue) && isset($organisationValue['item_id'])) { - $userId = $organisationValue['item_id']; - } - - if (empty($userId)) { - // If the previous step didn't work, get the user ID from - // EXTRA_FIELD_AZURE_ID - if (!empty($azureValue) && isset($azureValue['item_id'])) { - $userId = $azureValue['item_id']; - } - } - - if (empty($userId)) { - // If the previous step didn't work, get the user ID from - // EXTRA_FIELD_AZURE_UID - if (!empty($uidValue) && isset($uidValue['item_id'])) { - $userId = $uidValue['item_id']; - } - } + $userId = $plugin->getUserIdByVerificationOrder($me); if (empty($userId)) { // If we didn't find the user From 9cecd7bf71ec4bfc6a253e3f79dfac2efe8aab04 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Tue, 3 Sep 2024 01:37:35 -0500 Subject: [PATCH 03/22] Plugin: Azure: Move code to function - refs BT#21930 --- .../src/AzureActiveDirectory.php | 110 +++++++++++++++++- .../azure_active_directory/src/callback.php | 68 +---------- 2 files changed, 112 insertions(+), 66 deletions(-) diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 75fd178206..788ba8b3bb 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -1,6 +1,7 @@ getExistingUserVerificationOrder(); $extraFieldValue = new ExtraFieldValue('user'); @@ -176,7 +176,7 @@ class AzureActiveDirectory extends Plugin ), 3 => $extraFieldValue->get_item_id_from_field_variable_and_field_value( AzureActiveDirectory::EXTRA_FIELD_AZURE_UID, - $azureUserData['objectId'] + $azureUserData[$azureUidKey] ), ]; @@ -188,4 +188,108 @@ class AzureActiveDirectory extends Plugin return null; } + + /** + * @throws Exception + */ + public function registerUser( + AccessTokenInterface $token, + Azure $provider, + array $azureUserInfo, + string $apiGroupsRef = 'me/memberOf', + string $objectIdKey = 'objectId', + string $azureUidKey = 'objectId' + ) { + if (empty($azureUserInfo)) { + throw new Exception('Groups info not found.'); + } + + $userId = $this->getUserIdByVerificationOrder($azureUserInfo, $azureUidKey); + + if (empty($userId)) { + // If we didn't find the user + if ($this->get(self::SETTING_PROVISION_USERS) === 'true') { + [$userRole, $isAdmin] = $this->getUserRoleAndCheckIsAdmin( + $token, + $provider, + $apiGroupsRef, + $objectIdKey + ); + + $phone = null; + + if (isset($azureUserInfo['telephoneNumber'])) { + $phone = $azureUserInfo['telephoneNumber']; + } elseif (isset($azureUserInfo['businessPhones'][0])) { + $phone = $azureUserInfo['businessPhones'][0]; + } elseif (isset($azureUserInfo['mobilePhone'])) { + $phone = $azureUserInfo['mobilePhone']; + } + + // If the option is set to create users, create it + $userId = UserManager::create_user( + $azureUserInfo['givenName'], + $azureUserInfo['surname'], + $userRole, + $azureUserInfo['mail'], + $azureUserInfo['userPrincipalName'], + '', + null, + null, + $phone, + null, + 'azure', + null, + ($azureUserInfo['accountEnabled'] ? 1 : 0), + null, + [ + 'extra_'.self::EXTRA_FIELD_ORGANISATION_EMAIL => $azureUserInfo['mail'], + 'extra_'.self::EXTRA_FIELD_AZURE_ID => $azureUserInfo['mailNickname'], + 'extra_'.self::EXTRA_FIELD_AZURE_UID => $azureUserInfo[$azureUidKey], + ], + null, + null, + $isAdmin + ); + if (!$userId) { + throw new Exception(get_lang('UserNotAdded').' '.$azureUserInfo['userPrincipalName']); + } + } else { + throw new Exception('User not found when checking the extra fields from '.$azureUserInfo['mail'].' or '.$azureUserInfo['mailNickname'].' or '.$azureUserInfo[$azureUidKey].'.'); + } + } + + return $userId; + } + + private function getUserRoleAndCheckIsAdmin( + AccessTokenInterface $token, + Azure $provider = null, + string $apiRef = 'me/memberOf', + string $objectIdKey = 'objectId' + ): array { + $provider = $provider ?: $this->getProvider(); + + $groups = $provider->get($apiRef, $token); + + // If any specific group ID has been defined for a specific role, use that + // ID to give the user the right role + $givenAdminGroup = $this->get(self::SETTING_GROUP_ID_ADMIN); + $givenSessionAdminGroup = $this->get(self::SETTING_GROUP_ID_SESSION_ADMIN); + $givenTeacherGroup = $this->get(self::SETTING_GROUP_ID_TEACHER); + $userRole = STUDENT; + $isAdmin = false; + foreach ($groups as $group) { + if ($givenAdminGroup == $group[$objectIdKey]) { + $userRole = COURSEMANAGER; + $isAdmin = true; + } elseif ($givenSessionAdminGroup == $group[$objectIdKey]) { + $userRole = SESSIONADMIN; + } elseif ($userRole != SESSIONADMIN && $givenTeacherGroup == $group[$objectIdKey]) { + $userRole = COURSEMANAGER; + } + } + + return [$userRole, $isAdmin]; + } } diff --git a/plugin/azure_active_directory/src/callback.php b/plugin/azure_active_directory/src/callback.php index cb76aca6b1..09d0b363d6 100644 --- a/plugin/azure_active_directory/src/callback.php +++ b/plugin/azure_active_directory/src/callback.php @@ -85,69 +85,11 @@ try { throw new Exception('The id field is empty in Azure AD and is needed to set the unique Azure ID for this user.'); } - $userId = $plugin->getUserIdByVerificationOrder($me); - - if (empty($userId)) { - // If we didn't find the user - if ($plugin->get(AzureActiveDirectory::SETTING_PROVISION_USERS) === 'true') { - // Get groups info, if any - $groups = $provider->get('me/memberOf', $token); - if (empty($me)) { - throw new Exception('Groups info not found.'); - } - // If any specific group ID has been defined for a specific role, use that - // ID to give the user the right role - $givenAdminGroup = $plugin->get(AzureActiveDirectory::SETTING_GROUP_ID_ADMIN); - $givenSessionAdminGroup = $plugin->get(AzureActiveDirectory::SETTING_GROUP_ID_SESSION_ADMIN); - $givenTeacherGroup = $plugin->get(AzureActiveDirectory::SETTING_GROUP_ID_TEACHER); - $userRole = STUDENT; - $isAdmin = false; - foreach ($groups as $group) { - if ($isAdmin) { - break; - } - if ($givenAdminGroup == $group['objectId']) { - $userRole = COURSEMANAGER; - $isAdmin = true; - } elseif (!$isAdmin && $givenSessionAdminGroup == $group['objectId']) { - $userRole = SESSIONADMIN; - } elseif (!$isAdmin && $userRole != SESSIONADMIN && $givenTeacherGroup == $group['objectId']) { - $userRole = COURSEMANAGER; - } - } - - // If the option is set to create users, create it - $userId = UserManager::create_user( - $me['givenName'], - $me['surname'], - $userRole, - $me['mail'], - $me['mailNickname'], - '', - null, - null, - $me['telephoneNumber'], - null, - 'azure', - null, - ($me['accountEnabled'] ? 1 : 0), - null, - [ - 'extra_'.AzureActiveDirectory::EXTRA_FIELD_ORGANISATION_EMAIL => $me['mail'], - 'extra_'.AzureActiveDirectory::EXTRA_FIELD_AZURE_ID => $me['mailNickname'], - 'extra_'.AzureActiveDirectory::EXTRA_FIELD_AZURE_UID => $me['id'], - ], - null, - null, - $isAdmin - ); - if (!$userId) { - throw new Exception(get_lang('UserNotAdded').' '.$me['mailNickname']); - } - } else { - throw new Exception('User not found when checking the extra fields from '.$me['mail'].' or '.$me['mailNickname'].' or '.$me['id'].'.'); - } - } + $userId = $plugin->registerUser( + $token, + $provider, + $me + ); $userInfo = api_get_user_info($userId); From dc27ce5f43499f3546cb57dd9b2a8e100146903c Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Tue, 3 Sep 2024 03:08:16 -0500 Subject: [PATCH 04/22] Plugin: Azure: Add option to update existing users - refs BT#21930 --- plugin/azure_active_directory/lang/dutch.php | 2 + .../azure_active_directory/lang/english.php | 2 + plugin/azure_active_directory/lang/french.php | 2 + .../azure_active_directory/lang/spanish.php | 2 + .../src/AzureActiveDirectory.php | 138 ++++++++++++++---- 5 files changed, 119 insertions(+), 27 deletions(-) diff --git a/plugin/azure_active_directory/lang/dutch.php b/plugin/azure_active_directory/lang/dutch.php index 7cafd778dc..151c039fd7 100644 --- a/plugin/azure_active_directory/lang/dutch.php +++ b/plugin/azure_active_directory/lang/dutch.php @@ -32,6 +32,8 @@ $strings['AzureUid'] = 'Azure UID (internal ID)'; $strings['ManagementLogin'] = 'Beheer Login'; $strings['InvalidId'] = 'Deze identificatie is niet geldig (verkeerde log-in of wachtwoord). Errocode: AZMNF'; $strings['provisioning'] = 'Geautomatiseerde inrichting'; +$strings['update_users'] = 'Update users'; +$strings['update_users_help'] = 'Allow user data to be updated at the start of the session.'; $strings['provisioning_help'] = 'Maak automatisch nieuwe gebruikers (als studenten) vanuit Azure wanneer ze niet in Chamilo zijn.'; $strings['group_id_admin'] = 'Groeps-ID voor platformbeheerders'; $strings['group_id_admin_help'] = 'De groeps-ID is te vinden in de details van de gebruikersgroep en ziet er ongeveer zo uit: ae134eef-cbd4-4a32-ba99-49898a1314b6. Indien leeg, wordt er automatisch geen gebruiker aangemaakt als admin.'; diff --git a/plugin/azure_active_directory/lang/english.php b/plugin/azure_active_directory/lang/english.php index 2dae53b180..59f76d654f 100644 --- a/plugin/azure_active_directory/lang/english.php +++ b/plugin/azure_active_directory/lang/english.php @@ -33,6 +33,8 @@ $strings['ManagementLogin'] = 'Management Login'; $strings['InvalidId'] = 'Login failed - incorrect login or password. Errocode: AZMNF'; $strings['provisioning'] = 'Automated provisioning'; $strings['provisioning_help'] = 'Automatically create new users (as students) from Azure when they are not in Chamilo.'; +$strings['update_users'] = 'Update users'; +$strings['update_users_help'] = 'Allow user data to be updated at the start of the session.'; $strings['group_id_admin'] = 'Group ID for platform admins'; $strings['group_id_admin_help'] = 'The group ID can be found in the user group details, looking similar to this: ae134eef-cbd4-4a32-ba99-49898a1314b6. If empty, no user will be automatically created as admin.'; $strings['group_id_session_admin'] = 'Group ID for session admins'; diff --git a/plugin/azure_active_directory/lang/french.php b/plugin/azure_active_directory/lang/french.php index 0446518c43..691c34a717 100644 --- a/plugin/azure_active_directory/lang/french.php +++ b/plugin/azure_active_directory/lang/french.php @@ -33,6 +33,8 @@ $strings['ManagementLogin'] = 'Login de gestion'; $strings['InvalidId'] = 'Échec du login - nom d\'utilisateur ou mot de passe incorrect. Errocode: AZMNF'; $strings['provisioning'] = 'Création automatisée'; $strings['provisioning_help'] = 'Créer les utilisateurs automatiquement (en tant qu\'apprenants) depuis Azure s\'ils n\'existent pas encore dans Chamilo.'; +$strings['update_users'] = 'Actualiser les utilisateurs'; +$strings['update_users_help'] = 'Permettre d\'actualiser les données de l\'utilisateur lors du démarrage de la session.'; $strings['group_id_admin'] = 'ID du groupe administrateur'; $strings['group_id_admin_help'] = 'L\'id du groupe peut être trouvé dans les détails du groupe, et ressemble à ceci : ae134eef-cbd4-4a32-ba99-49898a1314b6. Si ce champ est laissé vide, aucun utilisateur ne sera créé en tant qu\'administrateur.'; $strings['group_id_session_admin'] = 'ID du groupe administrateur de sessions'; diff --git a/plugin/azure_active_directory/lang/spanish.php b/plugin/azure_active_directory/lang/spanish.php index e82a1775a4..03978fcf3e 100644 --- a/plugin/azure_active_directory/lang/spanish.php +++ b/plugin/azure_active_directory/lang/spanish.php @@ -33,6 +33,8 @@ $strings['ManagementLogin'] = 'Login de gestión'; $strings['InvalidId'] = 'Problema en el login - nombre de usuario o contraseña incorrecto. Errocode: AZMNF'; $strings['provisioning'] = 'Creación automatizada'; $strings['provisioning_help'] = 'Crear usuarios automáticamente (como alumnos) desde Azure si no existen en Chamilo todavía.'; +$strings['update_users'] = 'Actualizar los usuarios'; +$strings['update_users_help'] = 'Permite actualizar los datos del usuario al iniciar sesión.'; $strings['group_id_admin'] = 'ID de grupo administrador'; $strings['group_id_admin_help'] = 'El ID de grupo se encuentra en los detalles del grupo en Azure, y parece a: ae134eef-cbd4-4a32-ba99-49898a1314b6. Si deja este campo vacío, ningún usuario será creado como administrador.'; $strings['group_id_session_admin'] = 'ID de grupo admin de sesiones'; diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 788ba8b3bb..eef9559ffb 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -21,6 +21,7 @@ class AzureActiveDirectory extends Plugin public const SETTING_MANAGEMENT_LOGIN_ENABLE = 'management_login_enable'; public const SETTING_MANAGEMENT_LOGIN_NAME = 'management_login_name'; public const SETTING_PROVISION_USERS = 'provisioning'; + public const SETTING_UPDATE_USERS = 'update_users'; public const SETTING_GROUP_ID_ADMIN = 'group_id_admin'; public const SETTING_GROUP_ID_SESSION_ADMIN = 'group_id_session_admin'; public const SETTING_GROUP_ID_TEACHER = 'group_id_teacher'; @@ -47,6 +48,7 @@ class AzureActiveDirectory extends Plugin self::SETTING_MANAGEMENT_LOGIN_ENABLE => 'boolean', self::SETTING_MANAGEMENT_LOGIN_NAME => 'text', self::SETTING_PROVISION_USERS => 'boolean', + self::SETTING_UPDATE_USERS => 'boolean', self::SETTING_GROUP_ID_ADMIN => 'text', self::SETTING_GROUP_ID_SESSION_ADMIN => 'text', self::SETTING_GROUP_ID_TEACHER => 'text', @@ -209,44 +211,36 @@ class AzureActiveDirectory extends Plugin if (empty($userId)) { // If we didn't find the user if ($this->get(self::SETTING_PROVISION_USERS) === 'true') { - [$userRole, $isAdmin] = $this->getUserRoleAndCheckIsAdmin( - $token, - $provider, - $apiGroupsRef, - $objectIdKey - ); - - $phone = null; - - if (isset($azureUserInfo['telephoneNumber'])) { - $phone = $azureUserInfo['telephoneNumber']; - } elseif (isset($azureUserInfo['businessPhones'][0])) { - $phone = $azureUserInfo['businessPhones'][0]; - } elseif (isset($azureUserInfo['mobilePhone'])) { - $phone = $azureUserInfo['mobilePhone']; - } + [ + $firstNme, + $lastName, + $username, + $email, + $phone, + $authSource, + $active, + $extra, + $userRole, + $isAdmin, + ] = $this->formatUserData($token, $provider, $azureUserInfo, $apiGroupsRef, $objectIdKey, $azureUidKey); // If the option is set to create users, create it $userId = UserManager::create_user( - $azureUserInfo['givenName'], - $azureUserInfo['surname'], + $firstNme, + $lastName, $userRole, - $azureUserInfo['mail'], - $azureUserInfo['userPrincipalName'], + $email, + $username, '', null, null, $phone, null, - 'azure', + $authSource, null, - ($azureUserInfo['accountEnabled'] ? 1 : 0), + $active, null, - [ - 'extra_'.self::EXTRA_FIELD_ORGANISATION_EMAIL => $azureUserInfo['mail'], - 'extra_'.self::EXTRA_FIELD_AZURE_ID => $azureUserInfo['mailNickname'], - 'extra_'.self::EXTRA_FIELD_AZURE_UID => $azureUserInfo[$azureUidKey], - ], + $extra, null, null, $isAdmin @@ -257,11 +251,101 @@ class AzureActiveDirectory extends Plugin } else { throw new Exception('User not found when checking the extra fields from '.$azureUserInfo['mail'].' or '.$azureUserInfo['mailNickname'].' or '.$azureUserInfo[$azureUidKey].'.'); } + } else { + if ($this->get(self::SETTING_UPDATE_USERS) === 'true') { + [ + $firstNme, + $lastName, + $username, + $email, + $phone, + $authSource, + $active, + $extra, + $userRole, + $isAdmin, + ] = $this->formatUserData($token, $provider, $azureUserInfo, $apiGroupsRef, $objectIdKey, $azureUidKey); + + $userId = UserManager::update_user( + $userId, + $firstNme, + $lastName, + $username, + '', + $authSource, + $email, + $userRole, + null, + $phone, + null, + null, + $active, + null, + 0, + $extra + ); + + if (!$userId) { + throw new Exception(get_lang('CouldNotUpdateUser').' '.$azureUserInfo['userPrincipalName']); + } + } } return $userId; } + private function formatUserData( + AccessTokenInterface $token, + Azure $provider, + array $azureUserInfo, + string $apiGroupsRef, + string $objectIdKey, + string $azureUidKey + ): array { + [$userRole, $isAdmin] = $this->getUserRoleAndCheckIsAdmin( + $token, + $provider, + $apiGroupsRef, + $objectIdKey + ); + + $phone = null; + + if (isset($azureUserInfo['telephoneNumber'])) { + $phone = $azureUserInfo['telephoneNumber']; + } elseif (isset($azureUserInfo['businessPhones'][0])) { + $phone = $azureUserInfo['businessPhones'][0]; + } elseif (isset($azureUserInfo['mobilePhone'])) { + $phone = $azureUserInfo['mobilePhone']; + } + + // If the option is set to create users, create it + $firstNme = $azureUserInfo['givenName']; + $lastName = $azureUserInfo['surname']; + $email = $azureUserInfo['mail']; + $username = $azureUserInfo['userPrincipalName']; + $authSource = 'azure'; + $active = ($azureUserInfo['accountEnabled'] ? 1 : 0); + $extra = [ + 'extra_'.self::EXTRA_FIELD_ORGANISATION_EMAIL => $azureUserInfo['mail'], + 'extra_'.self::EXTRA_FIELD_AZURE_ID => $azureUserInfo['mailNickname'], + 'extra_'.self::EXTRA_FIELD_AZURE_UID => $azureUserInfo[$azureUidKey], + ]; + + return [ + $firstNme, + $lastName, + $username, + $email, + $phone, + $authSource, + $active, + $extra, + $userRole, + $isAdmin, + ]; + } + private function getUserRoleAndCheckIsAdmin( AccessTokenInterface $token, Azure $provider = null, From 6949a076847ad71b9eb762dddc89468a96ab9664 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:39:11 -0500 Subject: [PATCH 05/22] Plugin: Azure: Add script to sync users from Azure - refs BT#21930 --- plugin/azure_active_directory/lang/dutch.php | 2 + .../azure_active_directory/lang/english.php | 2 + plugin/azure_active_directory/lang/french.php | 2 + .../azure_active_directory/lang/spanish.php | 2 + .../src/AzureActiveDirectory.php | 2 + .../src/scripts/sync_users.php | 69 +++++++++++++++++++ 6 files changed, 79 insertions(+) create mode 100644 plugin/azure_active_directory/src/scripts/sync_users.php diff --git a/plugin/azure_active_directory/lang/dutch.php b/plugin/azure_active_directory/lang/dutch.php index 151c039fd7..3fa2927b09 100644 --- a/plugin/azure_active_directory/lang/dutch.php +++ b/plugin/azure_active_directory/lang/dutch.php @@ -42,3 +42,5 @@ $strings['group_id_session_admin_help'] = 'De groeps-ID voor sessiebeheerders. I $strings['group_id_teacher'] = 'Groeps-ID voor docenten'; $strings['group_id_teacher_help'] = 'De groeps-ID voor docenten. Indien leeg, wordt er automatisch geen gebruiker aangemaakt als docent.'; $strings['additional_interaction_required'] = 'Er is aanvullende interactie vereist om u te authenticeren. Log rechtstreeks in via uw authenticatiesysteem en kom dan terug naar deze pagina om in te loggen.'; +$strings['tenant_id'] = 'Mandanten-ID'; +$strings['tenant_id_help'] = 'Required to run scripts.'; diff --git a/plugin/azure_active_directory/lang/english.php b/plugin/azure_active_directory/lang/english.php index 59f76d654f..0295f093c4 100644 --- a/plugin/azure_active_directory/lang/english.php +++ b/plugin/azure_active_directory/lang/english.php @@ -42,3 +42,5 @@ $strings['group_id_session_admin_help'] = 'The group ID for session admins. If e $strings['group_id_teacher'] = 'Group ID for teachers'; $strings['group_id_teacher_help'] = 'The group ID for teachers. If empty, no user will be automatically created as teacher.'; $strings['additional_interaction_required'] = 'Some additional interaction is required to authenticate you. Please login directly through your authentication system, then come back to this page to login.'; +$strings['tenant_id'] = 'Tenant ID'; +$strings['tenant_id_help'] = 'Required to run scripts.'; diff --git a/plugin/azure_active_directory/lang/french.php b/plugin/azure_active_directory/lang/french.php index 691c34a717..77f195a80b 100644 --- a/plugin/azure_active_directory/lang/french.php +++ b/plugin/azure_active_directory/lang/french.php @@ -42,3 +42,5 @@ $strings['group_id_session_admin_help'] = 'The group ID for session admins. Si c $strings['group_id_teacher'] = 'ID du groupe enseignant'; $strings['group_id_teacher_help'] = 'The group ID for teachers. Si ce champ est laissé vide, aucun utilisateur ne sera créé en tant qu\'enseignant.'; $strings['additional_interaction_required'] = 'Une interaction supplémentaire est nécessaire pour vous authentifier. Veuillez vous connecter directement auprès de votre système d\'authentification, puis revenir ici pour vous connecter.'; +$strings['tenant_id'] = 'ID du client'; +$strings['tenant_id_help'] = 'Nécessaire pour exécuter des scripts.'; diff --git a/plugin/azure_active_directory/lang/spanish.php b/plugin/azure_active_directory/lang/spanish.php index 03978fcf3e..b657c5b775 100644 --- a/plugin/azure_active_directory/lang/spanish.php +++ b/plugin/azure_active_directory/lang/spanish.php @@ -42,3 +42,5 @@ $strings['group_id_session_admin_help'] = 'El ID de grupo para administradores d $strings['group_id_teacher'] = 'ID de grupo profesor'; $strings['group_id_teacher_help'] = 'El ID de grupo para profesores. Si deja este campo vacío, ningún usuario será creado como profesor.'; $strings['additional_interaction_required'] = 'Alguna interacción adicional es necesaria para identificarlo/a. Por favor conéctese primero a través de su sistema de autenticación, luego regrese aquí para logearse.'; +$strings['tenant_id'] = 'Id. del inquilino'; +$strings['tenant_id_help'] = 'Necesario para ejecutar scripts.'; diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index eef9559ffb..5daaad6cad 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -26,6 +26,7 @@ class AzureActiveDirectory extends Plugin public const SETTING_GROUP_ID_SESSION_ADMIN = 'group_id_session_admin'; public const SETTING_GROUP_ID_TEACHER = 'group_id_teacher'; public const SETTING_EXISTING_USER_VERIFICATION_ORDER = 'existing_user_verification_order'; + public const SETTING_TENANT_ID = 'tenant_id'; public const URL_TYPE_AUTHORIZE = 'login'; public const URL_TYPE_LOGOUT = 'logout'; @@ -53,6 +54,7 @@ class AzureActiveDirectory extends Plugin self::SETTING_GROUP_ID_SESSION_ADMIN => 'text', self::SETTING_GROUP_ID_TEACHER => 'text', self::SETTING_EXISTING_USER_VERIFICATION_ORDER => 'text', + self::SETTING_TENANT_ID => 'text', ]; parent::__construct('2.3', 'Angel Fernando Quiroz Campos, Yannick Warnier', $settings); diff --git a/plugin/azure_active_directory/src/scripts/sync_users.php b/plugin/azure_active_directory/src/scripts/sync_users.php new file mode 100644 index 0000000000..58a65f475a --- /dev/null +++ b/plugin/azure_active_directory/src/scripts/sync_users.php @@ -0,0 +1,69 @@ +getProvider(); +$provider->urlAPI = "https://graph.microsoft.com/v1.0/"; +$provider->resource = "https://graph.microsoft.com/"; +$provider->tenant = $plugin->get(AzureActiveDirectory::SETTING_TENANT_ID); +$provider->authWithResource = false; + +echo 'Synchronizing users from Azure.'.PHP_EOL; + +try { + $token = $provider->getAccessToken( + 'client_credentials', + ['resource' => $provider->resource] + ); + + $userFields = [ + 'givenName', + 'surname', + 'mail', + 'userPrincipalName', + 'businessPhones', + 'mobilePhone', + 'accountEnabled', + 'mailNickname', + 'id' + ]; + + $azureUsersInfo = $provider->get( + 'users?$select='.implode(',', $userFields), + $token + ); +} catch (Exception $e) { + printf("%s - %s".PHP_EOL, time(), $e->getMessage()); + die; +} + +printf("%s - Number of users obtained %d".PHP_EOL, time(), count($azureUsersInfo)); + +/** @var array $user */ +foreach ($azureUsersInfo as $azureUserInfo) { + try { + $userId = $plugin->registerUser( + $token, + $provider, + $azureUserInfo, + 'users/' . $azureUserInfo['id'] . '/memberOf', + 'id', + 'id' + ); + + $userInfo = api_get_user_info($userId); + + printf("%s - UserInfo %s".PHP_EOL, time(), serialize($userInfo)); + } catch (Exception $e) { + printf("%s - %s".PHP_EOL, time(), $e->getMessage()); + + continue; + } +} From 331d9fac410755439da20706d0627d14ff977369 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Tue, 3 Sep 2024 12:07:26 -0500 Subject: [PATCH 06/22] Plugin: Azure: Add option to deactivate non-existing users in Azure when running sync_users script - refs BT#21930 --- plugin/azure_active_directory/lang/dutch.php | 2 ++ .../azure_active_directory/lang/english.php | 2 ++ plugin/azure_active_directory/lang/french.php | 2 ++ .../azure_active_directory/lang/spanish.php | 2 ++ .../src/AzureActiveDirectory.php | 2 ++ .../src/scripts/sync_users.php | 26 +++++++++++++++++++ .../UserBundle/Repository/UserRepository.php | 5 ++++ 7 files changed, 41 insertions(+) diff --git a/plugin/azure_active_directory/lang/dutch.php b/plugin/azure_active_directory/lang/dutch.php index 3fa2927b09..32885f3f68 100644 --- a/plugin/azure_active_directory/lang/dutch.php +++ b/plugin/azure_active_directory/lang/dutch.php @@ -44,3 +44,5 @@ $strings['group_id_teacher_help'] = 'De groeps-ID voor docenten. Indien leeg, wo $strings['additional_interaction_required'] = 'Er is aanvullende interactie vereist om u te authenticeren. Log rechtstreeks in via uw authenticatiesysteem en kom dan terug naar deze pagina om in te loggen.'; $strings['tenant_id'] = 'Mandanten-ID'; $strings['tenant_id_help'] = 'Required to run scripts.'; +$strings['deactivate_nonexisting_users'] = 'Deactivate non-existing users'; +$strings['deactivate_nonexisting_users_help'] = 'Compare registered users in Chamilo with those in Azure and deactivate accounts in Chamilo that do not exist in Azure.'; diff --git a/plugin/azure_active_directory/lang/english.php b/plugin/azure_active_directory/lang/english.php index 0295f093c4..0d000a7d79 100644 --- a/plugin/azure_active_directory/lang/english.php +++ b/plugin/azure_active_directory/lang/english.php @@ -44,3 +44,5 @@ $strings['group_id_teacher_help'] = 'The group ID for teachers. If empty, no use $strings['additional_interaction_required'] = 'Some additional interaction is required to authenticate you. Please login directly through your authentication system, then come back to this page to login.'; $strings['tenant_id'] = 'Tenant ID'; $strings['tenant_id_help'] = 'Required to run scripts.'; +$strings['deactivate_nonexisting_users'] = 'Deactivate non-existing users'; +$strings['deactivate_nonexisting_users_help'] = 'Compare registered users in Chamilo with those in Azure and deactivate accounts in Chamilo that do not exist in Azure.'; diff --git a/plugin/azure_active_directory/lang/french.php b/plugin/azure_active_directory/lang/french.php index 77f195a80b..e699c6d91d 100644 --- a/plugin/azure_active_directory/lang/french.php +++ b/plugin/azure_active_directory/lang/french.php @@ -44,3 +44,5 @@ $strings['group_id_teacher_help'] = 'The group ID for teachers. Si ce champ est $strings['additional_interaction_required'] = 'Une interaction supplémentaire est nécessaire pour vous authentifier. Veuillez vous connecter directement auprès de votre système d\'authentification, puis revenir ici pour vous connecter.'; $strings['tenant_id'] = 'ID du client'; $strings['tenant_id_help'] = 'Nécessaire pour exécuter des scripts.'; +$strings['deactivate_nonexisting_users'] = 'Deactivate non-existing users'; +$strings['deactivate_nonexisting_users_help'] = 'Compare registered users in Chamilo with those in Azure and deactivate accounts in Chamilo that do not exist in Azure.'; diff --git a/plugin/azure_active_directory/lang/spanish.php b/plugin/azure_active_directory/lang/spanish.php index b657c5b775..389a4912e4 100644 --- a/plugin/azure_active_directory/lang/spanish.php +++ b/plugin/azure_active_directory/lang/spanish.php @@ -44,3 +44,5 @@ $strings['group_id_teacher_help'] = 'El ID de grupo para profesores. Si deja est $strings['additional_interaction_required'] = 'Alguna interacción adicional es necesaria para identificarlo/a. Por favor conéctese primero a través de su sistema de autenticación, luego regrese aquí para logearse.'; $strings['tenant_id'] = 'Id. del inquilino'; $strings['tenant_id_help'] = 'Necesario para ejecutar scripts.'; +$strings['deactivate_nonexisting_users'] = 'Desactivar usuarios no existentes'; +$strings['deactivate_nonexisting_users_help'] = 'Compara los usuarios registrados en Chamilo con los de Azure y desactiva las cuentas en Chamilo que no existan en Azure.'; diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 5daaad6cad..7399344b19 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -27,6 +27,7 @@ class AzureActiveDirectory extends Plugin public const SETTING_GROUP_ID_TEACHER = 'group_id_teacher'; public const SETTING_EXISTING_USER_VERIFICATION_ORDER = 'existing_user_verification_order'; public const SETTING_TENANT_ID = 'tenant_id'; + public const SETTING_DEACTIVATE_NONEXISTING_USERS = 'deactivate_nonexisting_users'; public const URL_TYPE_AUTHORIZE = 'login'; public const URL_TYPE_LOGOUT = 'logout'; @@ -55,6 +56,7 @@ class AzureActiveDirectory extends Plugin self::SETTING_GROUP_ID_TEACHER => 'text', self::SETTING_EXISTING_USER_VERIFICATION_ORDER => 'text', self::SETTING_TENANT_ID => 'text', + self::SETTING_DEACTIVATE_NONEXISTING_USERS => 'boolean', ]; parent::__construct('2.3', 'Angel Fernando Quiroz Campos, Yannick Warnier', $settings); diff --git a/plugin/azure_active_directory/src/scripts/sync_users.php b/plugin/azure_active_directory/src/scripts/sync_users.php index 58a65f475a..af9fc764fd 100644 --- a/plugin/azure_active_directory/src/scripts/sync_users.php +++ b/plugin/azure_active_directory/src/scripts/sync_users.php @@ -46,6 +46,8 @@ try { printf("%s - Number of users obtained %d".PHP_EOL, time(), count($azureUsersInfo)); +$existingUsers = []; + /** @var array $user */ foreach ($azureUsersInfo as $azureUserInfo) { try { @@ -58,6 +60,8 @@ foreach ($azureUsersInfo as $azureUserInfo) { 'id' ); + $existingUsers[] = $userId; + $userInfo = api_get_user_info($userId); printf("%s - UserInfo %s".PHP_EOL, time(), serialize($userInfo)); @@ -67,3 +71,25 @@ foreach ($azureUsersInfo as $azureUserInfo) { continue; } } + +if ('true' === $plugin->get(AzureActiveDirectory::SETTING_DEACTIVATE_NONEXISTING_USERS)) { + echo '----------------'.PHP_EOL; + printf('Trying deactivate non-existing users in Azure.'.PHP_EOL, time()); + + $users = UserManager::getRepository()->findByAuthSource('azure'); + $userIdList = array_map( + function ($user) { + return $user->getId(); + }, + $users + ); + + $nonExistingUsers = array_diff($userIdList, $existingUsers); + + UserManager::deactivate_users($nonExistingUsers); + printf( + "%d - Deactivated users IDs: %s".PHP_EOL, + time(), + implode(', ', $nonExistingUsers) + ); +} diff --git a/src/Chamilo/UserBundle/Repository/UserRepository.php b/src/Chamilo/UserBundle/Repository/UserRepository.php index 8ddbf2ce42..dae382a02f 100644 --- a/src/Chamilo/UserBundle/Repository/UserRepository.php +++ b/src/Chamilo/UserBundle/Repository/UserRepository.php @@ -1382,4 +1382,9 @@ class UserRepository extends EntityRepository ->getQuery() ->getOneOrNullResult(); } + + public function findByAuthSource(string $authSource): array + { + return $this->findBy(['authSource' => $authSource]); + } } From 7df539554beeccf94d18086c1fdec3aee70f2ec3 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:21:03 -0500 Subject: [PATCH 07/22] Plugin: Azure: Add script to sync groups from Azure - refs BT#21930 --- .../src/AzureActiveDirectory.php | 11 +++ .../src/scripts/sync_usergroups.php | 97 +++++++++++++++++++ .../src/scripts/sync_users.php | 6 +- 3 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 plugin/azure_active_directory/src/scripts/sync_usergroups.php diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 7399344b19..053e4aba4f 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -98,6 +98,17 @@ class AzureActiveDirectory extends Plugin return $provider; } + public function getProviderForApiGraph(): Azure + { + $provider = $this->getProvider(); + $provider->urlAPI = "https://graph.microsoft.com/v1.0/"; + $provider->resource = "https://graph.microsoft.com/"; + $provider->tenant = $this->get(AzureActiveDirectory::SETTING_TENANT_ID); + $provider->authWithResource = false; + + return $provider; + } + /** * @param string $urlType Type of URL to generate * diff --git a/plugin/azure_active_directory/src/scripts/sync_usergroups.php b/plugin/azure_active_directory/src/scripts/sync_usergroups.php new file mode 100644 index 0000000000..c91b7e823f --- /dev/null +++ b/plugin/azure_active_directory/src/scripts/sync_usergroups.php @@ -0,0 +1,97 @@ +getProviderForApiGraph(); + +echo 'Synchronizing groups from Azure.'.PHP_EOL; + +try { + $token = $provider->getAccessToken( + 'client_credentials', + ['resource' => $provider->resource] + ); + + $groupFields = [ + 'id', + 'displayName', + 'description', + ]; + + $azureGroupsInfo = $provider->get( + 'groups?$select='.implode(',', $groupFields), + $token + ); +} catch (Exception $e) { + printf("%s - %s".PHP_EOL, time(), $e->getMessage()); + die; +} + +printf("%s - Number of groups obtained %d".PHP_EOL, time(), count($azureGroupsInfo)); + +/** @var array $azureGroupInfo */ +foreach ($azureGroupsInfo as $azureGroupInfo) { + $usergroup = new UserGroup(); + + $exists = $usergroup->usergroup_exists($azureGroupInfo['displayName']); + + if (!$exists) { + $groupId = $usergroup->save([ + 'name' => $azureGroupInfo['displayName'], + 'description' => $azureGroupInfo['description'], + ]); + + if ($groupId) { + printf('%d - Class created: %s'.PHP_EOL, time(), $azureGroupInfo['displayName']); + } + } else { + $groupId = $usergroup->getIdByName($azureGroupInfo['displayName']); + + if ($groupId) { + $usergroup->subscribe_users_to_usergroup($groupId, []); + + printf('%d - Class exists, all users unsubscribed: %s'.PHP_EOL, time(), $azureGroupInfo['displayName']); + } + } + + try { + $userFields = [ + 'mail', + 'mailNickname', + 'id' + ]; + $azureGroupMembers = $provider->get( + sprintf('groups/%s/members?$select=%s', $azureGroupInfo['id'], implode(',', $userFields)), + $token + ); + } catch (Exception $e) { + printf("%s - %s".PHP_EOL, time(), $e->getMessage()); + + continue; + } + + $newGroupMembers = []; + + foreach ($azureGroupMembers as $azureGroupMember) { + $userId = $plugin->getUserIdByVerificationOrder($azureGroupMember, 'id'); + + if ($userId) { + $newGroupMembers[] = $userId; + } + } + + $usergroup->subscribe_users_to_usergroup($groupId, $newGroupMembers); + printf( + '%d - User IDs subscribed in class %s: %s'.PHP_EOL, + time(), + $azureGroupInfo['displayName'], + implode(', ', $newGroupMembers) + ); +} diff --git a/plugin/azure_active_directory/src/scripts/sync_users.php b/plugin/azure_active_directory/src/scripts/sync_users.php index af9fc764fd..82d9de6c93 100644 --- a/plugin/azure_active_directory/src/scripts/sync_users.php +++ b/plugin/azure_active_directory/src/scripts/sync_users.php @@ -9,11 +9,7 @@ if (PHP_SAPI !== 'cli') { $plugin = AzureActiveDirectory::create(); -$provider = $plugin->getProvider(); -$provider->urlAPI = "https://graph.microsoft.com/v1.0/"; -$provider->resource = "https://graph.microsoft.com/"; -$provider->tenant = $plugin->get(AzureActiveDirectory::SETTING_TENANT_ID); -$provider->authWithResource = false; +$provider = $plugin->getProviderForApiGraph(); echo 'Synchronizing users from Azure.'.PHP_EOL; From 22afc8eab5ac27d3edaee77826ca9f5c64e9504f Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:42:15 -0500 Subject: [PATCH 08/22] Plugin: Azure: Bump version to v2.4 - refs BT#21930 --- plugin/azure_active_directory/CHANGELOG.md | 21 +++++++++++++++++++ .../src/AzureActiveDirectory.php | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/plugin/azure_active_directory/CHANGELOG.md b/plugin/azure_active_directory/CHANGELOG.md index 8185067329..cb19ecdeb9 100644 --- a/plugin/azure_active_directory/CHANGELOG.md +++ b/plugin/azure_active_directory/CHANGELOG.md @@ -1,5 +1,26 @@ # Azure Active Directory Changelog +## 2.4 - 2024-08-28 + +* Added a new user extra field to save the unique Azure ID (internal UID). +This requires manually doing the following changes to your database if you are upgrading from v2.3 +```sql +INSERT INTO extra_field (extra_field_type, field_type, variable, display_text, default_value, field_order, visible_to_self, visible_to_others, changeable, filter, created_at) VALUES (1, 1, 'azure_uid', 'Azure UID (internal ID)', '', 1, null, null, null, null, '2024-08-28 00:00:00'); +``` +* Added a new option to set the order to verify the existing user in Chamilo +```sql +INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_existing_user_verification_order', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', '', '', 1, 1, 0); +``` +* Added a new option to update user info during the login proccess. +```sql +INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_update_users', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', '', '', 1, 1, 0); +``` +* Added new scripts to syncronize users and groups with users and usergroups (classes). And an option to deactivate accounts in Chamilo that do not exist in Azure. +```sql +INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_tenant_id', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', '', '', 1, 1, 0); +INSERT INTO settings_current (variable, subkey, type, category, selected_value, title, comment, scope, subkeytext, access_url, access_url_changeable, access_url_locked) VALUES ('azure_active_directory_deactivate_nonexisting_users', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', '', '', 1, 1, 0); +``` + ## 2.3 - 2021-03-30 * Added admin, session admin and teacher groups. This requires adding the following fields to your database if diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 053e4aba4f..454150986e 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -59,7 +59,7 @@ class AzureActiveDirectory extends Plugin self::SETTING_DEACTIVATE_NONEXISTING_USERS => 'boolean', ]; - parent::__construct('2.3', 'Angel Fernando Quiroz Campos, Yannick Warnier', $settings); + parent::__construct('2.4', 'Angel Fernando Quiroz Campos, Yannick Warnier', $settings); } /** From ca32e13288747ff58d113b9975d84fa29704dfbf Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:25:38 -0500 Subject: [PATCH 09/22] Plugin: Azure: Refactor to get paginated results when syncing users - refs BT#21930 --- .../src/AzureActiveDirectory.php | 2 + .../src/AzureCommand.php | 23 ++++ .../src/AzureSyncUsersCommand.php | 116 ++++++++++++++++++ .../src/scripts/sync_users.php | 85 +------------ 4 files changed, 147 insertions(+), 79 deletions(-) create mode 100644 plugin/azure_active_directory/src/AzureCommand.php create mode 100644 plugin/azure_active_directory/src/AzureSyncUsersCommand.php diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 454150986e..4bc55054a4 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -36,6 +36,8 @@ class AzureActiveDirectory extends Plugin public const EXTRA_FIELD_AZURE_ID = 'azure_id'; public const EXTRA_FIELD_AZURE_UID = 'azure_uid'; + public const API_PAGE_SIZE = 100; + /** * AzureActiveDirectory constructor. */ diff --git a/plugin/azure_active_directory/src/AzureCommand.php b/plugin/azure_active_directory/src/AzureCommand.php new file mode 100644 index 0000000000..60e6f44f97 --- /dev/null +++ b/plugin/azure_active_directory/src/AzureCommand.php @@ -0,0 +1,23 @@ +plugin = AzureActiveDirectory::create(); + $this->provider = $this->plugin->getProviderForApiGraph(); + } +} diff --git a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php new file mode 100644 index 0000000000..826c22eb3b --- /dev/null +++ b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php @@ -0,0 +1,116 @@ + + * @throws Exception + */ + public function __invoke(): Generator + { + yield 'Synchronizing users from Azure.'; + + $token = $this->provider->getAccessToken( + 'client_credentials', + ['resource' => $this->provider->resource] + ); + + $existingUsers = []; + + foreach ($this->getAzureUsers($token) as $azureUserInfo) { + try { + $userId = $this->plugin->registerUser( + $token, + $this->provider, + $azureUserInfo, + 'users/' . $azureUserInfo['id'] . '/memberOf', + 'id', + 'id' + ); + } catch (Exception $e) { + yield $e->getMessage(); + + continue; + } + + $existingUsers[] = $userId; + + $userInfo = api_get_user_info($userId); + + yield sprintf('User info: %s', serialize($userInfo)); + } + + if ('true' === $this->plugin->get(AzureActiveDirectory::SETTING_DEACTIVATE_NONEXISTING_USERS)) { + yield '----------------'; + + yield 'Trying deactivate non-existing users in Azure'; + + $users = UserManager::getRepository()->findByAuthSource('azure'); + $userIdList = array_map( + function ($user) { + return $user->getId(); + }, + $users + ); + + $nonExistingUsers = array_diff($userIdList, $existingUsers); + + UserManager::deactivate_users($nonExistingUsers); + + yield sprintf( + 'Deactivated users IDs: %s', + implode(', ', $nonExistingUsers) + ); + } + } + + /** + * @return Generator> + * @throws Exception + */ + private function getAzureUsers(AccessTokenInterface $token): Generator + { + $userFields = [ + 'givenName', + 'surname', + 'mail', + 'userPrincipalName', + 'businessPhones', + 'mobilePhone', + 'accountEnabled', + 'mailNickname', + 'id' + ]; + + $query = sprintf( + '$top=%d&$select=%s', + AzureActiveDirectory::API_PAGE_SIZE, + implode(',', $userFields) + ); + + do { + try { + $azureUsersRequest = $this->provider->request('get', "users?$query", $token); + } catch (Exception $e) { + throw new Exception('Exception when requesting users from Azure: '.$e->getMessage()); + } + + $azureUsersInfo = $azureUsersRequest['value'] ?? []; + + foreach ($azureUsersInfo as $azureUserInfo) { + yield $azureUserInfo; + } + + $hasNextLink = false; + + if (!empty($azureUsersRequest['@odata.nextLink'])) { + $hasNextLink = true; + $query = parse_url($azureUsersRequest['@odata.nextLink'], PHP_URL_QUERY); + } + } while ($hasNextLink); + } +} diff --git a/plugin/azure_active_directory/src/scripts/sync_users.php b/plugin/azure_active_directory/src/scripts/sync_users.php index 82d9de6c93..350848b004 100644 --- a/plugin/azure_active_directory/src/scripts/sync_users.php +++ b/plugin/azure_active_directory/src/scripts/sync_users.php @@ -1,91 +1,18 @@ getProviderForApiGraph(); - -echo 'Synchronizing users from Azure.'.PHP_EOL; +$command = new AzureSyncUsersCommand(); try { - $token = $provider->getAccessToken( - 'client_credentials', - ['resource' => $provider->resource] - ); - - $userFields = [ - 'givenName', - 'surname', - 'mail', - 'userPrincipalName', - 'businessPhones', - 'mobilePhone', - 'accountEnabled', - 'mailNickname', - 'id' - ]; - - $azureUsersInfo = $provider->get( - 'users?$select='.implode(',', $userFields), - $token - ); -} catch (Exception $e) { - printf("%s - %s".PHP_EOL, time(), $e->getMessage()); - die; -} - -printf("%s - Number of users obtained %d".PHP_EOL, time(), count($azureUsersInfo)); - -$existingUsers = []; - -/** @var array $user */ -foreach ($azureUsersInfo as $azureUserInfo) { - try { - $userId = $plugin->registerUser( - $token, - $provider, - $azureUserInfo, - 'users/' . $azureUserInfo['id'] . '/memberOf', - 'id', - 'id' - ); - - $existingUsers[] = $userId; - - $userInfo = api_get_user_info($userId); - - printf("%s - UserInfo %s".PHP_EOL, time(), serialize($userInfo)); - } catch (Exception $e) { - printf("%s - %s".PHP_EOL, time(), $e->getMessage()); - - continue; + foreach ($command() as $str) { + printf("%d - %s".PHP_EOL, time(), $str); } -} - -if ('true' === $plugin->get(AzureActiveDirectory::SETTING_DEACTIVATE_NONEXISTING_USERS)) { - echo '----------------'.PHP_EOL; - printf('Trying deactivate non-existing users in Azure.'.PHP_EOL, time()); - - $users = UserManager::getRepository()->findByAuthSource('azure'); - $userIdList = array_map( - function ($user) { - return $user->getId(); - }, - $users - ); - - $nonExistingUsers = array_diff($userIdList, $existingUsers); - - UserManager::deactivate_users($nonExistingUsers); - printf( - "%d - Deactivated users IDs: %s".PHP_EOL, - time(), - implode(', ', $nonExistingUsers) - ); +} catch (Exception $e) { + printf('%s - Exception: %s'.PHP_EOL, time(), $e->getMessage()); } From 091a9d29af8934bbae7cb1d0364aec2d3b534bb8 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:52:58 -0500 Subject: [PATCH 10/22] Plugin: Azure: Refactor to get paginated results when syncing user groups - refs BT#21930 --- .../src/AzureSyncUsergroupsCommand.php | 143 ++++++++++++++++++ .../src/scripts/sync_usergroups.php | 91 +---------- 2 files changed, 149 insertions(+), 85 deletions(-) create mode 100644 plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php diff --git a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php new file mode 100644 index 0000000000..9102c9d82c --- /dev/null +++ b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php @@ -0,0 +1,143 @@ + + * @throws Exception + */ + public function __invoke(): Generator + { + yield 'Synchronizing groups from Azure.'; + + $token = $this->provider->getAccessToken( + 'client_credentials', + ['resource' => $this->provider->resource] + ); + + foreach ($this->getAzureGroups($token) as $azureGroupInfo) { + $usergroup = new UserGroup(); + + if ($usergroup->usergroup_exists($azureGroupInfo['displayName'])) { + $groupId = $usergroup->getIdByName($azureGroupInfo['displayName']); + + if ($groupId) { + $usergroup->subscribe_users_to_usergroup($groupId, []); + + yield sprintf('Class exists, all users unsubscribed: %s', $azureGroupInfo['displayName']); + } + } else { + $groupId = $usergroup->save([ + 'name' => $azureGroupInfo['displayName'], + 'description' => $azureGroupInfo['description'], + ]); + + if ($groupId) { + yield sprintf('Class created: %s', $azureGroupInfo['displayName']); + } + } + + $newGroupMembers = []; + + foreach ($this->getAzureGroupMembers($token, $azureGroupInfo['id']) as $azureGroupMember) { + if ($userId = $this->plugin->getUserIdByVerificationOrder($azureGroupMember, 'id')) { + $newGroupMembers[] = $userId; + } + } + + $usergroup->subscribe_users_to_usergroup($groupId, $newGroupMembers); + + yield sprintf( + 'User IDs subscribed in class %s: %s', + $azureGroupInfo['displayName'], + implode(', ', $newGroupMembers) + ); + } + } + + /** + * @return Generator> + * @throws Exception + */ + private function getAzureGroups(AccessTokenInterface $token): Generator + { + $groupFields = [ + 'id', + 'displayName', + 'description', + ]; + + $query = sprintf( + '$top=%d&$select=%s', + AzureActiveDirectory::API_PAGE_SIZE, + implode(',', $groupFields) + ); + + do { + try { + $azureGroupsRequest = $this->provider->request('get', "groups?$query", $token); + } catch (Exception $e) { + throw new Exception('Exception when requesting groups from Azure: '.$e->getMessage()); + } + + $azureGroupsInfo = $azureGroupsRequest['value'] ?? []; + + foreach ($azureGroupsInfo as $azureGroupInfo) { + yield $azureGroupInfo; + } + + $hasNextLink = false; + + if (!empty($azureGroupsRequest['@odata.nextLink'])) { + $hasNextLink = true; + $query = parse_url($azureGroupsRequest['@odata.nextLink'], PHP_URL_QUERY); + } + } while($hasNextLink); + } + + /** + * @return Generator> + * @throws Exception + */ + private function getAzureGroupMembers(AccessTokenInterface $token, string $groupObjectId): Generator + { + $userFields = [ + 'mail', + 'mailNickname', + 'id' + ]; + $query = sprintf( + '$top=%d&$select=%s', + AzureActiveDirectory::API_PAGE_SIZE, + implode(',', $userFields) + ); + $hasNextLink = false; + + do { + try { + $azureGroupMembersRequest = $this->provider->request( + 'get', + "groups/$groupObjectId/members?$query", + $token + ); + } catch (Exception $e) { + throw new Exception('Exception when requesting group members from Azure: '.$e->getMessage()); + } + + $azureGroupMembers = $azureGroupMembersRequest['value'] ?? []; + + foreach ($azureGroupMembers as $azureGroupMember) { + yield $azureGroupMember; + } + + if (!empty($azureGroupMembersRequest['@odata.nextLink'])) { + $hasNextLink = true; + $query = parse_url($azureGroupMembersRequest['@odata.nextLink'], PHP_URL_QUERY); + } + } while ($hasNextLink); + } +} diff --git a/plugin/azure_active_directory/src/scripts/sync_usergroups.php b/plugin/azure_active_directory/src/scripts/sync_usergroups.php index c91b7e823f..f215a950e6 100644 --- a/plugin/azure_active_directory/src/scripts/sync_usergroups.php +++ b/plugin/azure_active_directory/src/scripts/sync_usergroups.php @@ -1,97 +1,18 @@ getProviderForApiGraph(); - -echo 'Synchronizing groups from Azure.'.PHP_EOL; +$command = new AzureSyncUsergroupsCommand(); try { - $token = $provider->getAccessToken( - 'client_credentials', - ['resource' => $provider->resource] - ); - - $groupFields = [ - 'id', - 'displayName', - 'description', - ]; - - $azureGroupsInfo = $provider->get( - 'groups?$select='.implode(',', $groupFields), - $token - ); -} catch (Exception $e) { - printf("%s - %s".PHP_EOL, time(), $e->getMessage()); - die; -} - -printf("%s - Number of groups obtained %d".PHP_EOL, time(), count($azureGroupsInfo)); - -/** @var array $azureGroupInfo */ -foreach ($azureGroupsInfo as $azureGroupInfo) { - $usergroup = new UserGroup(); - - $exists = $usergroup->usergroup_exists($azureGroupInfo['displayName']); - - if (!$exists) { - $groupId = $usergroup->save([ - 'name' => $azureGroupInfo['displayName'], - 'description' => $azureGroupInfo['description'], - ]); - - if ($groupId) { - printf('%d - Class created: %s'.PHP_EOL, time(), $azureGroupInfo['displayName']); - } - } else { - $groupId = $usergroup->getIdByName($azureGroupInfo['displayName']); - - if ($groupId) { - $usergroup->subscribe_users_to_usergroup($groupId, []); - - printf('%d - Class exists, all users unsubscribed: %s'.PHP_EOL, time(), $azureGroupInfo['displayName']); - } - } - - try { - $userFields = [ - 'mail', - 'mailNickname', - 'id' - ]; - $azureGroupMembers = $provider->get( - sprintf('groups/%s/members?$select=%s', $azureGroupInfo['id'], implode(',', $userFields)), - $token - ); - } catch (Exception $e) { - printf("%s - %s".PHP_EOL, time(), $e->getMessage()); - - continue; + foreach ($command() as $str) { + printf("%d - %s".PHP_EOL, time(), $str); } - - $newGroupMembers = []; - - foreach ($azureGroupMembers as $azureGroupMember) { - $userId = $plugin->getUserIdByVerificationOrder($azureGroupMember, 'id'); - - if ($userId) { - $newGroupMembers[] = $userId; - } - } - - $usergroup->subscribe_users_to_usergroup($groupId, $newGroupMembers); - printf( - '%d - User IDs subscribed in class %s: %s'.PHP_EOL, - time(), - $azureGroupInfo['displayName'], - implode(', ', $newGroupMembers) - ); +} catch (Exception $e) { + printf('%s - Exception: %s'.PHP_EOL, time(), $e->getMessage()); } From 7e0862badee5f59e2486357629fc4cfe80346c19 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 4 Sep 2024 18:00:11 -0500 Subject: [PATCH 11/22] Plugin: Azure: Refactor conditions to register/update user - refs BT#21930 --- .../src/AzureActiveDirectory.php | 201 ++++++++++-------- 1 file changed, 109 insertions(+), 92 deletions(-) diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 4bc55054a4..9a05cbfa7e 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -227,104 +227,116 @@ class AzureActiveDirectory extends Plugin if (empty($userId)) { // If we didn't find the user - if ($this->get(self::SETTING_PROVISION_USERS) === 'true') { - [ - $firstNme, - $lastName, - $username, - $email, - $phone, - $authSource, - $active, - $extra, - $userRole, - $isAdmin, - ] = $this->formatUserData($token, $provider, $azureUserInfo, $apiGroupsRef, $objectIdKey, $azureUidKey); - - // If the option is set to create users, create it - $userId = UserManager::create_user( - $firstNme, - $lastName, - $userRole, - $email, - $username, - '', - null, - null, - $phone, - null, - $authSource, - null, - $active, - null, - $extra, - null, - null, - $isAdmin - ); - if (!$userId) { - throw new Exception(get_lang('UserNotAdded').' '.$azureUserInfo['userPrincipalName']); - } - } else { - throw new Exception('User not found when checking the extra fields from '.$azureUserInfo['mail'].' or '.$azureUserInfo['mailNickname'].' or '.$azureUserInfo[$azureUidKey].'.'); + if ($this->get(self::SETTING_PROVISION_USERS) !== 'true') { + throw new Exception('User not found when checking the extra fields from ' . $azureUserInfo['mail'] . ' or ' . $azureUserInfo['mailNickname'] . ' or ' . $azureUserInfo[$azureUidKey] . '.'); } - } else { - if ($this->get(self::SETTING_UPDATE_USERS) === 'true') { - [ - $firstNme, - $lastName, - $username, - $email, - $phone, - $authSource, - $active, - $extra, - $userRole, - $isAdmin, - ] = $this->formatUserData($token, $provider, $azureUserInfo, $apiGroupsRef, $objectIdKey, $azureUidKey); - - $userId = UserManager::update_user( - $userId, - $firstNme, - $lastName, - $username, - '', - $authSource, - $email, - $userRole, - null, - $phone, - null, - null, - $active, - null, - 0, - $extra - ); - - if (!$userId) { - throw new Exception(get_lang('CouldNotUpdateUser').' '.$azureUserInfo['userPrincipalName']); - } + + [ + $firstNme, + $lastName, + $username, + $email, + $phone, + $authSource, + $active, + $extra, + $userRole, + $isAdmin, + ] = $this->formatUserData($token, $provider, $azureUserInfo, $apiGroupsRef, $objectIdKey, $azureUidKey); + + // If the option is set to create users, create it + $userId = UserManager::create_user( + $firstNme, + $lastName, + $userRole, + $email, + $username, + '', + null, + null, + $phone, + null, + $authSource, + null, + $active, + null, + $extra, + null, + null, + $isAdmin + ); + + if (!$userId) { + throw new Exception(get_lang('UserNotAdded') . ' ' . $azureUserInfo['userPrincipalName']); + } + + return $userId; + } + + if ($this->get(self::SETTING_UPDATE_USERS) === 'true') { + [ + $firstNme, + $lastName, + $username, + $email, + $phone, + $authSource, + $active, + $extra, + $userRole, + $isAdmin, + ] = $this->formatUserData($token, $provider, $azureUserInfo, $apiGroupsRef, $objectIdKey, $azureUidKey); + + $userId = UserManager::update_user( + $userId, + $firstNme, + $lastName, + $username, + '', + $authSource, + $email, + $userRole, + null, + $phone, + null, + null, + $active, + null, + 0, + $extra + ); + + if (!$userId) { + throw new Exception(get_lang('CouldNotUpdateUser').' '.$azureUserInfo['userPrincipalName']); } } return $userId; } + /** + * @throws Exception + */ private function formatUserData( AccessTokenInterface $token, Azure $provider, array $azureUserInfo, string $apiGroupsRef, - string $objectIdKey, + string $groupObjectIdKey, string $azureUidKey ): array { - [$userRole, $isAdmin] = $this->getUserRoleAndCheckIsAdmin( - $token, - $provider, - $apiGroupsRef, - $objectIdKey - ); + try { + [$userRole, $isAdmin] = $this->getUserRoleAndCheckIsAdmin( + $token, + $provider, + $apiGroupsRef, + $groupObjectIdKey + ); + } catch (Exception $e) { + throw new Exception( + 'Exception when formatting user '.$azureUserInfo[$azureUidKey].' data: '.$e->getMessage() + ); + } $phone = null; @@ -363,15 +375,20 @@ class AzureActiveDirectory extends Plugin ]; } + /** + * @throws Exception + */ private function getUserRoleAndCheckIsAdmin( AccessTokenInterface $token, - Azure $provider = null, + Azure $provider, string $apiRef = 'me/memberOf', - string $objectIdKey = 'objectId' + string $groupObjectIdKey = 'objectId' ): array { - $provider = $provider ?: $this->getProvider(); - - $groups = $provider->get($apiRef, $token); + try { + $groups = $provider->get($apiRef, $token); + } catch (Exception $e) { + throw new Exception('Exception when requesting user groups from Azure: '.$e->getMessage()); + } // If any specific group ID has been defined for a specific role, use that // ID to give the user the right role @@ -381,12 +398,12 @@ class AzureActiveDirectory extends Plugin $userRole = STUDENT; $isAdmin = false; foreach ($groups as $group) { - if ($givenAdminGroup == $group[$objectIdKey]) { + if ($givenAdminGroup == $group[$groupObjectIdKey]) { $userRole = COURSEMANAGER; $isAdmin = true; - } elseif ($givenSessionAdminGroup == $group[$objectIdKey]) { + } elseif ($givenSessionAdminGroup == $group[$groupObjectIdKey]) { $userRole = SESSIONADMIN; - } elseif ($userRole != SESSIONADMIN && $givenTeacherGroup == $group[$objectIdKey]) { + } elseif ($userRole != SESSIONADMIN && $givenTeacherGroup == $group[$groupObjectIdKey]) { $userRole = COURSEMANAGER; } } From e3978e15d448375fa19a6ada1eae21e8c7036207 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 4 Sep 2024 18:32:35 -0500 Subject: [PATCH 12/22] Plugin: Azure: Reload plugin settings when running scripts - refs BT#21930 --- plugin/azure_active_directory/src/AzureCommand.php | 1 + plugin/azure_active_directory/src/scripts/sync_usergroups.php | 3 +++ plugin/azure_active_directory/src/scripts/sync_users.php | 3 +++ 3 files changed, 7 insertions(+) diff --git a/plugin/azure_active_directory/src/AzureCommand.php b/plugin/azure_active_directory/src/AzureCommand.php index 60e6f44f97..18ecc04284 100644 --- a/plugin/azure_active_directory/src/AzureCommand.php +++ b/plugin/azure_active_directory/src/AzureCommand.php @@ -18,6 +18,7 @@ class AzureCommand public function __construct() { $this->plugin = AzureActiveDirectory::create(); + $this->plugin->get_settings(true); $this->provider = $this->plugin->getProviderForApiGraph(); } } diff --git a/plugin/azure_active_directory/src/scripts/sync_usergroups.php b/plugin/azure_active_directory/src/scripts/sync_usergroups.php index f215a950e6..8ad128c5db 100644 --- a/plugin/azure_active_directory/src/scripts/sync_usergroups.php +++ b/plugin/azure_active_directory/src/scripts/sync_usergroups.php @@ -7,6 +7,9 @@ if (PHP_SAPI !== 'cli') { exit('Run this script through the command line or comment this line in the code'); } +// Uncomment to indicate the access url to get the plugin settings when using multi-url +//$_configuration['access_url'] = 1; + $command = new AzureSyncUsergroupsCommand(); try { diff --git a/plugin/azure_active_directory/src/scripts/sync_users.php b/plugin/azure_active_directory/src/scripts/sync_users.php index 350848b004..33eb4768e6 100644 --- a/plugin/azure_active_directory/src/scripts/sync_users.php +++ b/plugin/azure_active_directory/src/scripts/sync_users.php @@ -7,6 +7,9 @@ if (PHP_SAPI !== 'cli') { exit('Run this script through the command line or comment this line in the code'); } +// Uncomment to indicate the access url to get the plugin settings when using multi-url +//$_configuration['access_url'] = 1; + $command = new AzureSyncUsersCommand(); try { From 228c3dc8fd932e0cb31ed4467a70186ed152cedf Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 4 Sep 2024 18:33:28 -0500 Subject: [PATCH 13/22] Minor: Format code - refs BT#21930 --- .../src/AzureActiveDirectory.php | 11 +++++------ .../src/AzureSyncUsergroupsCommand.php | 13 ++++++++----- .../src/AzureSyncUsersCommand.php | 10 ++++++---- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 9a05cbfa7e..f3467cebea 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -180,7 +180,8 @@ class AzureActiveDirectory extends Plugin return $defaultOrder; } - public function getUserIdByVerificationOrder(array $azureUserData, string $azureUidKey = 'objectId'): ?int { + public function getUserIdByVerificationOrder(array $azureUserData, string $azureUidKey = 'objectId'): ?int + { $selectedOrder = $this->getExistingUserVerificationOrder(); $extraFieldValue = new ExtraFieldValue('user'); @@ -228,7 +229,7 @@ class AzureActiveDirectory extends Plugin if (empty($userId)) { // If we didn't find the user if ($this->get(self::SETTING_PROVISION_USERS) !== 'true') { - throw new Exception('User not found when checking the extra fields from ' . $azureUserInfo['mail'] . ' or ' . $azureUserInfo['mailNickname'] . ' or ' . $azureUserInfo[$azureUidKey] . '.'); + throw new Exception('User not found when checking the extra fields from '.$azureUserInfo['mail'].' or '.$azureUserInfo['mailNickname'].' or '.$azureUserInfo[$azureUidKey].'.'); } [ @@ -267,7 +268,7 @@ class AzureActiveDirectory extends Plugin ); if (!$userId) { - throw new Exception(get_lang('UserNotAdded') . ' ' . $azureUserInfo['userPrincipalName']); + throw new Exception(get_lang('UserNotAdded').' '.$azureUserInfo['userPrincipalName']); } return $userId; @@ -333,9 +334,7 @@ class AzureActiveDirectory extends Plugin $groupObjectIdKey ); } catch (Exception $e) { - throw new Exception( - 'Exception when formatting user '.$azureUserInfo[$azureUidKey].' data: '.$e->getMessage() - ); + throw new Exception('Exception when formatting user '.$azureUserInfo[$azureUidKey].' data: '.$e->getMessage()); } $phone = null; diff --git a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php index 9102c9d82c..6677683596 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php @@ -7,8 +7,9 @@ use League\OAuth2\Client\Token\AccessTokenInterface; class AzureSyncUsergroupsCommand extends AzureCommand { /** - * @return Generator * @throws Exception + * + * @return Generator */ public function __invoke(): Generator { @@ -60,8 +61,9 @@ class AzureSyncUsergroupsCommand extends AzureCommand } /** - * @return Generator> * @throws Exception + * + * @return Generator> */ private function getAzureGroups(AccessTokenInterface $token): Generator { @@ -96,19 +98,20 @@ class AzureSyncUsergroupsCommand extends AzureCommand $hasNextLink = true; $query = parse_url($azureGroupsRequest['@odata.nextLink'], PHP_URL_QUERY); } - } while($hasNextLink); + } while ($hasNextLink); } /** - * @return Generator> * @throws Exception + * + * @return Generator> */ private function getAzureGroupMembers(AccessTokenInterface $token, string $groupObjectId): Generator { $userFields = [ 'mail', 'mailNickname', - 'id' + 'id', ]; $query = sprintf( '$top=%d&$select=%s', diff --git a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php index 826c22eb3b..3e3cf3b11a 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php @@ -7,8 +7,9 @@ use League\OAuth2\Client\Token\AccessTokenInterface; class AzureSyncUsersCommand extends AzureCommand { /** - * @return Generator * @throws Exception + * + * @return Generator */ public function __invoke(): Generator { @@ -27,7 +28,7 @@ class AzureSyncUsersCommand extends AzureCommand $token, $this->provider, $azureUserInfo, - 'users/' . $azureUserInfo['id'] . '/memberOf', + 'users/'.$azureUserInfo['id'].'/memberOf', 'id', 'id' ); @@ -69,8 +70,9 @@ class AzureSyncUsersCommand extends AzureCommand } /** - * @return Generator> * @throws Exception + * + * @return Generator> */ private function getAzureUsers(AccessTokenInterface $token): Generator { @@ -83,7 +85,7 @@ class AzureSyncUsersCommand extends AzureCommand 'mobilePhone', 'accountEnabled', 'mailNickname', - 'id' + 'id', ]; $query = sprintf( From c9d99a60dbbbab38212dcd5fec29533da3ed5285 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:56:49 -0500 Subject: [PATCH 14/22] Plugin: Azure: Request a new access token when it expires - refs BT#21930 --- .../src/AzureActiveDirectory.php | 6 +++--- .../azure_active_directory/src/AzureCommand.php | 17 +++++++++++++++++ .../src/AzureSyncUsergroupsCommand.php | 9 +++++---- .../src/AzureSyncUsersCommand.php | 15 ++++++++++----- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index f3467cebea..88f1e079ab 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -213,7 +213,7 @@ class AzureActiveDirectory extends Plugin * @throws Exception */ public function registerUser( - AccessTokenInterface $token, + AccessTokenInterface &$token, Azure $provider, array $azureUserInfo, string $apiGroupsRef = 'me/memberOf', @@ -319,7 +319,7 @@ class AzureActiveDirectory extends Plugin * @throws Exception */ private function formatUserData( - AccessTokenInterface $token, + AccessTokenInterface &$token, Azure $provider, array $azureUserInfo, string $apiGroupsRef, @@ -378,7 +378,7 @@ class AzureActiveDirectory extends Plugin * @throws Exception */ private function getUserRoleAndCheckIsAdmin( - AccessTokenInterface $token, + AccessTokenInterface &$token, Azure $provider, string $apiRef = 'me/memberOf', string $groupObjectIdKey = 'objectId' diff --git a/plugin/azure_active_directory/src/AzureCommand.php b/plugin/azure_active_directory/src/AzureCommand.php index 18ecc04284..6ebd9ded2b 100644 --- a/plugin/azure_active_directory/src/AzureCommand.php +++ b/plugin/azure_active_directory/src/AzureCommand.php @@ -2,6 +2,8 @@ /* For license terms, see /license.txt */ +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; +use League\OAuth2\Client\Token\AccessTokenInterface; use TheNetworg\OAuth2\Client\Provider\Azure; class AzureCommand @@ -21,4 +23,19 @@ class AzureCommand $this->plugin->get_settings(true); $this->provider = $this->plugin->getProviderForApiGraph(); } + + /** + * @throws IdentityProviderException + */ + protected function getToken(?AccessTokenInterface $currentToken = null): AccessTokenInterface + { + if (!$currentToken || ($currentToken->getExpires() && !$currentToken->getRefreshToken())) { + return $this->provider->getAccessToken( + 'client_credentials', + ['resource' => $this->provider->resource] + ); + } + + return $currentToken; + } } diff --git a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php index 6677683596..26a6215d1c 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php @@ -15,10 +15,7 @@ class AzureSyncUsergroupsCommand extends AzureCommand { yield 'Synchronizing groups from Azure.'; - $token = $this->provider->getAccessToken( - 'client_credentials', - ['resource' => $this->provider->resource] - ); + $token = $this->getToken(); foreach ($this->getAzureGroups($token) as $azureGroupInfo) { $usergroup = new UserGroup(); @@ -80,6 +77,8 @@ class AzureSyncUsergroupsCommand extends AzureCommand ); do { + $token = $this->getToken($token); + try { $azureGroupsRequest = $this->provider->request('get', "groups?$query", $token); } catch (Exception $e) { @@ -121,6 +120,8 @@ class AzureSyncUsergroupsCommand extends AzureCommand $hasNextLink = false; do { + $token = $this->getToken($token); + try { $azureGroupMembersRequest = $this->provider->request( 'get', diff --git a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php index 3e3cf3b11a..6bfed0b792 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php @@ -15,15 +15,14 @@ class AzureSyncUsersCommand extends AzureCommand { yield 'Synchronizing users from Azure.'; - $token = $this->provider->getAccessToken( - 'client_credentials', - ['resource' => $this->provider->resource] - ); + $token = $this->getToken(); $existingUsers = []; foreach ($this->getAzureUsers($token) as $azureUserInfo) { try { + $token = $this->getToken($token); + $userId = $this->plugin->registerUser( $token, $this->provider, @@ -95,8 +94,14 @@ class AzureSyncUsersCommand extends AzureCommand ); do { + $token = $this->getToken($token); + try { - $azureUsersRequest = $this->provider->request('get', "users?$query", $token); + $azureUsersRequest = $this->provider->request( + 'get', + "users?$query", + $token + ); } catch (Exception $e) { throw new Exception('Exception when requesting users from Azure: '.$e->getMessage()); } From d2ebff9e0ed6000bac73f0eb19c89156353027df Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:08:15 -0500 Subject: [PATCH 15/22] Plugin: Azure: Optimize request when registering/updating user - refs BT#21930 --- .../src/AzureActiveDirectory.php | 42 +++++++++++-------- .../src/AzureCommand.php | 4 ++ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 88f1e079ab..4bb6b25f9c 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -383,27 +383,35 @@ class AzureActiveDirectory extends Plugin string $apiRef = 'me/memberOf', string $groupObjectIdKey = 'objectId' ): array { - try { - $groups = $provider->get($apiRef, $token); - } catch (Exception $e) { - throw new Exception('Exception when requesting user groups from Azure: '.$e->getMessage()); - } - // If any specific group ID has been defined for a specific role, use that // ID to give the user the right role - $givenAdminGroup = $this->get(self::SETTING_GROUP_ID_ADMIN); - $givenSessionAdminGroup = $this->get(self::SETTING_GROUP_ID_SESSION_ADMIN); - $givenTeacherGroup = $this->get(self::SETTING_GROUP_ID_TEACHER); $userRole = STUDENT; $isAdmin = false; - foreach ($groups as $group) { - if ($givenAdminGroup == $group[$groupObjectIdKey]) { - $userRole = COURSEMANAGER; - $isAdmin = true; - } elseif ($givenSessionAdminGroup == $group[$groupObjectIdKey]) { - $userRole = SESSIONADMIN; - } elseif ($userRole != SESSIONADMIN && $givenTeacherGroup == $group[$groupObjectIdKey]) { - $userRole = COURSEMANAGER; + + $groupRoles = [ + 'admin' => $this->get(self::SETTING_GROUP_ID_ADMIN), + 'sessionAdmin' => $this->get(self::SETTING_GROUP_ID_SESSION_ADMIN), + 'teacher' => $this->get(self::SETTING_GROUP_ID_TEACHER), + ]; + + if ($groupRoles = array_filter($groupRoles)) { + try { + $groups = $provider->get($apiRef, $token); + } catch (Exception $e) { + throw new Exception('Exception when requesting user groups from Azure: '.$e->getMessage()); + } + + foreach ($groups as $group) { + $groupId = $group[$groupObjectIdKey]; + + if (isset($groupRoles['admin']) && $groupRoles['admin'] === $groupId) { + $userRole = COURSEMANAGER; + $isAdmin = true; + } elseif (isset($groupRoles['sessionAdmin']) && $groupRoles['sessionAdmin'] === $groupId) { + $userRole = SESSIONADMIN; + } elseif (isset($groupRoles['teacher']) && $groupRoles['teacher'] === $groupId && $userRole !== SESSIONADMIN) { + $userRole = COURSEMANAGER; + } } } diff --git a/plugin/azure_active_directory/src/AzureCommand.php b/plugin/azure_active_directory/src/AzureCommand.php index 6ebd9ded2b..9151705aad 100644 --- a/plugin/azure_active_directory/src/AzureCommand.php +++ b/plugin/azure_active_directory/src/AzureCommand.php @@ -16,6 +16,10 @@ class AzureCommand * @var Azure */ protected $provider; + /** + * @var AccessTokenInterface + */ + private $token; public function __construct() { From a600f8bb1bebec7ef783fe06e50159fd58bc1161 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:18:56 -0500 Subject: [PATCH 16/22] Plugin: Azure: Increase page size for results - refs BT#21930 --- plugin/azure_active_directory/src/AzureActiveDirectory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index 4bb6b25f9c..d9374cbb1d 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -36,7 +36,7 @@ class AzureActiveDirectory extends Plugin public const EXTRA_FIELD_AZURE_ID = 'azure_id'; public const EXTRA_FIELD_AZURE_UID = 'azure_uid'; - public const API_PAGE_SIZE = 100; + public const API_PAGE_SIZE = 999; /** * AzureActiveDirectory constructor. From 95426d37c3b468c0974a507e8bbbb3401055206e Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:10:19 -0500 Subject: [PATCH 17/22] Plugin: Azure: Register users as student and then update their status according defined groups - refs BT#21930 --- main/inc/lib/usermanager.lib.php | 8 +- .../src/AzureActiveDirectory.php | 98 ++++++---------- .../src/AzureCommand.php | 17 ++- .../src/AzureSyncUsersCommand.php | 106 ++++++++++++++---- .../azure_active_directory/src/callback.php | 35 ++++-- 5 files changed, 164 insertions(+), 100 deletions(-) diff --git a/main/inc/lib/usermanager.lib.php b/main/inc/lib/usermanager.lib.php index 183f082b29..2893af7fd1 100755 --- a/main/inc/lib/usermanager.lib.php +++ b/main/inc/lib/usermanager.lib.php @@ -6250,7 +6250,7 @@ class UserManager return $icon_link; } - public static function addUserAsAdmin(User $user) + public static function addUserAsAdmin(User $user, bool $andFlush = true) { if ($user) { $userId = $user->getId(); @@ -6261,11 +6261,11 @@ class UserManager } $user->addRole('ROLE_SUPER_ADMIN'); - self::getManager()->updateUser($user, true); + self::getManager()->updateUser($user, $andFlush); } } - public static function removeUserAdmin(User $user) + public static function removeUserAdmin(User $user, bool $andFlush = true) { $userId = (int) $user->getId(); if (self::is_admin($userId)) { @@ -6273,7 +6273,7 @@ class UserManager $sql = "DELETE FROM $table WHERE user_id = $userId"; Database::query($sql); $user->removeRole('ROLE_SUPER_ADMIN'); - self::getManager()->updateUser($user, true); + self::getManager()->updateUser($user, $andFlush); } } diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index d9374cbb1d..d0f6e466ce 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -1,7 +1,7 @@ formatUserData($token, $provider, $azureUserInfo, $apiGroupsRef, $objectIdKey, $azureUidKey); + ] = $this->formatUserData($azureUserInfo, $azureUidKey); // If the option is set to create users, create it $userId = UserManager::create_user( $firstNme, $lastName, - $userRole, + STUDENT, $email, $username, '', @@ -263,8 +257,7 @@ class AzureActiveDirectory extends Plugin null, $extra, null, - null, - $isAdmin + null ); if (!$userId) { @@ -284,9 +277,7 @@ class AzureActiveDirectory extends Plugin $authSource, $active, $extra, - $userRole, - $isAdmin, - ] = $this->formatUserData($token, $provider, $azureUserInfo, $apiGroupsRef, $objectIdKey, $azureUidKey); + ] = $this->formatUserData($azureUserInfo, $azureUidKey); $userId = UserManager::update_user( $userId, @@ -296,7 +287,7 @@ class AzureActiveDirectory extends Plugin '', $authSource, $email, - $userRole, + STUDENT, null, $phone, null, @@ -319,24 +310,9 @@ class AzureActiveDirectory extends Plugin * @throws Exception */ private function formatUserData( - AccessTokenInterface &$token, - Azure $provider, array $azureUserInfo, - string $apiGroupsRef, - string $groupObjectIdKey, string $azureUidKey ): array { - try { - [$userRole, $isAdmin] = $this->getUserRoleAndCheckIsAdmin( - $token, - $provider, - $apiGroupsRef, - $groupObjectIdKey - ); - } catch (Exception $e) { - throw new Exception('Exception when formatting user '.$azureUserInfo[$azureUidKey].' data: '.$e->getMessage()); - } - $phone = null; if (isset($azureUserInfo['telephoneNumber'])) { @@ -369,52 +345,44 @@ class AzureActiveDirectory extends Plugin $authSource, $active, $extra, - $userRole, - $isAdmin, ]; } /** - * @throws Exception + * @return array */ - private function getUserRoleAndCheckIsAdmin( - AccessTokenInterface &$token, - Azure $provider, - string $apiRef = 'me/memberOf', - string $groupObjectIdKey = 'objectId' - ): array { - // If any specific group ID has been defined for a specific role, use that - // ID to give the user the right role - $userRole = STUDENT; - $isAdmin = false; - - $groupRoles = [ + public function getGroupUidByRole(): array + { + $groupUidList = [ 'admin' => $this->get(self::SETTING_GROUP_ID_ADMIN), 'sessionAdmin' => $this->get(self::SETTING_GROUP_ID_SESSION_ADMIN), 'teacher' => $this->get(self::SETTING_GROUP_ID_TEACHER), ]; - if ($groupRoles = array_filter($groupRoles)) { - try { - $groups = $provider->get($apiRef, $token); - } catch (Exception $e) { - throw new Exception('Exception when requesting user groups from Azure: '.$e->getMessage()); - } + return array_filter($groupUidList); + } - foreach ($groups as $group) { - $groupId = $group[$groupObjectIdKey]; - - if (isset($groupRoles['admin']) && $groupRoles['admin'] === $groupId) { - $userRole = COURSEMANAGER; - $isAdmin = true; - } elseif (isset($groupRoles['sessionAdmin']) && $groupRoles['sessionAdmin'] === $groupId) { - $userRole = SESSIONADMIN; - } elseif (isset($groupRoles['teacher']) && $groupRoles['teacher'] === $groupId && $userRole !== SESSIONADMIN) { - $userRole = COURSEMANAGER; - } - } - } + /** + * @return array + */ + public function getUpdateActionByRole(): array + { + return [ + 'admin' => function (User $user) { + $user->setStatus(COURSEMANAGER); + + UserManager::addUserAsAdmin($user, false); + }, + 'sessionAdmin' => function (User $user) { + $user->setStatus(SESSIONADMIN); - return [$userRole, $isAdmin]; + UserManager::removeUserAdmin($user, false); + }, + 'teacher' => function (User $user) { + $user->setStatus(COURSEMANAGER); + + UserManager::removeUserAdmin($user, false); + }, + ]; } } diff --git a/plugin/azure_active_directory/src/AzureCommand.php b/plugin/azure_active_directory/src/AzureCommand.php index 9151705aad..9790c174f1 100644 --- a/plugin/azure_active_directory/src/AzureCommand.php +++ b/plugin/azure_active_directory/src/AzureCommand.php @@ -16,10 +16,6 @@ class AzureCommand * @var Azure */ protected $provider; - /** - * @var AccessTokenInterface - */ - private $token; public function __construct() { @@ -42,4 +38,17 @@ class AzureCommand return $currentToken; } + + /** + * @throws IdentityProviderException + */ + protected function generateOrRefreshToken(?AccessTokenInterface &$token) + { + if (!$token || ($token->getExpires() && !$token->getRefreshToken())) { + $token = $this->provider->getAccessToken( + 'client_credentials', + ['resource' => $this->provider->resource] + ); + } + } } diff --git a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php index 6bfed0b792..fea4f77af1 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php @@ -2,7 +2,7 @@ /* For license terms, see /license.txt */ -use League\OAuth2\Client\Token\AccessTokenInterface; +use Chamilo\UserBundle\Entity\User; class AzureSyncUsersCommand extends AzureCommand { @@ -15,33 +15,54 @@ class AzureSyncUsersCommand extends AzureCommand { yield 'Synchronizing users from Azure.'; - $token = $this->getToken(); - + /** @var array $existingUsers */ $existingUsers = []; - foreach ($this->getAzureUsers($token) as $azureUserInfo) { + foreach ($this->getAzureUsers() as $azureUserInfo) { try { - $token = $this->getToken($token); - - $userId = $this->plugin->registerUser( - $token, - $this->provider, - $azureUserInfo, - 'users/'.$azureUserInfo['id'].'/memberOf', - 'id', - 'id' - ); + $userId = $this->plugin->registerUser($azureUserInfo, 'id'); } catch (Exception $e) { yield $e->getMessage(); continue; } - $existingUsers[] = $userId; + $existingUsers[$azureUserInfo['id']] = $userId; + + yield sprintf('User (ID %d) with received info: %s ', $userId, serialize($azureUserInfo)); + } + + yield '----------------'; + yield 'Updating users status'; + + $roleGroups = $this->plugin->getGroupUidByRole(); + $roleActions = $this->plugin->getUpdateActionByRole(); + + $userManager = UserManager::getManager(); + $em = Database::getManager(); + + foreach ($roleGroups as $userRole => $groupUid) { + $azureGroupMembersInfo = iterator_to_array($this->getAzureGroupMembers($groupUid)); + $azureGroupMembersUids = array_column($azureGroupMembersInfo, 'id'); + + foreach ($azureGroupMembersUids as $azureGroupMembersUid) { + $userId = $existingUsers[$azureGroupMembersUid] ?? null; + + if (!$userId) { + continue; + } + + if (isset($roleActions[$userRole])) { + /** @var User $user */ + $user = $userManager->find($userId); + + $roleActions[$userRole]($user); - $userInfo = api_get_user_info($userId); + yield sprintf('User (ID %d) status %s', $userId, $userRole); + } + } - yield sprintf('User info: %s', serialize($userInfo)); + $em->flush(); } if ('true' === $this->plugin->get(AzureActiveDirectory::SETTING_DEACTIVATE_NONEXISTING_USERS)) { @@ -73,7 +94,7 @@ class AzureSyncUsersCommand extends AzureCommand * * @return Generator> */ - private function getAzureUsers(AccessTokenInterface $token): Generator + private function getAzureUsers(): Generator { $userFields = [ 'givenName', @@ -93,8 +114,10 @@ class AzureSyncUsersCommand extends AzureCommand implode(',', $userFields) ); + $token = null; + do { - $token = $this->getToken($token); + $this->generateOrRefreshToken($token); try { $azureUsersRequest = $this->provider->request( @@ -120,4 +143,49 @@ class AzureSyncUsersCommand extends AzureCommand } } while ($hasNextLink); } + + /** + * @throws Exception + */ + public function getAzureGroupMembers(string $groupUid): Generator + { + $userFields = [ + 'id', + ]; + + $query = sprintf( + '$top=%d&$select=%s', + AzureActiveDirectory::API_PAGE_SIZE, + implode(',', $userFields) + ); + + $token = null; + + do { + $this->generateOrRefreshToken($token); + + try { + $azureGroupMembersRequest = $this->provider->request( + 'get', + "groups/$groupUid/members?$query", + $token + ); + } catch (Exception $e) { + throw new Exception('Exception when requesting group members from Azure: '.$e->getMessage()); + } + + $azureGroupMembers = $azureGroupMembersRequest['value'] ?? []; + + foreach ($azureGroupMembers as $azureGroupMember) { + yield $azureGroupMember; + } + + $hasNextLink = false; + + if (!empty($azureGroupMembersRequest['@odata.nextLink'])) { + $hasNextLink = true; + $query = parse_url($azureGroupMembersRequest['@odata.nextLink'], PHP_URL_QUERY); + } + } while ($hasNextLink); + } } diff --git a/plugin/azure_active_directory/src/callback.php b/plugin/azure_active_directory/src/callback.php index 09d0b363d6..6e59c9c812 100644 --- a/plugin/azure_active_directory/src/callback.php +++ b/plugin/azure_active_directory/src/callback.php @@ -4,6 +4,9 @@ * Callback script for Azure. The URL of this file is sent to Azure as a * point of contact to send particular signals. */ + +use Chamilo\UserBundle\Entity\User; + require __DIR__.'/../../../main/inc/global.inc.php'; if (!empty($_GET['error']) && !empty($_GET['state'])) { @@ -85,17 +88,33 @@ try { throw new Exception('The id field is empty in Azure AD and is needed to set the unique Azure ID for this user.'); } - $userId = $plugin->registerUser( - $token, - $provider, - $me - ); + $userId = $plugin->registerUser($me); - $userInfo = api_get_user_info($userId); + if ($roleGroups = $plugin->getGroupUidByRole()) { + $roleActions = $plugin->getUpdateActionByRole(); + /** @var User $user */ + $user = UserManager::getManager()->find($userId); + + $azureGroups = $provider->get('me/memberOf', $token); + + foreach ($azureGroups as $azureGroup) { + $azureGroupUid = $azureGroup['objectId']; - //TODO add user update management for groups + foreach ($roleGroups as $userRole => $groupUid) { + if ($azureGroupUid === $groupUid) { + $roleActions[$userRole]($user); + + break 2; + } + } + } + + Database::getManager()->flush(); + } + + $userInfo = api_get_user_info($userId); - //TODO add support if user exists in another URL but is validated in this one, add the user to access_url_rel_user + /* @TODO add support if user exists in another URL but is validated in this one, add the user to access_url_rel_user */ if (empty($userInfo)) { throw new Exception('User '.$userId.' not found.'); From 90588f255884b1f567c4217fc338e99c666e524b Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:49:10 -0500 Subject: [PATCH 18/22] Plugin: Azure: Fix script to sync user groups and divide process to subscriptions - refs BT#21930 --- .../src/AzureCommand.php | 62 ++++++++++---- .../src/AzureSyncUsergroupsCommand.php | 82 ++++++------------- .../src/AzureSyncUsersCommand.php | 45 ---------- 3 files changed, 73 insertions(+), 116 deletions(-) diff --git a/plugin/azure_active_directory/src/AzureCommand.php b/plugin/azure_active_directory/src/AzureCommand.php index 9790c174f1..6a14c94e07 100644 --- a/plugin/azure_active_directory/src/AzureCommand.php +++ b/plugin/azure_active_directory/src/AzureCommand.php @@ -6,7 +6,7 @@ use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Token\AccessTokenInterface; use TheNetworg\OAuth2\Client\Provider\Azure; -class AzureCommand +abstract class AzureCommand { /** * @var AzureActiveDirectory @@ -27,28 +27,62 @@ class AzureCommand /** * @throws IdentityProviderException */ - protected function getToken(?AccessTokenInterface $currentToken = null): AccessTokenInterface + protected function generateOrRefreshToken(?AccessTokenInterface &$token) { - if (!$currentToken || ($currentToken->getExpires() && !$currentToken->getRefreshToken())) { - return $this->provider->getAccessToken( + if (!$token || ($token->getExpires() && !$token->getRefreshToken())) { + $token = $this->provider->getAccessToken( 'client_credentials', ['resource' => $this->provider->resource] ); } - - return $currentToken; } /** - * @throws IdentityProviderException + * @return Generator> + * + * @throws Exception */ - protected function generateOrRefreshToken(?AccessTokenInterface &$token) + protected function getAzureGroupMembers(string $groupUid): Generator { - if (!$token || ($token->getExpires() && !$token->getRefreshToken())) { - $token = $this->provider->getAccessToken( - 'client_credentials', - ['resource' => $this->provider->resource] - ); - } + $userFields = [ + 'mail', + 'mailNickname', + 'id', + ]; + + $query = sprintf( + '$top=%d&$select=%s', + AzureActiveDirectory::API_PAGE_SIZE, + implode(',', $userFields) + ); + + $token = null; + + do { + $this->generateOrRefreshToken($token); + + try { + $azureGroupMembersRequest = $this->provider->request( + 'get', + "groups/$groupUid/members?$query", + $token + ); + } catch (Exception $e) { + throw new Exception('Exception when requesting group members from Azure: '.$e->getMessage()); + } + + $azureGroupMembers = $azureGroupMembersRequest['value'] ?? []; + + foreach ($azureGroupMembers as $azureGroupMember) { + yield $azureGroupMember; + } + + $hasNextLink = false; + + if (!empty($azureGroupMembersRequest['@odata.nextLink'])) { + $hasNextLink = true; + $query = parse_url($azureGroupMembersRequest['@odata.nextLink'], PHP_URL_QUERY); + } + } while ($hasNextLink); } } diff --git a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php index 26a6215d1c..2fe7c0508f 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php @@ -15,11 +15,11 @@ class AzureSyncUsergroupsCommand extends AzureCommand { yield 'Synchronizing groups from Azure.'; - $token = $this->getToken(); + $usergroup = new UserGroup(); - foreach ($this->getAzureGroups($token) as $azureGroupInfo) { - $usergroup = new UserGroup(); + $groupIdByUid = []; + foreach ($this->getAzureGroups() as $azureGroupInfo) { if ($usergroup->usergroup_exists($azureGroupInfo['displayName'])) { $groupId = $usergroup->getIdByName($azureGroupInfo['displayName']); @@ -39,21 +39,32 @@ class AzureSyncUsergroupsCommand extends AzureCommand } } + $groupIdByUid[$azureGroupInfo['id']] = $groupId; + } + + yield '----------------'; + yield 'Subscribing users to groups'; + + foreach ($groupIdByUid as $azureGroupUid => $groupId) { $newGroupMembers = []; - foreach ($this->getAzureGroupMembers($token, $azureGroupInfo['id']) as $azureGroupMember) { + yield sprintf('Obtaining members for group (ID %d)', $groupId); + + foreach ($this->getAzureGroupMembers($azureGroupUid) as $azureGroupMember) { if ($userId = $this->plugin->getUserIdByVerificationOrder($azureGroupMember, 'id')) { $newGroupMembers[] = $userId; } } - $usergroup->subscribe_users_to_usergroup($groupId, $newGroupMembers); + if ($newGroupMembers) { + $usergroup->subscribe_users_to_usergroup($groupId, $newGroupMembers); - yield sprintf( - 'User IDs subscribed in class %s: %s', - $azureGroupInfo['displayName'], - implode(', ', $newGroupMembers) - ); + yield sprintf( + 'User IDs subscribed in class (ID %d): %s', + $groupId, + implode(', ', $newGroupMembers) + ); + } } } @@ -62,7 +73,7 @@ class AzureSyncUsergroupsCommand extends AzureCommand * * @return Generator> */ - private function getAzureGroups(AccessTokenInterface $token): Generator + private function getAzureGroups(): Generator { $groupFields = [ 'id', @@ -76,8 +87,10 @@ class AzureSyncUsergroupsCommand extends AzureCommand implode(',', $groupFields) ); + $token = null; + do { - $token = $this->getToken($token); + $this->generateOrRefreshToken($token); try { $azureGroupsRequest = $this->provider->request('get', "groups?$query", $token); @@ -99,49 +112,4 @@ class AzureSyncUsergroupsCommand extends AzureCommand } } while ($hasNextLink); } - - /** - * @throws Exception - * - * @return Generator> - */ - private function getAzureGroupMembers(AccessTokenInterface $token, string $groupObjectId): Generator - { - $userFields = [ - 'mail', - 'mailNickname', - 'id', - ]; - $query = sprintf( - '$top=%d&$select=%s', - AzureActiveDirectory::API_PAGE_SIZE, - implode(',', $userFields) - ); - $hasNextLink = false; - - do { - $token = $this->getToken($token); - - try { - $azureGroupMembersRequest = $this->provider->request( - 'get', - "groups/$groupObjectId/members?$query", - $token - ); - } catch (Exception $e) { - throw new Exception('Exception when requesting group members from Azure: '.$e->getMessage()); - } - - $azureGroupMembers = $azureGroupMembersRequest['value'] ?? []; - - foreach ($azureGroupMembers as $azureGroupMember) { - yield $azureGroupMember; - } - - if (!empty($azureGroupMembersRequest['@odata.nextLink'])) { - $hasNextLink = true; - $query = parse_url($azureGroupMembersRequest['@odata.nextLink'], PHP_URL_QUERY); - } - } while ($hasNextLink); - } } diff --git a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php index fea4f77af1..97bf5f1bb4 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php @@ -143,49 +143,4 @@ class AzureSyncUsersCommand extends AzureCommand } } while ($hasNextLink); } - - /** - * @throws Exception - */ - public function getAzureGroupMembers(string $groupUid): Generator - { - $userFields = [ - 'id', - ]; - - $query = sprintf( - '$top=%d&$select=%s', - AzureActiveDirectory::API_PAGE_SIZE, - implode(',', $userFields) - ); - - $token = null; - - do { - $this->generateOrRefreshToken($token); - - try { - $azureGroupMembersRequest = $this->provider->request( - 'get', - "groups/$groupUid/members?$query", - $token - ); - } catch (Exception $e) { - throw new Exception('Exception when requesting group members from Azure: '.$e->getMessage()); - } - - $azureGroupMembers = $azureGroupMembersRequest['value'] ?? []; - - foreach ($azureGroupMembers as $azureGroupMember) { - yield $azureGroupMember; - } - - $hasNextLink = false; - - if (!empty($azureGroupMembersRequest['@odata.nextLink'])) { - $hasNextLink = true; - $query = parse_url($azureGroupMembersRequest['@odata.nextLink'], PHP_URL_QUERY); - } - } while ($hasNextLink); - } } From 274f9f28cd6deb461c8da12a0ce746b9ebdefb6a Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:53:13 -0500 Subject: [PATCH 19/22] Plugin: Azure: Move methods to parent class - refs BT#21930 --- .../src/AzureCommand.php | 100 ++++++++++++++++++ .../src/AzureSyncUsergroupsCommand.php | 45 -------- .../src/AzureSyncUsersCommand.php | 55 ---------- 3 files changed, 100 insertions(+), 100 deletions(-) diff --git a/plugin/azure_active_directory/src/AzureCommand.php b/plugin/azure_active_directory/src/AzureCommand.php index 6a14c94e07..1c0ccd3e7b 100644 --- a/plugin/azure_active_directory/src/AzureCommand.php +++ b/plugin/azure_active_directory/src/AzureCommand.php @@ -37,6 +37,106 @@ abstract class AzureCommand } } + /** + * @throws Exception + * + * @return Generator> + */ + protected function getAzureUsers(): Generator + { + $userFields = [ + 'givenName', + 'surname', + 'mail', + 'userPrincipalName', + 'businessPhones', + 'mobilePhone', + 'accountEnabled', + 'mailNickname', + 'id', + ]; + + $query = sprintf( + '$top=%d&$select=%s', + AzureActiveDirectory::API_PAGE_SIZE, + implode(',', $userFields) + ); + + $token = null; + + do { + $this->generateOrRefreshToken($token); + + try { + $azureUsersRequest = $this->provider->request( + 'get', + "users?$query", + $token + ); + } catch (Exception $e) { + throw new Exception('Exception when requesting users from Azure: '.$e->getMessage()); + } + + $azureUsersInfo = $azureUsersRequest['value'] ?? []; + + foreach ($azureUsersInfo as $azureUserInfo) { + yield $azureUserInfo; + } + + $hasNextLink = false; + + if (!empty($azureUsersRequest['@odata.nextLink'])) { + $hasNextLink = true; + $query = parse_url($azureUsersRequest['@odata.nextLink'], PHP_URL_QUERY); + } + } while ($hasNextLink); + } + + /** + * @throws Exception + * + * @return Generator> + */ + protected function getAzureGroups(): Generator + { + $groupFields = [ + 'id', + 'displayName', + 'description', + ]; + + $query = sprintf( + '$top=%d&$select=%s', + AzureActiveDirectory::API_PAGE_SIZE, + implode(',', $groupFields) + ); + + $token = null; + + do { + $this->generateOrRefreshToken($token); + + try { + $azureGroupsRequest = $this->provider->request('get', "groups?$query", $token); + } catch (Exception $e) { + throw new Exception('Exception when requesting groups from Azure: '.$e->getMessage()); + } + + $azureGroupsInfo = $azureGroupsRequest['value'] ?? []; + + foreach ($azureGroupsInfo as $azureGroupInfo) { + yield $azureGroupInfo; + } + + $hasNextLink = false; + + if (!empty($azureGroupsRequest['@odata.nextLink'])) { + $hasNextLink = true; + $query = parse_url($azureGroupsRequest['@odata.nextLink'], PHP_URL_QUERY); + } + } while ($hasNextLink); + } + /** * @return Generator> * diff --git a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php index 2fe7c0508f..3f51db2736 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php @@ -67,49 +67,4 @@ class AzureSyncUsergroupsCommand extends AzureCommand } } } - - /** - * @throws Exception - * - * @return Generator> - */ - private function getAzureGroups(): Generator - { - $groupFields = [ - 'id', - 'displayName', - 'description', - ]; - - $query = sprintf( - '$top=%d&$select=%s', - AzureActiveDirectory::API_PAGE_SIZE, - implode(',', $groupFields) - ); - - $token = null; - - do { - $this->generateOrRefreshToken($token); - - try { - $azureGroupsRequest = $this->provider->request('get', "groups?$query", $token); - } catch (Exception $e) { - throw new Exception('Exception when requesting groups from Azure: '.$e->getMessage()); - } - - $azureGroupsInfo = $azureGroupsRequest['value'] ?? []; - - foreach ($azureGroupsInfo as $azureGroupInfo) { - yield $azureGroupInfo; - } - - $hasNextLink = false; - - if (!empty($azureGroupsRequest['@odata.nextLink'])) { - $hasNextLink = true; - $query = parse_url($azureGroupsRequest['@odata.nextLink'], PHP_URL_QUERY); - } - } while ($hasNextLink); - } } diff --git a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php index 97bf5f1bb4..748204680b 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php @@ -88,59 +88,4 @@ class AzureSyncUsersCommand extends AzureCommand ); } } - - /** - * @throws Exception - * - * @return Generator> - */ - private function getAzureUsers(): Generator - { - $userFields = [ - 'givenName', - 'surname', - 'mail', - 'userPrincipalName', - 'businessPhones', - 'mobilePhone', - 'accountEnabled', - 'mailNickname', - 'id', - ]; - - $query = sprintf( - '$top=%d&$select=%s', - AzureActiveDirectory::API_PAGE_SIZE, - implode(',', $userFields) - ); - - $token = null; - - do { - $this->generateOrRefreshToken($token); - - try { - $azureUsersRequest = $this->provider->request( - 'get', - "users?$query", - $token - ); - } catch (Exception $e) { - throw new Exception('Exception when requesting users from Azure: '.$e->getMessage()); - } - - $azureUsersInfo = $azureUsersRequest['value'] ?? []; - - foreach ($azureUsersInfo as $azureUserInfo) { - yield $azureUserInfo; - } - - $hasNextLink = false; - - if (!empty($azureUsersRequest['@odata.nextLink'])) { - $hasNextLink = true; - $query = parse_url($azureUsersRequest['@odata.nextLink'], PHP_URL_QUERY); - } - } while ($hasNextLink); - } } From 2b5eeb58ad79c954afa8d3da2046e454670e7802 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:10:48 -0500 Subject: [PATCH 20/22] Minor: Format code - refs BT#21930 --- .../src/AzureActiveDirectory.php | 76 +++++++++---------- .../src/AzureCommand.php | 4 +- .../src/AzureSyncUsergroupsCommand.php | 2 - 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/plugin/azure_active_directory/src/AzureActiveDirectory.php b/plugin/azure_active_directory/src/AzureActiveDirectory.php index d0f6e466ce..fce9cf9b68 100644 --- a/plugin/azure_active_directory/src/AzureActiveDirectory.php +++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php @@ -306,6 +306,44 @@ class AzureActiveDirectory extends Plugin return $userId; } + /** + * @return array + */ + public function getGroupUidByRole(): array + { + $groupUidList = [ + 'admin' => $this->get(self::SETTING_GROUP_ID_ADMIN), + 'sessionAdmin' => $this->get(self::SETTING_GROUP_ID_SESSION_ADMIN), + 'teacher' => $this->get(self::SETTING_GROUP_ID_TEACHER), + ]; + + return array_filter($groupUidList); + } + + /** + * @return array + */ + public function getUpdateActionByRole(): array + { + return [ + 'admin' => function (User $user) { + $user->setStatus(COURSEMANAGER); + + UserManager::addUserAsAdmin($user, false); + }, + 'sessionAdmin' => function (User $user) { + $user->setStatus(SESSIONADMIN); + + UserManager::removeUserAdmin($user, false); + }, + 'teacher' => function (User $user) { + $user->setStatus(COURSEMANAGER); + + UserManager::removeUserAdmin($user, false); + }, + ]; + } + /** * @throws Exception */ @@ -347,42 +385,4 @@ class AzureActiveDirectory extends Plugin $extra, ]; } - - /** - * @return array - */ - public function getGroupUidByRole(): array - { - $groupUidList = [ - 'admin' => $this->get(self::SETTING_GROUP_ID_ADMIN), - 'sessionAdmin' => $this->get(self::SETTING_GROUP_ID_SESSION_ADMIN), - 'teacher' => $this->get(self::SETTING_GROUP_ID_TEACHER), - ]; - - return array_filter($groupUidList); - } - - /** - * @return array - */ - public function getUpdateActionByRole(): array - { - return [ - 'admin' => function (User $user) { - $user->setStatus(COURSEMANAGER); - - UserManager::addUserAsAdmin($user, false); - }, - 'sessionAdmin' => function (User $user) { - $user->setStatus(SESSIONADMIN); - - UserManager::removeUserAdmin($user, false); - }, - 'teacher' => function (User $user) { - $user->setStatus(COURSEMANAGER); - - UserManager::removeUserAdmin($user, false); - }, - ]; - } } diff --git a/plugin/azure_active_directory/src/AzureCommand.php b/plugin/azure_active_directory/src/AzureCommand.php index 1c0ccd3e7b..ce79c45ca2 100644 --- a/plugin/azure_active_directory/src/AzureCommand.php +++ b/plugin/azure_active_directory/src/AzureCommand.php @@ -138,9 +138,9 @@ abstract class AzureCommand } /** - * @return Generator> - * * @throws Exception + * + * @return Generator> */ protected function getAzureGroupMembers(string $groupUid): Generator { diff --git a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php index 3f51db2736..f64975cd25 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php @@ -2,8 +2,6 @@ /* For license terms, see /license.txt */ -use League\OAuth2\Client\Token\AccessTokenInterface; - class AzureSyncUsergroupsCommand extends AzureCommand { /** From f5d563c36143d792a50e0940b6291ff5cbee54a0 Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos <1697880+AngelFQC@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:17:14 -0500 Subject: [PATCH 21/22] Plugin: Azure: Catch exception when getting group members - refs BT#21930 --- .../src/AzureSyncUsergroupsCommand.php | 12 +++++++++--- .../src/AzureSyncUsersCommand.php | 9 ++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php index f64975cd25..30087a16e6 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php @@ -48,10 +48,16 @@ class AzureSyncUsergroupsCommand extends AzureCommand yield sprintf('Obtaining members for group (ID %d)', $groupId); - foreach ($this->getAzureGroupMembers($azureGroupUid) as $azureGroupMember) { - if ($userId = $this->plugin->getUserIdByVerificationOrder($azureGroupMember, 'id')) { - $newGroupMembers[] = $userId; + try { + foreach ($this->getAzureGroupMembers($azureGroupUid) as $azureGroupMember) { + if ($userId = $this->plugin->getUserIdByVerificationOrder($azureGroupMember, 'id')) { + $newGroupMembers[] = $userId; + } } + } catch (Exception $e) { + yield $e->getMessage(); + + continue; } if ($newGroupMembers) { diff --git a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php index 748204680b..36b60f9a2f 100644 --- a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php +++ b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php @@ -42,7 +42,14 @@ class AzureSyncUsersCommand extends AzureCommand $em = Database::getManager(); foreach ($roleGroups as $userRole => $groupUid) { - $azureGroupMembersInfo = iterator_to_array($this->getAzureGroupMembers($groupUid)); + try { + $azureGroupMembersInfo = iterator_to_array($this->getAzureGroupMembers($groupUid)); + } catch (Exception $e) { + yield $e->getMessage(); + + continue; + } + $azureGroupMembersUids = array_column($azureGroupMembersInfo, 'id'); foreach ($azureGroupMembersUids as $azureGroupMembersUid) { From df28327bba235204f9012ec9f35f88e2a5ace0e3 Mon Sep 17 00:00:00 2001 From: Nicolas Ducoulombier Date: Mon, 23 Sep 2024 14:56:39 +0200 Subject: [PATCH 22/22] Plugin: Azure: adapt order for role verification to have first admin, then teacher to avoid setting a teacher role to an admin - refs BT#21500 --- plugin/azure_active_directory/src/callback.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugin/azure_active_directory/src/callback.php b/plugin/azure_active_directory/src/callback.php index 6e59c9c812..3bc85d443c 100644 --- a/plugin/azure_active_directory/src/callback.php +++ b/plugin/azure_active_directory/src/callback.php @@ -97,10 +97,9 @@ try { $azureGroups = $provider->get('me/memberOf', $token); - foreach ($azureGroups as $azureGroup) { - $azureGroupUid = $azureGroup['objectId']; - - foreach ($roleGroups as $userRole => $groupUid) { + foreach ($roleGroups as $userRole => $groupUid) { + foreach ($azureGroups as $azureGroup) { + $azureGroupUid = $azureGroup['objectId']; if ($azureGroupUid === $groupUid) { $roleActions[$userRole]($user);