o365-2.0.3/src/RolesService.php

src/RolesService.php
<?php

declare(strict_types=1);

namespace Drupal\o365;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\TempStore\TempStoreException;
use Drupal\user\UserInterface;
use GuzzleHttp\Exception\GuzzleException;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Psr\Log\LoggerInterface;

/**
 * Service for managing user roles based on Microsoft 365 group membership.
 */
final class RolesService {

  /**
   * The logger service.
   *
   * @var \Psr\Log\LoggerInterface
   */
  private LoggerInterface $logger;

  /**
   * Cached configured role mappings.
   *
   * @var array<string, string[]>|null
   */
  private ?array $configuredRoles = NULL;

  /**
   * Cached safe roles list.
   *
   * @var string[]|null
   */
  private ?array $safeRoles = NULL;

  /**
   * Cached user groups from Microsoft 365.
   *
   * @var array<int, array<string, mixed>>|null
   */
  private ?array $userGroups = NULL;

  /**
   * The default role to give to users if any.
   *
   * @var string
   */
  private string $defaultRole = '';

  /**
   * Constructs a RolesService object.
   */
  public function __construct(
    private readonly GraphService $graphService,
    private readonly ConfigFactoryInterface $configFactory,
    LoggerChannelFactoryInterface $loggerFactory,
  ) {
    $this->logger = $loggerFactory->get('o365');
  }

  /**
   * Updates user roles based on Microsoft 365 group membership.
   *
   * @param \Drupal\user\UserInterface $account
   *   The user account to update.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   When the user account cannot be saved.
   */
  public function handleRoles(UserInterface $account): void {
    try {
      $configuredRoles = $this->getConfiguredRoles();
      $newRoles = $this->calculateUserRoles($configuredRoles);
      $this->updateUserRoles($account, $newRoles);
    }
    catch (TempStoreException | GuzzleException | IdentityProviderException $e) {
      $this->logger->error('Failed to update user roles: @message', [
        '@message' => $e->getMessage(),
      ]);
      throw new EntityStorageException('Unable to update user roles due to external service error.', 0, $e);
    }
  }

  /**
   * Calculates the roles a user should have based on their group membership.
   *
   * @param array<string, string[]> $configuredRoles
   *   The configured role mappings.
   *
   * @return string[]
   *   Array of role IDs the user should have.
   *
   * @throws \Drupal\Core\TempStore\TempStoreException
   * @throws \GuzzleHttp\Exception\GuzzleException
   * @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException
   */
  private function calculateUserRoles(array $configuredRoles): array {
    $userGroups = $this->getUserGroups();
    if (empty($userGroups)) {
      return [];
    }

    // Create lookup sets for efficient searching.
    $groupLookup = $this->createGroupLookup($userGroups);
    $assignedRoles = [];

    foreach ($configuredRoles as $groupIdentifier => $roles) {
      if (isset($groupLookup[$groupIdentifier])) {
        foreach ($roles as $role) {
          $assignedRoles[$role] = TRUE;
        }
      }
    }

    return array_keys($assignedRoles);
  }

  /**
   * Creates a lookup array for efficient group membership checking.
   *
   * @param array<int, array<string, mixed>> $userGroups
   *   The user's Microsoft 365 groups.
   *
   * @return array<string, bool>
   *   Lookup array with group identifiers as keys.
   */
  private function createGroupLookup(array $userGroups): array {
    $lookup = [];
    foreach ($userGroups as $group) {
      // Support multiple identifiers per group.
      $identifiers = [
        $group['id'] ?? '',
        $group['displayName'] ?? '',
        $group['onPremisesSamAccountName'] ?? '',
      ];

      foreach (array_filter($identifiers) as $identifier) {
        $lookup[$identifier] = TRUE;
      }
    }
    return $lookup;
  }

