Skip to content
Snippets Groups Projects
Commit 2b8b9fae authored by Vegard A. Johansen's avatar Vegard A. Johansen
Browse files

initial

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 1364 additions and 0 deletions
## INTRODUCTION
Module that allows you to connect to the Microsoft Graph API for your tenant, and import users from Microsoft Entra ID (prev. Azure AD) to Drupal user entities. More information at https://www.drupal.org/project/entrasync
## FEATURES
The module will connect to your tenant and fetch the users that are not already in Drupal, and queue these up for importing using the Queue API.
You are able to map the user fields from Entra to your own Drupal fields on the user, you can decide which roles the incoming users should get, wether the user should be active or not, and wether you want to send welcome e-mail to the users, if they are set to active.
## REQUIREMENTS
The module is dependent on the Microsoft Graph API module, which is what you will use to authenticate to your tenant, so the first thing to do is set that up at Administration » Configuration » Web Services » Microsoft Graph API.
It is also dependent on having an Azure app configured with the right permissions. If you want to test it a good way to start is to sign up for the Microsoft 365 Developer Program. This is free, and will give you a free tenant with demo users to test with.
It's recommended to install Queue UI module, as this will give you a UI to what this modules queue, and also gives the extra possibility to process queues via batch on demand, and not only via cron.
## INSTALLATION
Install as you would normally install a contributed Drupal module.
See: https://www.drupal.org/node/895232 for further information.
## CONFIGURATION
- (Have a configured app for Endtra ID)
- Set up your connection to Microsoft Graph API for your tenant at Administration » Configuration » Web Services » Microsoft Graph API.
- Configure the processing you want to do on the incoming users at Administration » Configuration » Web Services » Microsoft Entra Synchronization Settings.
- Click the sync button, or run up cron.
## MAINTAINERS
Current maintainers for Drupal 10:
- Vegard A. Johansen (vegardjo) - https://www.drupal.org/u/vegardjo
entrauser_status: []
graph_key: 'ms_graph_api_default_key'
mapped_drupal_entities: []
modify_entrauser_roles: []
modify_drupaluser_roles: []
retrieve_on_cron: ''
\ No newline at end of file
services:
entrasync.commands:
class: \Drupal\entrasync\Commands\EntraSyncCommands
tags:
- { name: drush.command }
name: 'Microsoft Entra User Sync'
type: module
description: 'Utility module to sync users in Microsoft Entra with users in Drupal'
package: Custom
dependencies:
- ms_graph_api:ms_graph_api
core_version_requirement: ^10
\ No newline at end of file
entrasync.admin_settings:
title: 'Microsoft Entra Synchronisation Settings'
description: 'Configure MS tenant, sync settings and perform manual synchronisation.'
parent: system.admin_config_services
route_name: entrasync.manual_sync
weight: 100
entrasync.manual_sync:
title: 'Manual Entra Synchronisation'
parent: entrasync.admin_settings
route_name: entrasync.manual_sync
weight: 1
entrasync.sync_settings:
title: 'Entra Synchronisation Settings'
parent: entrasync.admin_settings
route_name: entrasync.sync_settings
weight: 2
entrasync.graph_settings:
title: 'Entra Tenant Settings'
parent: entrasync.admin_settings
route_name: entrasync.graph_settings
weight: 3
entrasync.manual_sync_tab:
title: 'Manual Synchronisation'
route_name: entrasync.manual_sync
base_route: entrasync.sync_settings
weight: 1
entrasync.sync_settings_tab:
title: 'Synchronisation settings'
route_name: entrasync.sync_settings
base_route: entrasync.sync_settings
weight: 2
entrasync.graph_settings_tab:
title: 'MS Graph Authentication'
route_name: entrasync.graph_settings
base_route: entrasync.sync_settings
weight: 3
<?php
/**
* @file
* Contains hooks etc for Entrasync.
*/
/**
* Implements hook_cron().
*
* This will queue potentially new users on both sites to the queueworkers, and
* queueworkers are also processed on cron.
*
* @todo Race? Check if we could get some kind of race condition here.
*/
function entrasync_cron() {
$config = \Drupal::config('entrasync.settings');
if ($config->get('retrieve_on_cron')) {
\Drupal::service('entrasync.entra_sync')->fullSync();
}
}
administer entra graph settings:
title: 'Administer Microsoft Entra graph settings'
description: 'Access the graph authentication settings page.'
administer entra sync settings:
title: 'Administer Entra sync settings'
description: '<em>Warning! This grants permission to alter all roles and statuses for users, and should only be given to trusted roles.</em>'
perform manual entra sync:
title: 'Synchronize users manually'
description: 'Sync data between MS Entra and Drupal manually. <em>Warning! should only be given to trusted roles.</em>'
entrasync.graph_settings:
path: '/admin/config/services/entrasync/graph-settings'
defaults:
_controller: '\Drupal\entrasync\Controller\AdminPageController::graphSettings'
_title: 'MS Graph Autentication'
requirements:
_permission: 'administer entra graph settings'
entrasync.sync_settings:
path: '/admin/config/services/entrasync/sync-settings'
defaults:
_controller: '\Drupal\entrasync\Controller\AdminPageController::syncSettings'
_title: 'Entra sync Settings'
requirements:
_permission: 'administer entra sync settings'
entrasync.manual_sync:
path: '/admin/config/services/entrasync/sync'
defaults:
_controller: '\Drupal\entrasync\Controller\AdminPageController::manualSync'
_title: 'Manual Sync'
requirements:
_permission: 'perform manual entra sync'
services:
# entrasync.microsoft_graph_api_client:
# class: Drupal\entrasync\Services\MicrosoftGraphApiClient
# arguments:
# - '@key.repository'
# - '@config.factory'
entrasync.entra_sync:
class: Drupal\entrasync\Services\EntraSync
arguments:
- '@entity_type.manager'
- '@ms_graph_api.graph.factory'
- '@queue'
- '@messenger'
- '@logger.factory'
- '@config.factory'
\ No newline at end of file
<?php
namespace Drupal\entrasync\Commands;
use Drush\Commands\DrushCommands;
/**
* Defines Drush commands for the Azure Sync module.
*/
class EntraSyncCommands extends DrushCommands {
/**
* Test Microsoft Graph SDK.
*
* @command entrasync:test_graph_sdk
* @aliases entra-test
* @usage entrasync:test_graph_sdk
* Test the Microsoft Graph SDK.
*/
public function testGraphSdk() {
$result = 'todo';
$this->output()->writeln($result);
}
}
<?php
namespace Drupal\entrasync\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\entrasync\Services\EntraSync;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class AdminPageController.
*
* Controller for administrative pages of the EntraSync module.
*/
class AdminPageController extends ControllerBase {
/**
* The EntraSync service.
*
* @var Drupal\entrasync\Services\EntraSync
*/
protected $entraSync;
/**
* The form builder service.
*
* @var Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* Constructs an AdminPageController object.
*
* @param Drupal\entrasync\Services\EntraSync $entraSync
* The EntraSync service.
* @param Drupal\Core\Form\FormBuilderInterface $formBuilder
* The form builder service.
*/
public function __construct(EntraSync $entraSync, FormBuilderInterface $formBuilder) {
$this->entraSync = $entraSync;
$this->formBuilder = $formBuilder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entrasync.entra_sync'),
$container->get('form_builder')
);
}
/**
* Renders the tenant settings form.
*
* @return array
* A render array representing the tenant settings form.
*/
public function graphSettings() : Array {
return $this->formBuilder->getForm('Drupal\entrasync\Form\GraphSettingsForm');
}
/**
* Renders the synchronization settings form.
*
* @return array
* A render array representing the synchronization settings form.
*/
public function syncSettings() : Array {
return $this->formBuilder->getForm('Drupal\entrasync\Form\SyncSettingsForm');
}
/**
* Handles the manual synchronization process.
*
* Fetches lists of Entra and Drupal users, compares them,
* and initiates the synchronization.
*
* @return array
* A render array for displaying the synchronization result.
*/
public function manualSync() {
return $this->formBuilder->getForm('Drupal\entrasync\Form\ManualSyncForm');
}
}
<?php
namespace Drupal\entrasync\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Form handler for the Entrasync tenant settings.
*/
class GraphSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['entrasync.settings'];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'entrasync_tenant_settings';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('entrasync.settings');
$form['graph_key'] = [
'#type' => 'key_select',
'#key_filters' => ['type' => 'ms_graph_api'],
'#title' => $this->t('MS Graph Authentication Key'),
'#default_value' => $config->get('graph_key'),
'#required' => TRUE,
'#key_description' => FALSE,
'#description' => t('Choose an available key. If the desired key is not listed, <a href=":link">create a new key</a> of type "MS Graph API Key".', [':link' => Url::fromRoute('entity.key.add_form')->toString()]),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('entrasync.settings')
->set('graph_key', $form_state->getValue('graph_key'))
->save();
parent::submitForm($form, $form_state);
}
}
<?php
namespace Drupal\entrasync\Form;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\entrasync\Services\EntraSync;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form for manual synchronization operations.
*
* Provides a Drupal form to perform various synchronization tasks with the
* EntraSync service.
*/
class ManualSyncForm extends FormBase implements ContainerInjectionInterface {
/**
* The EntraSync service.
*
* @var \Drupal\entrasync\Services\EntraSync
*/
protected $entraSync;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The logger service.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Constructs a new ManualSyncForm object.
*
* @param \Drupal\entrasync\Services\EntraSync $entraSync
* The EntraSync service.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \Psr\Log\LoggerInterface $logger
* The logger service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(EntraSync $entraSync,
MessengerInterface $messenger,
LoggerInterface $logger,
TranslationInterface $string_translation) {
$this->entraSync = $entraSync;
$this->messenger = $messenger;
$this->logger = $logger;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entrasync.entra_sync'),
$container->get('messenger'),
$container->get('logger.factory')->get('entrasync'),
$container->get('string_translation'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'entrasync_manual_sync';
}
/**
* Builds the form for manual synchronization.
*
* @param array $form
* The form structure.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The form array.
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['get_sync_status'] = [
'#type' => 'submit',
'#value' => $this->t('Get syncronisation status'),
'#submit' => ['::submitForm', '::getStatus'],
];
$form['full_sync'] = [
'#type' => 'submit',
'#value' => $this->t('Perform full sync'),
'#description' => $this->t('hey to I deszcribe'),
'#submit' => ['::submitForm', '::fullSync'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Default stuff here if needed.
}
/**
* Retrieves and displays the current synchronization status.
*
* @param array &$form
* The form structure.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function getStatus(array &$form, FormStateInterface $form_state) {
$this->messenger->addStatus($this->entraSync->getStatus());
}
/**
* Performs a full synchronization operation.
*
* This method triggers the full synchronization process
* of the EntraSync service.
*/
public function fullSync() : Void {
$this->entraSync->fullSync();
}
}
<?php
namespace Drupal\entrasync\Form;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for the EntraSync settings form.
*
* @package Drupal\entrasync\Form
*/
class SyncSettingsForm extends ConfigFormBase {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity field manager service.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['entrasync.settings'];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'entrasync_sync_settings';
}
/**
* Constructs a new SyncSettingsForm.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager,
EntityFieldManagerInterface $entityFieldManager) {
$this->entityTypeManager = $entityTypeManager;
$this->entityFieldManager = $entityFieldManager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('entity_field.manager')
);
}
/**
* Extracts user field mappings from form state.
*
* @param FormStateInterface $form_state
* The form state object.
*
* @return array
* An associative array of user field mappings.
*/
protected function getUserFieldMappingsFromFormState(FormStateInterface $form_state) {
$user_field_mapping = [];
foreach ($form_state->getValues() as $key => $value) {
if (strpos($key, 'user_field_to_') === 0) {
$entra_field = substr($key, strlen('user_field_to_'));
$user_field_mapping[$entra_field] = $value;
}
}
return $user_field_mapping;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('entrasync.settings');
// Start of general settings details
$form['general_settings'] = [
'#type' => 'details',
'#title' => $this->t('General setting'),
];
// Start of Entra settings details
$form['entrauser_settings'] = [
'#type' => 'details',
'#title' => $this->t('Settings for incoming users from Entra'),
];
// Start of Drupal settings details
$form['drupaluser_settings'] = [
'#type' => 'details',
'#title' => $this->t('Settings for remaining Drupal users'),
'#description' => $this->t('Decide what to do with users that are not in Entra, but still in Drupal.
This could be users that are orphaned, or they could be legit user that should be
there. User 1 is excluded'),
'#description_display' => 'above',
];
$form['general_settings']['retrieve_on_cron'] = [
'#type' => 'checkbox',
'#title' => $this->t('Import new users on cron'),
'#default_value' => $config->get('retrieve_on_cron', FALSE),
'#description' => $this->t('Enable to check for new users each time cron runs. If disabled you will need
to do a <a href=":link">manual syncronisation</a>, or invoke it via drush.',
[':link' => Url::fromRoute('entrasync.manual_sync')->toString()]),
];
// Add a select element for entity type to map to, per now only user is supported.
$entity_options = ['user' => 'User', 'node' => 'Node'];
$form['entrauser_settings']['entrauser_entities'] = [
'#type' => 'radios',
'#title' => $this->t('Map Entra users to Drupal entity'),
'#options' => $entity_options,
'#default_value' => 'user',
'#multiple' => FALSE,
'#description' => $this->t('Select which Drupal entities the new users should be added to (per now only user is supported)'),
'#attributes' => ['disabled' => ['node']],
];
// Info about mandatory mappings
$mandatory_mappings_message = '<p><strong>Mandatory field mappings</strong>
<p>The <em>User Principal Name (UPN)</em> is mapped to the
<em>Drupal user name</em>. This is the id from Entra, and changing it
will impact the functionality of the module.</p>
<p>The <em>email</em> field from Entra is often the same as the UPS,
and is mapped to the Drupal email field.</p>';
$form['entrauser_settings']['mandatory_mappings_html'] = [
'#type' => 'markup',
'#markup' => $this->t($mandatory_mappings_message),
];
// Add field mappings, to map Entra data to Drupal fields of selected entity
// Fields coming from Entra:
$entra_fields = [
// 'userprincipalname' => $this->t('User Principal Name'),
'displayName' => $this->t('Display Name'),
'givenname' => $this->t('Given Name'),
'surname' => $this->t('Surname'),
'businessphones' => $this->t('Business Phones'),
'mobilephone' => $this->t('Mobile Phone'),
'department' => $this->t('Department'),
// 'email' => $this->t('Email'),
'jobtitle' => $this->t('Job Title'),
'officelocation' => $this->t('Office Location'),
'id' => $this->t('ID')
];
// Fetch Drupal user fields, including custom fields
$user_fields = $this->entityFieldManager->getFieldDefinitions('user', 'user');
/**
* @todo Might be better to go for a whitelist here, in case other modules add fields
* to the user? Or we might want to support mapping also to that?
*/
// Exclude certain fields like 'uuid', and include only custom fields.
$exclude_user_fields = [
'name',
'mail',
'uuid',
'uid',
'langcode',
'created',
'changed',
'access',
'login',
'status',
'timezone',
'roles',
'langcode',
'preferred_langcode',
'preferred_admin_langcode',
'init',
'pass',
'timezone',
'default_langcode',
'path',
];
// Initiate options array
$drupal_user_field_options = [];
// Filter the options so we get only fields we want
foreach ($user_fields as $field_name => $field_definition) {
if (!in_array($field_name, $exclude_user_fields) && $field_definition->getType() != 'entity_reference') {
$drupal_user_field_options[$field_name] = $field_definition->getLabel();
}
}
/**
* Create unique form items per field mapping. With previous mapping as default, if set.
*
*/
$stored_mappings = $config->get('user_field_mapping');
foreach ($entra_fields as $entra_field_key => $entra_field_label) {
// Check if there's a stored mapping for this field.
$default_value = isset($stored_mappings[$entra_field_key]) ? $stored_mappings[$entra_field_key] : NULL;
$form['entrauser_settings']['user_field_to_' . $entra_field_key] = [
'#type' => 'select',
'#title' => $this->t('Map Entra field: @entra_field', ['@entra_field' => $entra_field_label]),
'#options' => $drupal_user_field_options,
'#default_value' => $default_value,
'#empty_option' => $this->t('- Select a Drupal field -'),
];
}
// Add a select element for roles to modify on import.
$roles = $this->entityTypeManager->getStorage('user_role')->loadMultiple();
unset($roles['authenticated']);
unset($roles['anonymous']);
$role_options = [];
foreach ($roles as $role_id => $role) {
$role_options[$role_id] = $role->label();
};
$form['entrauser_settings']['modify_entrauser_roles'] = [
'#type' => 'select',
'#title' => $this->t('Modify roles on Entra users'),
'#options' => $role_options,
'#default_value' => $config->get('modify_entrauser_roles'),
'#multiple' => TRUE,
'#description' => $this->t('Select which roles the new users should or should not have'),
];
// Add a select element to chose initial state of the imported user.
$user_status_options = ['blocked' => 'Blocked', 'active' => 'Active'];
$form['entrauser_settings']['entrauser_status'] = [
'#type' => 'radios',
'#title' => $this->t('Initial state of imported user'),
'#options' => $user_status_options,
'#default_value' => 'blocked',
'#multiple' => FALSE,
'#description' => $this->t('Select wether the user should be created as blocked or active account.'),
];
$form['entrauser_settings']['send_mail_on_activate'] = [
'#type' => 'checkbox',
'#title' => $this->t('Send activation e-mail when creating user'),
'#default_value' => $config->get('send_mail_on_activate', FALSE),
'#states' => [
'visible' => [
':input[name="entrauser_status"]' => ['value' => 'active'],
],
],
'#description' => $this->t('Enable to send the Welcome (new user created by administrator) e-mail when account is created.
Edit the e-mail in <a href=":link">Account settings</a>',
[':link' => Url::fromRoute('entity.user.admin_form')->toString()]),
];
// $form['drupaluser_settings']['deyo'] = [
// '#type' => 'radios',
// '#title' => $this->t('lalal'),
// '#options' => $user_status_options,
// '#default_value' => 'blocked',
// '#multiple' => FALSE,
// '#description' => $this->t('Select wether the user should be imported as blocked or active. Note that if active welcome e-mails may be sent out.'),
// ];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritDoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
/**
* @todo Validate that an Entra field is not mapped to multiple Drupal fields.
*/
$user_field_mapping = $this->getUserFieldMappingsFromFormState($form_state);
$selected_values = [];
foreach ($user_field_mapping as $entra_field => $drupal_field) {
if (!empty($drupal_field)) {
if (isset($selected_values[$drupal_field])) {
// Set an error if the same Drupal field is selected more than once.
$form_state->setErrorByName('user_field_to_' . $entra_field, $this->t('The Drupal field %field is already mapped to another Entra field.', ['%field' => $drupal_field]));
} else {
$selected_values[$drupal_field] = $entra_field;
}
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('entrasync.settings');
// cron settings handling
$config->set('retrieve_on_cron', $form_state->getValue('retrieve_on_cron'));
// Map to which Drupal entity settings handling
$mapped_drupal_entity = $form_state->getValue('entrauser_entities');
$mapped_drupal_entity = (array) ($mapped_drupal_entity);
$config->set('mapped_drupal_entities', $mapped_drupal_entity);
// Entra fields to Drupal fields handling
$user_field_mapping_config = $this->getUserFieldMappingsFromFormState($form_state);
// Remove any mappings that have an empty value
$user_field_mapping_config = array_filter($user_field_mapping_config, function ($value) {
return $value !== '';
});
$config->set('user_field_mapping', $user_field_mapping_config);
// Role settings handling
$modify_entrauser_roles = $form_state->getValue('modify_entrauser_roles');
$modify_entrauser_roles = empty($modify_entrauser_roles) ? [] : $modify_entrauser_roles;
/**
* @todo This has no corresponding settings field
*
*/
$modify_drupaluser_roles = $form_state->getValue('modify_drupaluser_roles');
$modify_drupaluser_roles = empty($modify_drupaluser_roles) ? [] : $modify_drupaluser_roles;
$config->set('modify_entrauser_roles', array_keys($modify_entrauser_roles));
// Initial user state handling (blocked or active)
$entrauser_status = $form_state->getValue('entrauser_status');
$config->set('entrauser_status', $entrauser_status);
// E-mail settings if user is set to active
$email_on_active = $form_state->getValue('send_mail_on_activate');
$config->set('send_mail_on_activate', $email_on_active);
// Save all set config
$config->save();
parent::submitForm($form, $form_state);
}
}
<?php
namespace Drupal\entrasync\Plugin\QueueWorker;
use Drupal\Core\Queue\QueueWorkerBase;
/**
* Processes Common (Entra and Drupal) users.
*
* @QueueWorker(
* id = "entra_common_user_processor",
* title = @Translation("Entra and Drupal common user processor"),
* cron = {"time" = 60}
* )
*/
class CommonUserProcessor extends QueueWorkerBase {
/**
* {@inheritdoc}
*/
public function processItem($data) {
\Drupal::logger('entrasync')->error('process log common: ' . $data);
// Process the user data.
// $data will be an individual item from the queue.
}
}
<?php
namespace Drupal\entrasync\Plugin\QueueWorker;
use Drupal\Core\Queue\QueueWorkerBase;
/**
* Processes Drupal users.
*
* @QueueWorker(
* id = "entra_drupal_user_processor",
* title = @Translation("Entra Drupal User Processor"),
* cron = {"time" = 60}
* )
*/
class DrupalUserProcessor extends QueueWorkerBase {
/**
* {@inheritdoc}
*/
public function processItem($data) {
\Drupal::logger('entrasync')->notice('process log drupaluser: ' . $data['email'] . ' currently doing nada');
// Process the user data.
// $data will be an individual item from the queue.
}
}
<?php
namespace Drupal\entrasync\Plugin\QueueWorker;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Password\PasswordGeneratorInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Processes Entra users.
*
* @QueueWorker(
* id = "entra_user_processor",
* title = @Translation("Entra User Processor"),
* cron = {"time" = 60}
* )
*/
class EntraUserProcessor extends QueueWorkerBase implements ContainerFactoryPluginInterface {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The logger factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* The logger.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* The config.
*
* @var Drupal\Core\Config\ConfigFactoryInterface
*/
protected $config;
/**
* The password generator.
*
* @var \Drupal\Core\Password\PasswordGeneratorInterface
*/
protected $passwordGenerator;
/**
* {@inheritdoc}
*/
public function __construct(ConfigFactoryInterface $configFactory,
LoggerChannelFactoryInterface $loggerFactory,
PasswordGeneratorInterface $passwordGenerator) {
$this->configFactory = $configFactory;
$this->config = $configFactory->get('entrasync.settings');
$this->logger = $loggerFactory->get('entrasync');
$this->passwordGenerator = $passwordGenerator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('config.factory'),
$container->get('logger.factory'),
$container->get('password_generator')
);
}
/**
* {@inheritdoc}
*/
public function processItem($data) {
// Check if user already exists by email.
$users = user_load_by_mail($data['email']);
if (!$users) {
try {
// Create user account.
$user = User::create();
// Set mandatory fields.
$user->setPassword($this->passwordGenerator->generate());
$user->enforceIsNew();
$user->setEmail($data['email']);
$user->setUsername($data['userprincipalname']);
// Set custom fields.
$user_field_mapping = $this->config->get('user_field_mapping');
foreach ($user_field_mapping as $entra_field => $drupal_field) {
if (isset($data[$entra_field]) && $data[$entra_field] !== '') {
// Tmp: Flatten array values to a comma-separated string,
// this is true for the businessPhones data.
$field_value = is_array($data[$entra_field]) ? implode(', ', $data[$entra_field]) : $data[$entra_field];
if ($user->hasField($drupal_field)) {
$user->set($drupal_field, $field_value);
}
else {
$this->logger->error('The field ' . $drupal_field . ' does not exist on the user entity.');
}
}
}
// Retrieve the configuration for roles.
$roles_to_modify = $this->config->get('modify_entrauser_roles');
// Add roles to the new user.
foreach ($roles_to_modify as $role_id) {
$user->addRole($role_id);
}
// Default state is blocked.
if ($this->config->get('entrauser_status') === 'active') {
$user->activate();
$user->save();
if ($this->config->get('send_mail_on_activate')) {
_user_mail_notify('register_admin_created', $user);
}
}
else {
$user->save();
}
$this->logger->notice('Created new user with ID: ' . $user->id());
}
catch (\Exception $e) {
$this->logger->error('User creation failed: ' . $e->getMessage());
}
}
else {
/*
* @todo This is not logging anything for some reason
*/
$this->logger->notice('User already exists with email: ' . $data['email']);
}
}
}
<?php
namespace Drupal\entrasync\Services;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\ms_graph_api\GraphApiGraphFactory;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
class EntraSync {
/**
* The entity type manager interface.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The Microsoft Graph API client.
*
* @var Drupal\ms_graph_api\GraphApiGraphFactory
*/
protected $graphFactory;
/**
* The Microsoft Graph API client.
*
* @var \Microsoft\Graph\Graph
*/
protected $graphClient;
/**
* The queue factory.
*
* @var \Drupal\Core\Queue\QueueFactory
*/
protected $queueFactory;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The logger channel.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* The configuration object.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* Constructs a new EntraSync object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* @param \Microsoft\Graph\GraphFactory $MicrosoftGraphApiFactory
* @param \Drupal\Core\Queue\QueueFactory $queueFactory
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
*
*/
function __construct(EntityTypeManagerInterface $entityTypeManager,
GraphApiGraphFactory $MicrosoftGraphApiFactory,
QueueFactory $queueFactory,
MessengerInterface $messenger,
LoggerChannelFactoryInterface $loggerFactory,
ConfigFactoryInterface $configFactory) {
$this->entityTypeManager = $entityTypeManager;
$this->graphFactory = $MicrosoftGraphApiFactory;
$this->queueFactory = $queueFactory;
$this->messenger = $messenger;
$this->logger = $loggerFactory->get('entrasync');
$this->config = $configFactory->get('entrasync.settings');
// $this->graphClient = $this->graphFactory->buildGraphFromKeyId('ms_graph_api_default_key');
}
/**
* Retrieves the status of user synchronization.
*
* @todo Everything
* @return array An associative array of synchronization status information.
*/
public function getStatus() {
// For an administrator overseeing the synchronization between Drupal users and Microsoft Entra (Azure AD), providing insightful statuses can greatly assist in monitoring and managing the system effectively. Here are some useful statuses you might consider:
// Last Synchronization Time: Date and time of the last successful synchronization. This helps in understanding the currency of the data.
// Number of Users in Drupal: The current count of users in the Drupal system.
// Number of Users in Entra (Azure AD): The current count of users in Entra.
// Number of Users Synced: How many users are successfully synchronized between the two systems.
// Number of Users Pending Sync: Users existing in one system but not yet synced to the other.
// Recent Sync Errors/Issues: Any errors or issues encountered during the last sync process, with timestamps.
// Status of Last Sync: Whether the last sync was successful, partially successful (with some errors), or failed.
// User Sync Mismatch Details: Specific details about any discrepancies between the user data in Drupal and Entra, such as mismatched user roles or profiles.
// Duration of the Last Sync Process: How long the last synchronization process took.
// Manual Intervention Required: Flag or notice if any manual intervention is required for specific cases or errors.
// Audit Logs Link: A direct link to detailed logs or an audit trail for more in-depth analysis.
// System Health Status: Overall health status of the sync system, possibly with a simple color-coded indicator (green for healthy, yellow for warnings, red for critical issues).
// Scheduled Next Sync: If synchronization is done on a schedule, show the next planned sync time.
// User Actions Required: Any actions that the administrator needs to take, such as approving user access, resolving conflicts, etc.
// API Call Statistics: Information about API usage, rate limits, and any related errors, if applicable.
// Version Information: Display the version of the sync software or module being used, useful for troubleshooting and updates.
// Implementing the Statuses
// These statuses can be implemented in the getStatus method of your synchronization class or a similar utility. Depending on your system's complexity, you might also need additional methods or services to gather this information. Remember, presenting this data in a clear, concise, and user-friendly manner in the Drupal admin interface is key to its usefulness.
// $entraUserCount = '33';
// $drupalUserCount = '53';
// return [
// 'entraUserCount' => $entraUserCount,
// 'drupalUserCount' => $drupalUserCount,
// ];
}
/**
* Retrieves a list of users from Microsoft Entra.
*
* @return array An array of user information from Entra.
*/
public function getEntraUsersList() {
/** @var \Microsoft\Graph\Graph $client */
$client = $this->graphFactory->buildGraphFromKeyId($this->config->get('client_secret'));
$getCollectionTimeStart = microtime(true);
try {
$userCollectionRequest = $client
->createCollectionRequest("GET", "/users")
->setReturnType(\Microsoft\Graph\Model\User::class);
// Handle pagination
$allUsers = [];
while(!$userCollectionRequest->isEnd()) {
$usersPage = $userCollectionRequest->getPage();
foreach($usersPage as $user) {
$allUsers[] = $user;
}
}
// SDK v 2
// $users = $userCollecton->getValue();
// getting only the information we need from the user classes
$destilledEntraUserInfo = [];
foreach($allUsers as $user) {
$userDetails = [
'userprincipalname' => $user->getUserPrincipalName(),
'displayName' => $user->getDisplayName(),
'givenname' => $user->getGivenName(),
'surname' => $user->getSurname(),
'businessphones' => $user->getBusinessPhones(),
'mobilephone' => $user->getMobilePhone(),
'department' => $user->getDepartment(),
'email' => $user->getMail(),
'jobtitle' => $user->getJobTitle(),
'officelocation' => $user->getOfficeLocation(),
'id' => $user->getId(),
];
$destilledEntraUserInfo[] = $userDetails;
}
// adding some logging:
$numberOfUsersFetched = count($destilledEntraUserInfo);
$getCollectionTimeStop = microtime(true);
$getCollectionExecutionTime = sprintf("%.2f", ($getCollectionTimeStop - $getCollectionTimeStart));
$logMessage = 'Fetching ' . $numberOfUsersFetched . ' users from Entra took ' . $getCollectionExecutionTime . ' seconds';
$this->logger->notice($logMessage);
$this->messenger->addMessage($logMessage);
return $destilledEntraUserInfo;
}
catch (\Throwable $e) {
$this->logger->error($e->getMessage());
return [];
}
}
/**
* Retrieves a list of Drupal users.
*
* @return array An array of Drupal user information.
*/
public function getDrupalUsersList() : Array {
// getting all drupal user objects
$userStorage = $this->entityTypeManager->getStorage('user');
$query = $userStorage->getQuery();
$uids = $query
->accessCheck(TRUE)
->condition('uid', 1, '!=')
->execute();
/** @var \Drupal\user\UserInterface $users */
$users = $userStorage->loadMultiple($uids);
// destilling the objects to only get an array of emails.
$drupalUserList = [];
foreach($users as $user) {
$userDetails = [
'email' => $user->getEmail(),
];
$drupalUserList[] = $userDetails;
}
return $drupalUserList;
}
/**
* Compares user lists from Entra and Drupal to identify unique and common users.
*
* @param array $entraUsers An array of users from Entra.
* @param array $drupalUsers An array of Drupal users.
*
* @return array An associative array categorizing users.
*
* @todo Add a count per array and return and/or log these values as well.
*/
public function compareUserLists(Array $entraUsers, Array $drupalUsers) : Array {
$drupalEmails = array_column($drupalUsers, 'email');
$onlyInEntra = [];
$onlyInDrupal = [];
$inBoth = [];
foreach ($entraUsers as $entraUser) {
if (in_array($entraUser['userprincipalname'], $drupalEmails)) {
$inBoth[] = $entraUser; // User is in both Entra and Drupal.
} else {
$onlyInEntra[] = $entraUser; // User is only in Entra.
}
}
foreach ($drupalUsers as $drupalUser) {
if (!in_array($drupalUser['email'], array_column($entraUsers, 'userprincipalname'))) {
$onlyInDrupal[] = $drupalUser; // User is only in Drupal.
}
}
// Return the categorized user objects.
return [
'onlyInEntra' => $onlyInEntra,
'onlyInDrupal' => $onlyInDrupal,
'inBoth' => $inBoth
];
}
/**
* Delegating users present only in Entra to a separate queue
*
* @param array $entraUsers An array of Entra users.
*/
public function processUsersOnlyInEntra(Array $entraUsers) {
$queue = $this->queueFactory->get('entra_user_processor');
foreach ($entraUsers as $user) {
$this->logger->notice('Adding Entra user to queue: ' . $user['userprincipalname']);
$queue->createItem($user);
}
}
/**
* Delegating users present only in Drupal to a separate queue
*
* @param array $drupalUsers An array of Drupal users.
*/
public function processUsersOnlyInDrupal(Array $drupalUsers) {
$queue = $this->queueFactory->get('entra_drupal_user_processor');
// Add each user to the queue
foreach ($drupalUsers as $user) {
$this->logger->notice('Adding Drupal user to queue: ' . $user['email']);
$queue->createItem($user);
}
}
public function processCommonUsers(Array $commonEmails) {}
/**
* Prepares data for synchronization.
*
* @return array An array containing data for synchronization.
*/
public function prepareSyncData() : Array {
return $this->compareUserLists($this->getEntraUsersList(), $this->getDrupalUsersList());
}
public function fullSync() {
// Handle the syncing of users.
$onlyInEntra = $this->prepareSyncData()['onlyInEntra'];
$onlyInDrupal = $this->prepareSyncData()['onlyInDrupal'];
// perform both syncs.
$this->processUsersOnlyInEntra($onlyInEntra);
$this->processUsersOnlyInDrupal($onlyInDrupal);
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment