This commit is contained in:
Xes
2025-08-14 22:39:38 +02:00
parent 3641e93527
commit 5403f346e3
3370 changed files with 327179 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
# Azure Active Directory Changelog
## 2.5 - 2024-11-18
* Added new options to get the user and groups with delta query (or change tracking) when syncing with scripts.
this requires manually doing the following changes to your database if you are upgrading from v2.4
```sql
CREATE TABLE azure_ad_sync_state (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, value LONGTEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB;
```
## 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
upgrading from a previous version of the plugin manually:
```
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_group_id_admin', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', null, null, 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_group_id_session_admin', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', null, null, 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_group_id_teacher', 'azure_active_directory', 'setting', 'Plugins', '', 'azure_active_directory', '', null, null, 1, 1, 0);
```
## 2.2 - 2021-03-02
* Added provisioning setting
## 2.1 - 2020
* Initial tested implementation of Azure Active Directory single sign on

View File

@@ -0,0 +1,46 @@
# The Azure Active Directory Plugin
This plugin allows users to authenticate (with OAuth2) through Microsoft's Azure Active Directory.
This will modify the login form to either substitute the default login form or add another option to connect through
Azure.
An option allows you to automatically provision/create users in Chamilo from their account on Azure if they don't exist
in Chamilo yet.
This plugin adds two extra fields for users:
* `organisationemail`, the email registered in Azure Active Directory for each user (under _Email_ in the _Contact info_ section).
* `azure_id`, to save the internal ID for each user in Azure (which is also the prefix before the _@_ sign in the _User Principal Name_).
### Prerequisites
This plugin will *not* work if you do not use HTTPS.
Make sure your portal is in HTTPS before you configure this plugin.
### To configure Azure Active Directory
* Create and configure an application in your Azure panel (Azure Active Directory -> Applications registration -> New registration)).
* In the _Authentication_ section, set an _Reply URL_ (or _Redirect URIs_) of `https://{CHAMILO_URL}/plugin/azure_active_directory/src/callback.php`.
* In the _Front-channel logout URL_, use `https://{CHAMILO_URL}/index.php?logout=logout`.
* Leave the rest of the _Authentication_ section unchanged.
* In _Certificates & secrets_, create a secret string (or application password). Keep the _Value_ field at hand. If you don't copy it somewhere at this point, it will later be hidden, so take a copy, seriously!
* Make sure you actually have users.
### To configure this plugin
* _Enable_: You can enable the plugin once everything is configured correctly. Disabling it will return to the normal Chamilo login procedure.
* _Application ID_: Enter the _Application (client) ID_ assigned to your app when you created it in your Azure Active Directory interface, under _App registrations_.
* _Application secret_: Enter the client secret _value_ created in _Certificate & secrets_ above.
* _Block name_: (Optional) The name to show above the login button.
* _Force logout button_: (Optional) Add a button to force logout from Azure.
* _Management login_: (Optional) Disable the chamilo login and enable an alternative login page for users.
You will need copy the `/plugin/azure_active_directory/layout/login_form.tpl` file to `/main/template/overrides/layout/` directory.
* _Name for the management login_: A name for the manager login. By default, it is set to "Management Login".
* _Automated provisioning_: Enable if you want users to be created automatically in Chamilo (as students) when they don't exist yet.
* Assign a region in which the login option will appear. Preferably `login_bottom`.
### Enable through the normal login form
You can configure the external login procedure to work with the classic Chamilo form login.
To do it, make sure users have _azure_ in their auth_source field, then add this line in `configuration.php` file
```php
$extAuthSource["azure"]["login"] = $_configuration['root_sys']."main/auth/external_login/login.azure.php";
```
### Dependencies
> This plugin uses the [`thenetworg/oauth2-azure`](https://github.com/TheNetworg/oauth2-azure) package.

View File

@@ -0,0 +1,38 @@
<?php
/* For licensing terms, see /license.txt */
/**
* @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*
* @package chamilo.plugin.azure_active_directory
*/
// Check if AzureActiveDirectory exists, since this is not loaded as a page.
// index.php is shown as a block when showing the region to which the plugin is assigned
if (class_exists(AzureActiveDirectory::class)) {
/** @var AzureActiveDirectory $activeDirectoryPlugin */
$activeDirectoryPlugin = AzureActiveDirectory::create();
if ($activeDirectoryPlugin->get(AzureActiveDirectory::SETTING_ENABLE) === 'true') {
$_template['block_title'] = $activeDirectoryPlugin->get(AzureActiveDirectory::SETTING_BLOCK_NAME);
$_template['signin_url'] = $activeDirectoryPlugin->getUrl(AzureActiveDirectory::URL_TYPE_AUTHORIZE);
if ('true' === $activeDirectoryPlugin->get(AzureActiveDirectory::SETTING_FORCE_LOGOUT_BUTTON)) {
$_template['signout_url'] = $activeDirectoryPlugin->getUrl(AzureActiveDirectory::URL_TYPE_LOGOUT);
}
$managementLoginEnabled = 'true' === $activeDirectoryPlugin->get(AzureActiveDirectory::SETTING_MANAGEMENT_LOGIN_ENABLE);
$_template['management_login_enabled'] = $managementLoginEnabled;
if ($managementLoginEnabled) {
$managementLoginName = $activeDirectoryPlugin->get(AzureActiveDirectory::SETTING_MANAGEMENT_LOGIN_NAME);
if (empty($managementLoginName)) {
$managementLoginName = $activeDirectoryPlugin->get_lang('ManagementLogin');
}
$_template['management_login_name'] = $managementLoginName;
}
}
}

View File

@@ -0,0 +1,8 @@
<?php
/* For licensing terms, see /license.txt */
if (!api_is_platform_admin()) {
exit('You must have admin permissions to install plugins');
}
AzureActiveDirectory::create()->install();

View File

@@ -0,0 +1,54 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Strings to Dutch L10n.
*
* @author Yannick Warnier <yannick.warnier@beeznest.com>
*
* @package chamilo.plugin.azure_active_directory
*/
$strings['plugin_title'] = 'Azure Active Directory';
$strings['plugin_comment'] = 'Sta authenticatie met Microsoft\'s Azure Active Directory toe';
$strings['enable'] = 'Inschakelen';
$strings['app_id'] = 'Applicatie ID';
$strings['app_id_help'] = 'Voeg de Applicatie Id toegewezen aan uw app bij de Azure portal, b.v. 580e250c-8f26-49d0-bee8-1c078add1609';
$strings['app_secret'] = 'Applicatie gehem';
$strings['force_logout'] = 'Forceer uitlogknop';
$strings['force_logout_help'] = 'Toon een knop om afmeldingssessie van Azure af te dwingen.';
$strings['block_name'] = 'Blok naam';
$strings['management_login_enable'] = 'Beheer login';
$strings['management_login_enable_help'] = 'Schakel de chamilo-login uit en schakel een alternatieve inlogpagina in voor gebruikers.<br>'
.'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.';
$strings['group_id_session_admin'] = 'Groeps-ID voor sessiebeheerders';
$strings['group_id_session_admin_help'] = 'De groeps-ID voor sessiebeheerders. Indien leeg, wordt er automatisch geen gebruiker aangemaakt als sessiebeheerder.';
$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.';
$strings['script_users_delta'] = 'Delta query for users';
$strings['script_users_delta_help'] = 'Get newly created, updated, or deleted users without having to perform a full read of the entire user collection. By default, is <code>No</code>.';
$strings['script_usergroups_delta'] = 'Delta query for usergroups';
$strings['script_usergroups_delta_help'] = 'Get newly created, updated, or deleted groups, including group membership changes, without having to perform a full read of the entire group collection. By default, is <code>No</code>.';
$strings['group_filter_regex'] = 'Group filter RegEx';
$strings['group_filter_regex_help'] = 'Regular expression to filter groups (only matches will be synchronized), e.g. <code>.*-FIL-.*</code> <code>.*-PAR-.*</code> <code>.*(FIL|PAR).*</code> <code>^(FIL|PAR).*</code>';

View File

@@ -0,0 +1,54 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Strings to English L10n.
*
* @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*
* @package chamilo.plugin.azure_active_directory
*/
$strings['plugin_title'] = 'Azure Active Directory';
$strings['plugin_comment'] = 'Allow authentication with Microsoft\'s Azure Active Directory';
$strings['enable'] = 'Enable';
$strings['app_id'] = 'Application ID';
$strings['app_id_help'] = 'Enter the Application Id assigned to your app by the Azure portal, e.g. 580e250c-8f26-49d0-bee8-1c078add1609';
$strings['app_secret'] = 'Application secret';
$strings['force_logout'] = 'Force logout button';
$strings['force_logout_help'] = 'Show a button to force logout session from Azure.';
$strings['block_name'] = 'Block name';
$strings['management_login_enable'] = 'Management login';
$strings['management_login_enable_help'] = 'Disable the chamilo login and enable an alternative login page for admin users.<br>'
.'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';
$strings['group_id_session_admin_help'] = 'The group ID for session admins. If empty, no user will be automatically created as session admin.';
$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.';
$strings['script_users_delta'] = 'Delta query for users';
$strings['script_users_delta_help'] = 'Get newly created, updated, or deleted users without having to perform a full read of the entire user collection. By default, is <code>No</code>.';
$strings['script_usergroups_delta'] = 'Delta query for usergroups';
$strings['script_usergroups_delta_help'] = 'Get newly created, updated, or deleted groups, including group membership changes, without having to perform a full read of the entire group collection. By default, is <code>No</code>.';
$strings['group_filter_regex'] = 'Group filter RegEx';
$strings['group_filter_regex_help'] = 'Regular expression to filter groups (only matches will be synchronized), e.g. <code>.*-FIL-.*</code> <code>.*-PAR-.*</code> <code>.*(FIL|PAR).*</code> <code>^(FIL|PAR).*</code>';

View File

@@ -0,0 +1,54 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Strings to French L10n.
*
* @author Yannick Warnier <yannick.warnier@beeznest.com>
*
* @package chamilo.plugin.azure_active_directory
*/
$strings['plugin_title'] = 'Azure Active Directory';
$strings['plugin_comment'] = 'Permet l\'authentification des utilisateurs via Azure Active Directory de Microsoft';
$strings['enable'] = 'Activer';
$strings['app_id'] = 'ID de l\'application';
$strings['app_id_help'] = 'Introduisez l\'ID de l\'application assigné à votre app par le portail d\'Azure, p.ex. 580e250c-8f26-49d0-bee8-1c078add1609';
$strings['app_secret'] = 'Clef secrète de l\'application';
$strings['force_logout'] = 'Bouton de logout';
$strings['force_logout_help'] = 'Affiche un bouton pour se délogger d\'Azure.';
$strings['block_name'] = 'Nom du bloc';
$strings['management_login_enable'] = 'Login de gestion';
$strings['management_login_enable_help'] = 'Désactiver le login de Chamilo et permettre une page de login alternative pour les utilisateurs administrateurs.<br>'
.'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';
$strings['group_id_session_admin_help'] = 'The group ID for session admins. Si ce champ est laissé vide, aucun utilisateur ne sera créé en tant qu\'administrateur de sessions.';
$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.';
$strings['script_users_delta'] = 'Requête delta pour les utilisateurs';
$strings['script_users_delta_help'] = 'Get newly created, updated, or deleted users without having to perform a full read of the entire user collection. By default, is <code>No</code>.';
$strings['script_usergroups_delta'] = 'Requête delta pour les groupes d\'utilisateurs';
$strings['script_usergroups_delta_help'] = 'Get newly created, updated, or deleted groups, including group membership changes, without having to perform a full read of the entire group collection. By default, is <code>No</code>.';
$strings['group_filter_regex'] = 'Group filter RegEx';
$strings['group_filter_regex_help'] = 'Regular expression to filter groups (only matches will be synchronized), e.g. <code>.*-FIL-.*</code> <code>.*-PAR-.*</code> <code>.*(FIL|PAR).*</code> <code>^(FIL|PAR).*</code>';

View File

@@ -0,0 +1,54 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Strings to Spanish L10n.
*
* @author Yannick Warnier <yannick.warnier@beeznest.com>
*
* @package chamilo.plugin.azure_active_directory
*/
$strings['plugin_title'] = 'Azure Active Directory';
$strings['plugin_comment'] = 'Permite la autenticación de usuarios por Azure Active Directory de Microsoft';
$strings['enable'] = 'Activar';
$strings['app_id'] = 'ID de la aplicación';
$strings['app_id_help'] = 'Introduzca el ID de la aplicación asignado a su app en el portal de Azure, p.ej. 580e250c-8f26-49d0-bee8-1c078add1609';
$strings['app_secret'] = 'Clave secreta de la aplicación';
$strings['force_logout'] = 'Botón de logout';
$strings['force_logout_help'] = 'Muestra un botón para hacer logout de Azure.';
$strings['block_name'] = 'Nombre del bloque';
$strings['management_login_enable'] = 'Login de gestión';
$strings['management_login_enable_help'] = 'Desactivar el login de Chamilo y activar una página de login alternativa para los usuarios de administración.<br>'
.'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';
$strings['group_id_session_admin_help'] = 'El ID de grupo para administradores de sesiones. Si deja este campo vacío, ningún usuario será creado como administrador de sesiones.';
$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.';
$strings['script_users_delta'] = 'Consula delta para usuarios';
$strings['script_users_delta_help'] = 'Obtiene usuarios recién creados, actualizados o eliminados sin tener que realizar una lectura completa de toda la colección de usuarios. De forma predeterminada, es <code>No</code>.';
$strings['script_usergroups_delta'] = 'Consulta delta para grupos de usuarios';
$strings['script_usergroups_delta_help'] = 'Obtiene grupos recién creados, actualizados o eliminados, incluidos los cambios de membresía del grupo, sin tener que realizar una lectura completa de toda la colección de grupos. De forma predeterminada, es <code>No</code>';
$strings['group_filter_regex'] = 'Group filter RegEx';
$strings['group_filter_regex_help'] = 'Expresión regular para filtrar grupos (solo las coincidencias serán sincronizadas), p.ej. <code>.*-FIL-.*</code> <code>.*-PAR-.*</code> <code>.*(FIL|PAR).*</code> <code>^(FIL|PAR).*</code>';

View File

@@ -0,0 +1,51 @@
{% if _u.logged == 0 %}
{% if login_form %}
<div id="login-block" class="panel panel-default">
<div class="panel-body">
{{ login_language_form }}
{% if plugin_login_top is not null %}
<div id="plugin_login_top">
{{ plugin_login_top }}
</div>
{% endif %}
{{ login_failed }}
{% set azure_plugin_enabled = 'azure_active_directory'|api_get_plugin_setting('enable') %}
{% set azure_plugin_manage_login = 'azure_active_directory'|api_get_plugin_setting('manage_login_enable') %}
{% if 'false' == azure_plugin_enabled or 'false' == azure_plugin_manage_login %}
{{ login_form }}
{% if "allow_lostpassword" | api_get_setting == 'true' or "allow_registration"|api_get_setting == 'true' %}
<ul class="nav nav-pills nav-stacked">
{% if "allow_registration"|api_get_setting != 'false' %}
<li><a href="{{ _p.web_main }}auth/inscription.php"> {{ 'SignUp'|get_lang }} </a></li>
{% endif %}
{% if "allow_lostpassword" | api_get_setting == 'true' %}
{% set pass_reminder_link = 'pass_reminder_custom_link'|api_get_configuration_value %}
{% set lost_password_link = _p.web_main ~ 'auth/lostPassword.php' %}
{% if not pass_reminder_link is empty %}
{% set lost_password_link = pass_reminder_link %}
{% endif %}
<li>
<a href="{{ lost_password_link }}"> {{ 'LostPassword' | get_lang }} </a>
</li>
{% endif %}
</ul>
{% endif %}
{% endif %}
{% if plugin_login_bottom is not null %}
<div id="plugin_login_bottom">
{{ plugin_login_bottom }}
</div>
{% endif %}
</div>
</div>
{% endif %}
{% endif %}

View File

@@ -0,0 +1,35 @@
<?php
/* For license terms, see /license.txt */
require __DIR__.'/../../main/inc/global.inc.php';
$plugin = AzureActiveDirectory::create();
$pluginEnabled = $plugin->get(AzureActiveDirectory::SETTING_ENABLE);
$managementLoginEnabled = $plugin->get(AzureActiveDirectory::SETTING_MANAGEMENT_LOGIN_ENABLE);
if ('true' !== $pluginEnabled || 'true' !== $managementLoginEnabled) {
header('Location: '.api_get_path(WEB_PATH));
exit;
}
$userId = api_get_user_id();
if (!($userId) || api_is_anonymous($userId)) {
$managementLoginName = $plugin->get(AzureActiveDirectory::SETTING_MANAGEMENT_LOGIN_NAME);
if (empty($managementLoginName)) {
$managementLoginName = $plugin->get_lang('ManagementLogin');
}
$template = new Template($managementLoginName);
// Only display if the user isn't logged in.
$template->assign('login_language_form', api_display_language_form(true, true));
$template->assign('login_form', $template->displayLoginForm());
$content = $template->fetch('azure_active_directory/view/login.tpl');
$template->assign('content', $content);
$template->display_one_col_template();
}

View File

@@ -0,0 +1,10 @@
<?php
/* For licensing terms, see /license.txt */
/**
* @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*
* @package chamilo.plugin.azure_active_directory
*/
$plugin_info = AzureActiveDirectory::create()->get_info();
$plugin_info['templates'] = ['view/block.tpl'];

View File

@@ -0,0 +1,442 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Entity\AzureActiveDirectory\AzureSyncState;
use Chamilo\UserBundle\Entity\User;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\ORM\Tools\ToolsException;
use TheNetworg\OAuth2\Client\Provider\Azure;
/**
* AzureActiveDirectory plugin class.
*
* @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*
* @package chamilo.plugin.azure_active_directory
*/
class AzureActiveDirectory extends Plugin
{
public const SETTING_ENABLE = 'enable';
public const SETTING_APP_ID = 'app_id';
public const SETTING_APP_SECRET = 'app_secret';
public const SETTING_BLOCK_NAME = 'block_name';
public const SETTING_FORCE_LOGOUT_BUTTON = 'force_logout';
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 SETTING_GET_USERS_DELTA = 'script_users_delta';
public const SETTING_GET_USERGROUPS_DELTA = 'script_usergroups_delta';
public const SETTING_GROUP_FILTER = 'group_filter_regex';
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.
*/
protected function __construct()
{
$settings = [
self::SETTING_ENABLE => 'boolean',
self::SETTING_APP_ID => 'text',
self::SETTING_APP_SECRET => 'text',
self::SETTING_BLOCK_NAME => 'text',
self::SETTING_FORCE_LOGOUT_BUTTON => 'boolean',
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',
self::SETTING_GET_USERS_DELTA => 'boolean',
self::SETTING_GET_USERGROUPS_DELTA => 'boolean',
self::SETTING_GROUP_FILTER => 'text',
];
parent::__construct('2.5', 'Angel Fernando Quiroz Campos, Yannick Warnier', $settings);
}
/**
* Instance the plugin.
*
* @staticvar null $result
*
* @return $this
*/
public static function create()
{
static $result = null;
return $result ? $result : $result = new self();
}
/**
* @return string
*/
public function get_name()
{
return 'azure_active_directory';
}
public function getProvider(): Azure
{
return new Azure([
'clientId' => $this->get(self::SETTING_APP_ID),
'clientSecret' => $this->get(self::SETTING_APP_SECRET),
'redirectUri' => api_get_path(WEB_PLUGIN_PATH).'azure_active_directory/src/callback.php',
'urlAPI' => 'https://graph.microsoft.com/v1.0/',
'resource' => 'https://graph.microsoft.com',
]);
}
public function getProviderForApiGraph(): Azure
{
$provider = $this->getProvider();
$provider->tenant = $this->get(AzureActiveDirectory::SETTING_TENANT_ID);
$provider->authWithResource = false;
return $provider;
}
/**
* @param string $urlType Type of URL to generate
*
* @return string
*/
public function getUrl($urlType)
{
if (self::URL_TYPE_LOGOUT === $urlType) {
$provider = $this->getProvider();
return $provider->getLogoutUrl(
api_get_path(WEB_PATH)
);
}
return api_get_path(WEB_PLUGIN_PATH).$this->get_name().'/src/callback.php';
}
/**
* Create extra fields for user when installing.
*
* @throws ToolsException
*/
public function install()
{
UserManager::create_extra_field(
self::EXTRA_FIELD_ORGANISATION_EMAIL,
ExtraField::FIELD_TYPE_TEXT,
$this->get_lang('OrganisationEmail'),
''
);
UserManager::create_extra_field(
self::EXTRA_FIELD_AZURE_ID,
ExtraField::FIELD_TYPE_TEXT,
$this->get_lang('AzureId'),
''
);
UserManager::create_extra_field(
self::EXTRA_FIELD_AZURE_UID,
ExtraField::FIELD_TYPE_TEXT,
$this->get_lang('AzureUid'),
''
);
$em = Database::getManager();
if ($em->getConnection()->getSchemaManager()->tablesExist(['azure_ad_sync_state'])) {
return;
}
$schemaTool = new SchemaTool($em);
$schemaTool->createSchema(
[
$em->getClassMetadata(AzureSyncState::class),
]
);
}
public function uninstall()
{
$em = Database::getManager();
if (!$em->getConnection()->getSchemaManager()->tablesExist(['azure_ad_sync_state'])) {
return;
}
$schemaTool = new SchemaTool($em);
$schemaTool->dropSchema(
[
$em->getClassMetadata(AzureSyncState::class),
]
);
}
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['id']
),
];
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)
{
if (empty($azureUserInfo)) {
throw new Exception('Groups info not found.');
}
$userId = $this->getUserIdByVerificationOrder($azureUserInfo);
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['id'].'.');
}
[
$firstNme,
$lastName,
$username,
$email,
$phone,
$authSource,
$active,
$extra,
] = $this->formatUserData($azureUserInfo);
// 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);
$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);
},
];
}
public function getSyncState(string $title): ?AzureSyncState
{
$stateRepo = Database::getManager()->getRepository(AzureSyncState::class);
return $stateRepo->findOneBy(['title' => $title]);
}
public function saveSyncState(string $title, $value)
{
$state = $this->getSyncState($title);
if (!$state) {
$state = new AzureSyncState();
$state->setTitle($title);
Database::getManager()->persist($state);
}
$state->setValue($value);
Database::getManager()->flush();
}
/**
* @throws Exception
*/
private function formatUserData(array $azureUserInfo): 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['id'],
];
return [
$firstNme,
$lastName,
$username,
$email,
$phone,
$authSource,
$active,
$extra,
];
}
}

View File

@@ -0,0 +1,239 @@
<?php
/* For license terms, see /license.txt */
use Chamilo\PluginBundle\Entity\AzureActiveDirectory\AzureSyncState;
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',
];
$getUsersDelta = 'true' === $this->plugin->get(AzureActiveDirectory::SETTING_GET_USERS_DELTA);
if ($getUsersDelta) {
$usersDeltaLink = $this->plugin->getSyncState(AzureSyncState::USERS_DATALINK);
$query = $usersDeltaLink
? $usersDeltaLink->getValue()
: sprintf('$select=%s', implode(',', $userFields));
} else {
$query = sprintf(
'$top=%d&$select=%s',
AzureActiveDirectory::API_PAGE_SIZE,
implode(',', $userFields)
);
}
$token = null;
do {
$this->generateOrRefreshToken($token);
try {
$azureUsersRequest = $this->provider->request(
'get',
$getUsersDelta ? "users/delta?$query" : "users?$query",
$token
);
} catch (Exception $e) {
throw new Exception('Exception when requesting users from Azure: '.$e->getMessage());
}
$azureUsersInfo = $azureUsersRequest['value'] ?? [];
foreach ($azureUsersInfo as $azureUserInfo) {
$azureUserInfo['mail'] = $azureUserInfo['mail'] ?? null;
$azureUserInfo['surname'] = $azureUserInfo['surname'] ?? null;
$azureUserInfo['givenName'] = $azureUserInfo['givenName'] ?? null;
yield $azureUserInfo;
}
$hasNextLink = false;
if (!empty($azureUsersRequest['@odata.nextLink'])) {
$hasNextLink = true;
$query = parse_url($azureUsersRequest['@odata.nextLink'], PHP_URL_QUERY);
}
if ($getUsersDelta && !empty($azureUsersRequest['@odata.deltaLink'])) {
$this->plugin->saveSyncState(
AzureSyncState::USERS_DATALINK,
parse_url($azureUsersRequest['@odata.deltaLink'], PHP_URL_QUERY),
);
}
} while ($hasNextLink);
}
/**
* @throws Exception
*
* @return Generator<int, array<string, string>>
*/
protected function getAzureGroups(): Generator
{
$groupFilter = $this->plugin->get(AzureActiveDirectory::SETTING_GROUP_FILTER);
$groupFields = [
'id',
'displayName',
'description',
];
$getUsergroupsDelta = 'true' === $this->plugin->get(AzureActiveDirectory::SETTING_GET_USERGROUPS_DELTA);
if ($getUsergroupsDelta) {
$usergroupsDeltaLink = $this->plugin->getSyncState(AzureSyncState::USERGROUPS_DATALINK);
$query = $usergroupsDeltaLink
? $usergroupsDeltaLink->getValue()
: sprintf('$select=%s', implode(',', $groupFields));
} else {
$query = sprintf(
'$top=%d&$select=%s',
AzureActiveDirectory::API_PAGE_SIZE,
implode(',', $groupFields)
);
}
$token = null;
do {
$this->generateOrRefreshToken($token);
try {
$azureGroupsRequest = $this->provider->request(
'get',
$getUsergroupsDelta ? "groups/delta?$query" : "groups?$query",
$token
);
} catch (Exception $e) {
throw new Exception('Exception when requesting groups from Azure: '.$e->getMessage());
}
$azureGroupsInfo = $azureGroupsRequest['value'] ?? [];
foreach ($azureGroupsInfo as $azureGroupInfo) {
if (!empty($groupFilter) &&
!preg_match("/$groupFilter/", $azureGroupInfo['displayName'])
) {
continue;
}
yield $azureGroupInfo;
}
$hasNextLink = false;
if (!empty($azureGroupsRequest['@odata.nextLink'])) {
$hasNextLink = true;
$query = parse_url($azureGroupsRequest['@odata.nextLink'], PHP_URL_QUERY);
}
if ($getUsergroupsDelta && !empty($azureGroupsRequest['@odata.deltaLink'])) {
$this->plugin->saveSyncState(
AzureSyncState::USERGROUPS_DATALINK,
parse_url($azureGroupsRequest['@odata.deltaLink'], 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);
}
}

View File

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

View File

@@ -0,0 +1,100 @@
<?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> $azureCreatedUserIdList */
$azureCreatedUserIdList = [];
foreach ($this->getAzureUsers() as $azureUserInfo) {
try {
$userId = $this->plugin->registerUser($azureUserInfo);
} catch (Exception $e) {
yield $e->getMessage();
continue;
}
$azureCreatedUserIdList[$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 = $azureCreatedUserIdList[$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)
&& 'true' !== $this->plugin->get(AzureActiveDirectory::SETTING_GET_USERS_DELTA)
) {
yield '----------------';
yield 'Trying deactivate non-existing users in Azure';
$users = UserManager::getRepository()->findByAuthSource('azure');
$chamiloUserIdList = array_map(
function ($user) {
return $user->getId();
},
$users
);
$nonExistingUsers = array_diff($chamiloUserIdList, $azureCreatedUserIdList);
UserManager::deactivate_users($nonExistingUsers);
yield sprintf(
'Deactivated users IDs: %s',
implode(', ', $nonExistingUsers)
);
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\PluginBundle\Entity\AzureActiveDirectory;
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
use Doctrine\ORM\Mapping as ORM;
/**
* @package Chamilo\PluginBundle\Entity\AzureActiveDirectory
*
* @ORM\Table(name="azure_ad_sync_state")
* @ORM\Entity()
*/
class AzureSyncState
{
use TimestampableTypedEntity;
public const USERS_DATALINK = 'users_datalink';
public const USERGROUPS_DATALINK = 'usergroups_datalink';
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id()
* @ORM\GeneratedValue()
*/
private int $id = 0;
/**
* @ORM\Column(name="title", type="string")
*/
private string $title;
/**
* @ORM\Column(name="value", type="text")
*/
private string $value;
public function getId(): int
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): AzureSyncState
{
$this->title = $title;
return $this;
}
public function getValue(): string
{
return $this->value;
}
public function setValue(string $value): AzureSyncState
{
$this->value = $value;
return $this;
}
}

View File

@@ -0,0 +1,166 @@
<?php
/* For license terms, see /license.txt */
/**
* 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;
$GLOBALS['noredirection'] = empty($_GET) || isset($_GET['code']) || isset($_GET['state']) || isset($_GET['session_state']) || isset($_GET['error']);
require __DIR__.'/../../../main/inc/global.inc.php';
if (!empty($_GET['error']) && !empty($_GET['state'])) {
if ($_GET['state'] === ChamiloSession::read('oauth2state')) {
api_not_allowed(
true,
Display::return_message(
$_GET['error_description'] ?? $_GET['error'],
'warning'
)
);
} else {
ChamiloSession::erase('oauth2state');
exit('Invalid state');
}
}
$plugin = AzureActiveDirectory::create();
if ('true' !== $plugin->get(AzureActiveDirectory::SETTING_ENABLE)) {
api_not_allowed(true);
}
$provider = $plugin->getProvider();
if (!isset($_GET['code'])) {
// If we don't have an authorization code then get one by redirecting
// users to Azure (with the callback URL information)
$authUrl = $provider->getAuthorizationUrl();
ChamiloSession::write('oauth2state', $provider->getState());
header('Location: '.$authUrl);
exit;
}
// Check given state against previously stored one to mitigate CSRF attack
if (empty($_GET['state']) || ($_GET['state'] !== ChamiloSession::read('oauth2state'))) {
ChamiloSession::erase('oauth2state');
exit;
}
// Try to get an access token (using the authorization code grant)
try {
$token = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code'],
]);
} catch (Exception $exception) {
if ($exception->getMessage() == 'interaction_required') {
$message = Display::return_message($plugin->get_lang('additional_interaction_required'), 'error', false);
} else {
$message = Display::return_message($exception->getMessage(), 'error');
}
Display::addFlash($message);
header('Location: '.api_get_path(WEB_PATH));
exit;
}
$me = null;
try {
$userFields = [
'givenName',
'surname',
'mail',
'userPrincipalName',
'businessPhones',
'mobilePhone',
'accountEnabled',
'mailNickname',
'id',
];
$querySelect = implode(',', $userFields);
$me = $provider->get(
sprintf('me?$select=%s', $querySelect),
$token
);
if (empty($me)) {
throw new Exception('Token not found.');
}
// We use the e-mail to authenticate the user, so check that at least one
// e-mail source exists
if (empty($me['mail'])) {
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 username for this user.');
}
if (empty($me['id'])) {
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($me);
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['id'];
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 */
if (empty($userInfo)) {
throw new Exception('User '.$userId.' not found.');
}
if ($userInfo['active'] != '1') {
throw new Exception(get_lang('AccountInactive'));
}
} catch (Exception $exception) {
$message = Display::return_message($exception->getMessage(), 'error');
Display::addFlash($message);
header('Location: '.api_get_path(WEB_PATH));
exit;
}
$userInfo['uidReset'] = true;
$_GET['redirect_after_not_allow_page'] = 1;
$redirectAfterNotAllowPage = ChamiloSession::read('redirect_after_not_allow_page');
ChamiloSession::clear();
ChamiloSession::write('redirect_after_not_allow_page', $redirectAfterNotAllowPage);
ChamiloSession::write('_user', $userInfo);
ChamiloSession::write('_user_auth_source', 'azure_active_directory');
Event::eventLogin($userInfo['user_id']);
$GLOBALS['noredirection'] = false;
Redirect::session_request_uri(true, $userInfo['user_id']);

View File

@@ -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());
}

View File

@@ -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());
}

View File

@@ -0,0 +1,9 @@
<?php
/* For licensing terms, see /license.txt */
if (!api_is_platform_admin()) {
exit('You must have admin permissions to uninstall plugins');
}
AzureActiveDirectory::create()->uninstall();

View File

@@ -0,0 +1,22 @@
{% if not _u.logged %}
<div id="azure-active-directory-login">
{% if not azure_active_directory.block_title is empty %}
<h4>{{ azure_active_directory.block_title }}</h4>
{% endif %}
{% if not azure_active_directory.signin_url is empty %}
<a href="{{ azure_active_directory.signin_url }}" class="btn btn-default">{{ 'SignIn'|get_lang }}</a>
{% endif %}
{% if not azure_active_directory.signout_url is empty %}
<a href="{{ azure_active_directory.signout_url }}" class="btn btn-danger">{{ 'Logout'|get_lang }}</a>
{% endif %}
{% if azure_active_directory.management_login_enabled %}
<hr>
<a href="{{ _p.web_plugin ~ 'azure_active_directory/login.php' }}">
{{ azure_active_directory.management_login_name }}
</a>
{% endif %}
</div>
{% endif %}

View File

@@ -0,0 +1,26 @@
<div class="row">
<div class="col-sm-4 col-sm-offset-4">
{{ login_language_form }}
{{ login_form }}
{% if "allow_lostpassword"|api_get_setting == 'true' or "allow_registration"|api_get_setting == 'true' %}
<ul class="nav nav-pills nav-stacked">
{% if "allow_registration"|api_get_setting != 'false' %}
<li><a href="{{ _p.web_main }}auth/inscription.php">{{ 'SignUp'|get_lang }}</a></li>
{% endif %}
{% if "allow_lostpassword" | api_get_setting == 'true' %}
{% set pass_reminder_link = 'pass_reminder_custom_link'|api_get_configuration_value %}
{% set lost_password_link = _p.web_main ~ 'auth/lostPassword.php' %}
{% if not pass_reminder_link is empty %}
{% set lost_password_link = pass_reminder_link %}
{% endif %}
<li><a href="{{ lost_password_link }}"> {{ 'LostPassword' | get_lang }} </a></li>
{% endif %}
</ul>
{% endif %}
</div>
</div>