Merge pull request #5763 from AngelFQC/BT21930

Plugin: Azure: Improvements for existing user verification and scripts to sync users
pull/5823/head
Nicolas Ducoulombier 1 year ago committed by GitHub
commit 52afec09f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      main/auth/external_login/login.azure.php
  2. 8
      main/inc/lib/usermanager.lib.php
  3. 21
      plugin/azure_active_directory/CHANGELOG.md
  4. 11
      plugin/azure_active_directory/lang/dutch.php
  5. 11
      plugin/azure_active_directory/lang/english.php
  6. 11
      plugin/azure_active_directory/lang/french.php
  7. 11
      plugin/azure_active_directory/lang/spanish.php
  8. 263
      plugin/azure_active_directory/src/AzureActiveDirectory.php
  9. 188
      plugin/azure_active_directory/src/AzureCommand.php
  10. 74
      plugin/azure_active_directory/src/AzureSyncUsergroupsCommand.php
  11. 98
      plugin/azure_active_directory/src/AzureSyncUsersCommand.php
  12. 107
      plugin/azure_active_directory/src/callback.php
  13. 21
      plugin/azure_active_directory/src/scripts/sync_usergroups.php
  14. 21
      plugin/azure_active_directory/src/scripts/sync_users.php
  15. 5
      src/Chamilo/UserBundle/Repository/UserRepository.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'],

@ -6249,7 +6249,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();
@ -6260,11 +6260,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)) {
@ -6272,7 +6272,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);
}
}

@ -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

@ -22,11 +22,18 @@ $strings['management_login_enable_help'] = 'Schakel de chamilo-login uit en scha
.'U zult moeten kopiëren de <code>/plugin/azure_active_directory/layout/login_form.tpl</code> bestand in het <code>/main/template/overrides/layout/</code> 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 <code>1, 2, 3</code>.'
.'<ol><li>EXTRA_FIELD_ORGANISATION_EMAIL (<code>mail</code>)</li><li>EXTRA_FIELD_AZURE_ID (<code>mailNickname</code>)</li><li>EXTRA_FIELD_AZURE_UID (<code>id</code> of <code>objectId</code>)</li></ol>';
$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_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 <a href="https://login.microsoftonline.com" target="_blank">uw authenticatiesysteem</a> 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.';

@ -22,12 +22,19 @@ $strings['management_login_enable_help'] = 'Disable the chamilo login and enable
.'You will need to copy the <code>/plugin/azure_active_directory/layout/login_form.tpl</code> file to <code>/main/template/overrides/layout/</code> 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 <code>1, 2, 3</code>.'
.'<ol><li>EXTRA_FIELD_ORGANISATION_EMAIL (<code>mail</code>)</li><li>EXTRA_FIELD_AZURE_ID (<code>mailNickname</code>)</li><li>EXTRA_FIELD_AZURE_UID (<code>id</code> or <code>objectId</code>)</li></ol>';
$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_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 <a href="https://login.microsoftonline.com" target="_blank">your authentication system</a>, 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.';

@ -22,12 +22,19 @@ $strings['management_login_enable_help'] = 'Désactiver le login de Chamilo et p
.'Vous devez, pour cela, copier le fichier <code>/plugin/azure_active_directory/layout/login_form.tpl</code> dans le répertoire <code>/main/template/overrides/layout/</code>.';
$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 <code>1, 2, 3</code>.'
.'<ol><li>EXTRA_FIELD_ORGANISATION_EMAIL (<code>mail</code>)</li><li>EXTRA_FIELD_AZURE_ID (<code>mailNickname</code>)</li><li>EXTRA_FIELD_AZURE_UID (<code>id</code> ou <code>objectId</code>)</li></ol>';
$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_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 <a href="https://login.microsoftonline.com" target="_blank">votre système d\'authentification</a>, 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.';

@ -22,12 +22,19 @@ $strings['management_login_enable_help'] = 'Desactivar el login de Chamilo y act
.'Para ello, tendrá que copiar el archivo <code>/plugin/azure_active_directory/layout/login_form.tpl</code> en la carpeta <code>/main/template/overrides/layout/</code>.';
$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 <code>1, 2, 3</code>.'
.'<ol><li>EXTRA_FIELD_ORGANISATION_EMAIL (<code>mail</code>)</li><li>EXTRA_FIELD_AZURE_ID (<code>mailNickname</code>)</li><li>EXTRA_FIELD_AZURE_UID (<code>id</code> o <code>objectId</code>)</li></ol>';
$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_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 <a href="https://login.microsoftonline.com" target="_blank">sistema de autenticación</a>, 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.';

@ -1,6 +1,7 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\UserBundle\Entity\User;
use TheNetworg\OAuth2\Client\Provider\Azure;
/**
@ -20,15 +21,22 @@ 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';
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';
public const EXTRA_FIELD_ORGANISATION_EMAIL = 'organisationemail';
public const EXTRA_FIELD_AZURE_ID = 'azure_id';
public const EXTRA_FIELD_AZURE_UID = 'azure_uid';
public const API_PAGE_SIZE = 999;
/**
* AzureActiveDirectory constructor.
@ -44,12 +52,16 @@ 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',
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 @@ 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
*
@ -123,5 +146,243 @@ 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'),
''
);
}
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<string, string|false>
*/
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<string, callable>
*/
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,
];
}
}

@ -0,0 +1,188 @@
<?php
/* 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;
abstract class AzureCommand
{
/**
* @var AzureActiveDirectory
*/
protected $plugin;
/**
* @var Azure
*/
protected $provider;
public function __construct()
{
$this->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<int, array<string, string>>
*/
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<int, array<string, string>>
*/
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<int, array<string, string>>
*/
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);
}
}

@ -0,0 +1,74 @@
<?php
/* For license terms, see /license.txt */
class AzureSyncUsergroupsCommand extends AzureCommand
{
/**
* @throws Exception
*
* @return Generator<int, string>
*/
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)
);
}
}
}
}

@ -0,0 +1,98 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\UserBundle\Entity\User;
class AzureSyncUsersCommand extends AzureCommand
{
/**
* @throws Exception
*
* @return Generator<int, string>
*/
public function __invoke(): Generator
{
yield 'Synchronizing users from Azure.';
/** @var array<string, int> $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)
);
}
}
}

@ -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 @@ 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.');
}
$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.');

@ -0,0 +1,21 @@
<?php
/* For license terms, see /license.txt */
require __DIR__.'/../../../../main/inc/global.inc.php';
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 {
foreach ($command() as $str) {
printf("%d - %s".PHP_EOL, time(), $str);
}
} catch (Exception $e) {
printf('%s - Exception: %s'.PHP_EOL, time(), $e->getMessage());
}

@ -0,0 +1,21 @@
<?php
/* For license terms, see /license.txt */
require __DIR__.'/../../../../main/inc/global.inc.php';
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 {
foreach ($command() as $str) {
printf("%d - %s".PHP_EOL, time(), $str);
}
} catch (Exception $e) {
printf('%s - Exception: %s'.PHP_EOL, time(), $e->getMessage());
}

@ -1382,4 +1382,9 @@ class UserRepository extends EntityRepository
->getQuery()
->getOneOrNullResult();
}
public function findByAuthSource(string $authSource): array
{
return $this->findBy(['authSource' => $authSource]);
}
}

Loading…
Cancel
Save