  /**
   * Updates the user's roles by removing unsafe roles and adding new ones.
   *
   * @param \Drupal\user\UserInterface $account
   *   The user account to update.
   * @param string[] $newRoles
   *   Array of role IDs to assign.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  private function updateUserRoles(UserInterface $account, array $newRoles): void {
    $existingRoles = $account->getRoles(TRUE);
    $safeRoles = $this->getSafeRoles();
    $rolesToRemove = array_diff($existingRoles, $safeRoles);
    $rolesToAdd = array_diff($newRoles, $existingRoles);

    if (!empty($this->defaultRole) && !$account->hasRole($this->defaultRole)) {
      $rolesToAdd[] = $this->defaultRole;
    }

    $hasChanges = FALSE;

    // Remove roles that are not safe and not in the new roles list.
    foreach ($rolesToRemove as $role) {
      if (!in_array($role, $newRoles, TRUE)) {
        $account->removeRole($role);
        $hasChanges = TRUE;
      }
    }

    // Add new roles.
    foreach ($rolesToAdd as $role) {
      $account->addRole($role);
      $hasChanges = TRUE;
    }

    if ($hasChanges) {
      $account->save();
      $this->logger->info('Updated roles for user @uid', ['@uid' => $account->id()]);
    }
  }

  /**
   * Gets the configured roles from configuration.
   *
   * @return array<string, string[]>
   *   Array mapping group identifiers to role arrays.
   */
  private function getConfiguredRoles(): array {
    if ($this->configuredRoles !== NULL) {
      return $this->configuredRoles;
    }

    $roleConfig = $this->configFactory->get('o365.role_settings');
    $rolesMap = $roleConfig->get('roles_map');

    // Get the default role if set in the config.
    $defaultRole = $roleConfig->get('default_role');
    if ($defaultRole !== '_none') {
      $this->defaultRole = $defaultRole;
    }

    $this->configuredRoles = [];

    if (!empty($rolesMap)) {
      $mapping = array_filter(
        array_map('trim', explode("\n", trim($rolesMap)))
      );

      foreach ($mapping as $line) {
        $parts = array_map('trim', explode('|', $line));
        if (count($parts) >= 2) {
          $groupIdentifier = array_shift($parts);
          $this->configuredRoles[$groupIdentifier] = array_filter($parts);
        }
      }
    }

    return $this->configuredRoles;
  }

  /**
   * Gets the list of safe roles that should not be removed.
   *
   * @return string[]
   *   Array of safe role IDs.
   */
  private function getSafeRoles(): array {
    if ($this->safeRoles !== NULL) {
      return $this->safeRoles;
    }

    $roleConfig = $this->configFactory->get('o365.role_settings');
    $safeRoles = $roleConfig->get('safe_roles');
    $this->safeRoles = [];

    if (!empty($safeRoles)) {
      $this->safeRoles = array_filter(
        array_map('trim', explode("\n", $safeRoles))
      );
    }

    // Make the default role a safe role so it doesn't get deleted.
    if (!empty($this->defaultRole)) {
      $this->safeRoles[] = $this->defaultRole;
    }

    return $this->safeRoles;
  }

  /**
   * Gets the user's Microsoft 365 groups.
   *
   * @return array<int, array<string, mixed>>
   *   Array of group data from Microsoft Graph API.
   *
   * @throws \Drupal\Core\TempStore\TempStoreException
   * @throws \GuzzleHttp\Exception\GuzzleException
   * @throws \League\OAuth2\Client\Provider\Exception\IdentityProviderException
   */
  private function getUserGroups(): array {
    if ($this->userGroups !== NULL) {
      return $this->userGroups;
    }

    $response = $this->graphService->getGraphData(
      '/me/transitiveMemberOf/microsoft.graph.group?$select=id,displayName,onPremisesSamAccountName'
    );

    $this->userGroups = $response['value'] ?? [];
    return $this->userGroups;
  }

  /**
   * Clears cached data to force fresh retrieval.
   */
  public function clearCache(): void {
    $this->configuredRoles = NULL;
    $this->safeRoles = NULL;
    $this->userGroups = NULL;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc