Actualización
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
/* For license terms, see /license.txt */
|
||||
|
||||
use Chamilo\UserBundle\Entity\User;
|
||||
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 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',
|
||||
];
|
||||
|
||||
parent::__construct('2.4', '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';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Azure
|
||||
*/
|
||||
public function getProvider()
|
||||
{
|
||||
$provider = 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',
|
||||
]);
|
||||
|
||||
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
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
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'),
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?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;
|
||||
|
||||
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'],
|
||||
'resource' => 'https://graph.windows.net',
|
||||
]);
|
||||
} 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 {
|
||||
$me = $provider->get('me', $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['objectId'])) {
|
||||
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['objectId'];
|
||||
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']);
|
||||
Redirect::session_request_uri(true, $userInfo['user_id']);
|
||||
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user