diff --git a/main/auth/external_login/login.azure.php b/main/auth/external_login/login.azure.php
index 95acea4530f..e00da56c878 100644
--- a/main/auth/external_login/login.azure.php
+++ b/main/auth/external_login/login.azure.php
@@ -12,6 +12,16 @@
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/main/inc/lib/usermanager.lib.php b/main/inc/lib/usermanager.lib.php
index 183f082b298..2893af7fd1e 100755
--- a/main/inc/lib/usermanager.lib.php
+++ b/main/inc/lib/usermanager.lib.php
@@ -6250,7 +6250,7 @@ public static function get_favicon_from_url($url1, $url2 = null)
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 @@ public static function addUserAsAdmin(User $user)
}
$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 @@ public static function removeUserAdmin(User $user)
$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/CHANGELOG.md b/plugin/azure_active_directory/CHANGELOG.md
index 8185067329d..cb19ecdeb9a 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/lang/dutch.php b/plugin/azure_active_directory/lang/dutch.php
index 7f3e03e5631..32885f3f68c 100644
--- a/plugin/azure_active_directory/lang/dutch.php
+++ b/plugin/azure_active_directory/lang/dutch.php
@@ -22,11 +22,18 @@
.'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
.'
+ .'
- EXTRA_FIELD_ORGANISATION_EMAIL (
mail
) - EXTRA_FIELD_AZURE_ID (
mailNickname
) - EXTRA_FIELD_AZURE_UID (
id
of objectId
)
';
$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';
+$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.';
@@ -35,3 +42,7 @@
$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.';
+$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 5faf822a740..0d000a7d797 100644
--- a/plugin/azure_active_directory/lang/english.php
+++ b/plugin/azure_active_directory/lang/english.php
@@ -22,12 +22,19 @@
.'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
.'
+ .'- EXTRA_FIELD_ORGANISATION_EMAIL (
mail
) - EXTRA_FIELD_AZURE_ID (
mailNickname
) - EXTRA_FIELD_AZURE_UID (
id
or objectId
)
';
$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';
$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';
@@ -35,3 +42,7 @@
$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.';
+$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 2fdb5480830..e699c6d91d2 100644
--- a/plugin/azure_active_directory/lang/french.php
+++ b/plugin/azure_active_directory/lang/french.php
@@ -22,12 +22,19 @@
.'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
.'
+ .'- EXTRA_FIELD_ORGANISATION_EMAIL (
mail
) - EXTRA_FIELD_AZURE_ID (
mailNickname
) - EXTRA_FIELD_AZURE_UID (
id
ou objectId
)
';
$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';
$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';
@@ -35,3 +42,7 @@
$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.';
+$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 b6aef5cd368..389a4912e49 100644
--- a/plugin/azure_active_directory/lang/spanish.php
+++ b/plugin/azure_active_directory/lang/spanish.php
@@ -22,12 +22,19 @@
.'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
.'
+ .'- EXTRA_FIELD_ORGANISATION_EMAIL (
mail
) - EXTRA_FIELD_AZURE_ID (
mailNickname
) - EXTRA_FIELD_AZURE_UID (
id
o objectId
)
';
$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';
$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';
@@ -35,3 +42,7 @@
$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.';
+$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 8e0f33f3d3b..fce9cf9b689 100644
--- a/plugin/azure_active_directory/src/AzureActiveDirectory.php
+++ b/plugin/azure_active_directory/src/AzureActiveDirectory.php
@@ -1,6 +1,7 @@
'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',
+ 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);
+ parent::__construct('2.4', 'Angel Fernando Quiroz Campos, Yannick Warnier', $settings);
}
/**
@@ -88,6 +100,17 @@ public function getProvider()
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
*
@@ -123,5 +146,243 @@ public function install()
$this->get_lang('AzureId'),
''
);
+ UserManager::create_extra_field(
+ self::EXTRA_FIELD_AZURE_UID,
+ ExtraField::FIELD_TYPE_TEXT,
+ $this->get_lang('AzureUid'),
+ ''
+ );
+ }
+
+ 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, string $azureUidKey = 'objectId'): ?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[$azureUidKey]
+ ),
+ ];
+
+ foreach ($selectedOrder as $position) {
+ if (!empty($positionsAndFields[$position]) && isset($positionsAndFields[$position]['item_id'])) {
+ return (int) $positionsAndFields[$position]['item_id'];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function registerUser(
+ array $azureUserInfo,
+ 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') {
+ throw new Exception('User not found when checking the extra fields from '.$azureUserInfo['mail'].' or '.$azureUserInfo['mailNickname'].' or '.$azureUserInfo[$azureUidKey].'.');
+ }
+
+ [
+ $firstNme,
+ $lastName,
+ $username,
+ $email,
+ $phone,
+ $authSource,
+ $active,
+ $extra,
+ ] = $this->formatUserData($azureUserInfo, $azureUidKey);
+
+ // If the option is set to create users, create it
+ $userId = UserManager::create_user(
+ $firstNme,
+ $lastName,
+ STUDENT,
+ $email,
+ $username,
+ '',
+ null,
+ null,
+ $phone,
+ null,
+ $authSource,
+ null,
+ $active,
+ null,
+ $extra,
+ null,
+ null
+ );
+
+ 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,
+ ] = $this->formatUserData($azureUserInfo, $azureUidKey);
+
+ $userId = UserManager::update_user(
+ $userId,
+ $firstNme,
+ $lastName,
+ $username,
+ '',
+ $authSource,
+ $email,
+ STUDENT,
+ null,
+ $phone,
+ null,
+ null,
+ $active,
+ null,
+ 0,
+ $extra
+ );
+
+ if (!$userId) {
+ throw new Exception(get_lang('CouldNotUpdateUser').' '.$azureUserInfo['userPrincipalName']);
+ }
+ }
+
+ 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
+ */
+ private function formatUserData(
+ array $azureUserInfo,
+ string $azureUidKey
+ ): array {
+ $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,
+ ];
}
}
diff --git a/plugin/azure_active_directory/src/AzureCommand.php b/plugin/azure_active_directory/src/AzureCommand.php
new file mode 100644
index 00000000000..ce79c45ca2f
--- /dev/null
+++ b/plugin/azure_active_directory/src/AzureCommand.php
@@ -0,0 +1,188 @@
+plugin = AzureActiveDirectory::create();
+ $this->plugin->get_settings(true);
+ $this->provider = $this->plugin->getProviderForApiGraph();
+ }
+
+ /**
+ * @throws IdentityProviderException
+ */
+ protected function generateOrRefreshToken(?AccessTokenInterface &$token)
+ {
+ if (!$token || ($token->getExpires() && !$token->getRefreshToken())) {
+ $token = $this->provider->getAccessToken(
+ 'client_credentials',
+ ['resource' => $this->provider->resource]
+ );
+ }
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * @throws Exception
+ *
+ * @return Generator>
+ */
+ protected function getAzureGroupMembers(string $groupUid): Generator
+ {
+ $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
new file mode 100644
index 00000000000..30087a16e61
--- /dev/null
+++ b/plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php
@@ -0,0 +1,74 @@
+
+ */
+ public function __invoke(): Generator
+ {
+ yield 'Synchronizing groups from Azure.';
+
+ $usergroup = new UserGroup();
+
+ $groupIdByUid = [];
+
+ foreach ($this->getAzureGroups() as $azureGroupInfo) {
+ 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']);
+ }
+ }
+
+ $groupIdByUid[$azureGroupInfo['id']] = $groupId;
+ }
+
+ yield '----------------';
+ yield 'Subscribing users to groups';
+
+ foreach ($groupIdByUid as $azureGroupUid => $groupId) {
+ $newGroupMembers = [];
+
+ yield sprintf('Obtaining members for group (ID %d)', $groupId);
+
+ 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) {
+ $usergroup->subscribe_users_to_usergroup($groupId, $newGroupMembers);
+
+ yield sprintf(
+ 'User IDs subscribed in class (ID %d): %s',
+ $groupId,
+ implode(', ', $newGroupMembers)
+ );
+ }
+ }
+ }
+}
diff --git a/plugin/azure_active_directory/src/AzureSyncUsersCommand.php b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php
new file mode 100644
index 00000000000..36b60f9a2f7
--- /dev/null
+++ b/plugin/azure_active_directory/src/AzureSyncUsersCommand.php
@@ -0,0 +1,98 @@
+
+ */
+ public function __invoke(): Generator
+ {
+ yield 'Synchronizing users from Azure.';
+
+ /** @var array $existingUsers */
+ $existingUsers = [];
+
+ foreach ($this->getAzureUsers() as $azureUserInfo) {
+ try {
+ $userId = $this->plugin->registerUser($azureUserInfo, 'id');
+ } catch (Exception $e) {
+ yield $e->getMessage();
+
+ continue;
+ }
+
+ $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) {
+ try {
+ $azureGroupMembersInfo = iterator_to_array($this->getAzureGroupMembers($groupUid));
+ } catch (Exception $e) {
+ yield $e->getMessage();
+
+ continue;
+ }
+
+ $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);
+
+ yield sprintf('User (ID %d) status %s', $userId, $userRole);
+ }
+ }
+
+ $em->flush();
+ }
+
+ 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)
+ );
+ }
+ }
+}
diff --git a/plugin/azure_active_directory/src/callback.php b/plugin/azure_active_directory/src/callback.php
index 08e140676e0..3bc85d443c5 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'])) {
@@ -79,100 +82,38 @@
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.');
}
-
- $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']
- );
-
- $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($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.');
}
- 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'];
- }
- }
+ $userId = $plugin->registerUser($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 ($roleGroups = $plugin->getGroupUidByRole()) {
+ $roleActions = $plugin->getUpdateActionByRole();
+ /** @var User $user */
+ $user = UserManager::getManager()->find($userId);
+
+ $azureGroups = $provider->get('me/memberOf', $token);
+
+ foreach ($roleGroups as $userRole => $groupUid) {
+ foreach ($azureGroups as $azureGroup) {
+ $azureGroupUid = $azureGroup['objectId'];
+ if ($azureGroupUid === $groupUid) {
+ $roleActions[$userRole]($user);
- // 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'],
- ],
- null,
- null,
- $isAdmin
- );
- if (!$userId) {
- throw new Exception(get_lang('UserNotAdded').' '.$me['mailNickname']);
+ break 2;
+ }
}
- } else {
- throw new Exception('User not found when checking the extra fields from '.$me['mail'].' or '.$me['mailNickname'].'.');
}
+
+ Database::getManager()->flush();
}
$userInfo = api_get_user_info($userId);
- //TODO add user update management for groups
-
- //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.');
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 00000000000..8ad128c5db2
--- /dev/null
+++ b/plugin/azure_active_directory/src/scripts/sync_usergroups.php
@@ -0,0 +1,21 @@
+getMessage());
+}
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 00000000000..33eb4768e62
--- /dev/null
+++ b/plugin/azure_active_directory/src/scripts/sync_users.php
@@ -0,0 +1,21 @@
+getMessage());
+}
diff --git a/src/Chamilo/UserBundle/Repository/UserRepository.php b/src/Chamilo/UserBundle/Repository/UserRepository.php
index 8ddbf2ce427..dae382a02fb 100644
--- a/src/Chamilo/UserBundle/Repository/UserRepository.php
+++ b/src/Chamilo/UserBundle/Repository/UserRepository.php
@@ -1382,4 +1382,9 @@ public function getLastLogin(User $user)
->getQuery()
->getOneOrNullResult();
}
+
+ public function findByAuthSource(string $authSource): array
+ {
+ return $this->findBy(['authSource' => $authSource]);
+ }
}