mirror of
https://gitlab.unicamp.br/infimecc_drupal11_modules/ldap_groups_sync.git
synced 2026-03-11 02:37:41 -03:00
Inicializa módulo base ldap_groups_sync
Cria super-módulo com infraestrutura compartilhada de regras de acesso para os módulos de sincronização LDAP de grupos. - GroupAccessRulesService: serviço parametrizável por config name - AccessRulesFormBase: listagem/remoção de regras (classe abstrata) - AccessRuleFormBase: formulário modal de criação/edição (classe abstrata) - Sub-módulos ldap_departments_sync e ldap_research_groups_sync refatorados para estender as classes base com subclasses mínimas - Traduções pt-br centralizadas em ldap_groups_sync.pt-br.po Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
771
src/Form/AccessRuleFormBase.php
Normal file
771
src/Form/AccessRuleFormBase.php
Normal file
@@ -0,0 +1,771 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\ldap_groups_sync\Form;
|
||||
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\CloseModalDialogCommand;
|
||||
use Drupal\Core\Ajax\RedirectCommand;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Base class for the modal form for creating or editing a single access rule.
|
||||
*
|
||||
* Opened from the main config form via a Drupal dialog (use-ajax link).
|
||||
* Saves directly to the module's settings config and closes the modal on
|
||||
* success, triggering a full page reload of the parent.
|
||||
*
|
||||
* Subclasses must implement getConfigName(), getFormId(), getAccessRulesRoute(),
|
||||
* and getDefaultGroupTypeId().
|
||||
*/
|
||||
abstract class AccessRuleFormBase extends FormBase {
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected $bundleInfo;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Routing\RouteMatchInterface
|
||||
*/
|
||||
protected $routeMatch;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
ConfigFactoryInterface $config_factory,
|
||||
EntityTypeManagerInterface $entity_type_manager,
|
||||
EntityTypeBundleInfoInterface $bundle_info,
|
||||
EntityFieldManagerInterface $entity_field_manager,
|
||||
RouteMatchInterface $route_match
|
||||
) {
|
||||
$this->configFactory = $config_factory;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->bundleInfo = $bundle_info;
|
||||
$this->entityFieldManager = $entity_field_manager;
|
||||
$this->routeMatch = $route_match;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('config.factory'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('entity_type.bundle.info'),
|
||||
$container->get('entity_field.manager'),
|
||||
$container->get('current_route_match')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the config object name for the implementing module.
|
||||
*
|
||||
* @return string
|
||||
* E.g. 'ldap_departments_sync.settings'.
|
||||
*/
|
||||
abstract protected function getConfigName(): string;
|
||||
|
||||
/**
|
||||
* Returns the route name for the access rules listing page.
|
||||
*
|
||||
* @return string
|
||||
* E.g. 'ldap_departments_sync.access_rules'.
|
||||
*/
|
||||
abstract protected function getAccessRulesRoute(): string;
|
||||
|
||||
/**
|
||||
* Returns the default group type ID used when the config has no value set.
|
||||
*
|
||||
* @return string
|
||||
* E.g. 'departments' or 'research_group'.
|
||||
*/
|
||||
abstract protected function getDefaultGroupTypeId(): string;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, $rule_index = 'new') {
|
||||
$form['#prefix'] = '<div id="access-rule-form-wrapper">';
|
||||
$form['#suffix'] = '</div>';
|
||||
|
||||
// Attach dialog library so the close button works.
|
||||
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
|
||||
|
||||
// Resolve the rule index (from route or form_state for AJAX rebuilds).
|
||||
$resolved_index = $form_state->get('rule_index') ?? $rule_index;
|
||||
$form_state->set('rule_index', $resolved_index);
|
||||
|
||||
// Load existing rule data if editing.
|
||||
$rule = $this->loadRule($resolved_index);
|
||||
|
||||
// ---- Resolve current entity_type and bundle (considering AJAX changes) ----
|
||||
$entity_type_id = $form_state->getValue('entity_type') ?? ($rule['entity_type'] ?? '');
|
||||
$bundle = $form_state->getValue('bundle') ?? ($rule['bundle'] ?? '');
|
||||
|
||||
// ---- Basic settings --------------------------------------------------------
|
||||
$form['label'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Label'),
|
||||
'#description' => $this->t('Human-readable description of this rule.'),
|
||||
'#default_value' => $rule['label'] ?? '',
|
||||
'#required' => TRUE,
|
||||
'#maxlength' => 128,
|
||||
];
|
||||
|
||||
$form['enabled'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Enabled'),
|
||||
'#default_value' => $rule['enabled'] ?? TRUE,
|
||||
];
|
||||
|
||||
// ---- Entity type -----------------------------------------------------------
|
||||
$entity_type_options = $this->getContentEntityTypeOptions();
|
||||
|
||||
$form['entity_type'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Entity Type'),
|
||||
'#options' => $entity_type_options,
|
||||
'#empty_option' => $this->t('- Select entity type -'),
|
||||
'#default_value' => $entity_type_id,
|
||||
'#required' => TRUE,
|
||||
'#ajax' => [
|
||||
'callback' => '::updateDependentFields',
|
||||
'wrapper' => 'access-rule-form-wrapper',
|
||||
'effect' => 'fade',
|
||||
],
|
||||
];
|
||||
|
||||
// ---- Bundle ----------------------------------------------------------------
|
||||
$bundle_options = [];
|
||||
if (!empty($entity_type_id)) {
|
||||
$bundle_options = $this->getBundleOptions($entity_type_id);
|
||||
}
|
||||
|
||||
$form['bundle'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Bundle'),
|
||||
'#description' => $this->t('Leave empty to apply to all bundles of the selected entity type.'),
|
||||
'#options' => $bundle_options,
|
||||
'#empty_option' => $this->t('- All bundles -'),
|
||||
'#default_value' => $bundle,
|
||||
'#ajax' => [
|
||||
'callback' => '::updateDependentFields',
|
||||
'wrapper' => 'access-rule-form-wrapper',
|
||||
'effect' => 'fade',
|
||||
],
|
||||
];
|
||||
|
||||
// ---- Operations ------------------------------------------------------------
|
||||
$form['operations'] = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Operations'),
|
||||
'#options' => [
|
||||
'create' => $this->t('Create'),
|
||||
'update' => $this->t('Update'),
|
||||
'delete' => $this->t('Delete'),
|
||||
'view' => $this->t('View'),
|
||||
],
|
||||
'#default_value' => $rule['operations'] ?? ['create'],
|
||||
'#required' => TRUE,
|
||||
];
|
||||
|
||||
// ---- Mode ------------------------------------------------------------------
|
||||
$form['mode'] = [
|
||||
'#type' => 'radios',
|
||||
'#title' => $this->t('Mode'),
|
||||
'#options' => [
|
||||
'restrictive' => $this->t('<strong>Restrictive</strong> — deny access to users who do not match this rule'),
|
||||
'additive' => $this->t('<strong>Additive</strong> — only grant access; never deny other users'),
|
||||
],
|
||||
'#default_value' => $rule['mode'] ?? 'restrictive',
|
||||
];
|
||||
|
||||
// ---- Membership requirements -----------------------------------------------
|
||||
$form['membership'] = [
|
||||
'#type' => 'fieldset',
|
||||
'#title' => $this->t('Membership requirements'),
|
||||
'#description' => $this->t(
|
||||
'A user is authorized if they satisfy <strong>at least one card</strong>: '
|
||||
. 'member of that group <em>and</em> holding at least one of the checked roles. '
|
||||
. 'Each card can specify a different group and different roles.'
|
||||
),
|
||||
];
|
||||
|
||||
$membership_count = $form_state->get('membership_count');
|
||||
|
||||
// Prefer form state values (preserves user edits during AJAX rebuilds).
|
||||
// Normalize from submitted format: checkboxes return [id => id|0], we
|
||||
// need [id, ...] for #default_value.
|
||||
$memberships_from_state = $form_state->getValue('memberships');
|
||||
if ($memberships_from_state !== NULL) {
|
||||
$existing_memberships = [];
|
||||
foreach ($memberships_from_state as $mem) {
|
||||
$existing_memberships[] = [
|
||||
'group_id' => $mem['group_id'] ?? '',
|
||||
'roles' => is_array($mem['roles'] ?? NULL)
|
||||
? array_values(array_filter($mem['roles']))
|
||||
: [],
|
||||
];
|
||||
}
|
||||
}
|
||||
else {
|
||||
$existing_memberships = $rule['memberships'] ?? [];
|
||||
}
|
||||
|
||||
if ($membership_count === NULL) {
|
||||
$membership_count = max(1, count($existing_memberships));
|
||||
$form_state->set('membership_count', $membership_count);
|
||||
}
|
||||
|
||||
$group_options = $this->getGroupOptions();
|
||||
$role_options = $this->getGroupRoleOptions();
|
||||
|
||||
// #tree => TRUE ensures values are nested as memberships[m][group_id],
|
||||
// memberships[m][roles], etc. Without it, all group_id fields would
|
||||
// collide at the top-level form values.
|
||||
$form['membership']['memberships'] = [
|
||||
'#type' => 'container',
|
||||
'#tree' => TRUE,
|
||||
];
|
||||
|
||||
// Render one card (container) per membership entry.
|
||||
for ($m = 0; $m < $membership_count; $m++) {
|
||||
$mem = $existing_memberships[$m] ?? ['group_id' => '', 'roles' => []];
|
||||
|
||||
$form['membership']['memberships'][$m] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => [
|
||||
'class' => ['membership-card'],
|
||||
'style' => 'border:1px solid #ccc;border-radius:4px;padding:12px 16px;margin-bottom:12px;background:#fafafa',
|
||||
],
|
||||
];
|
||||
|
||||
$form['membership']['memberships'][$m]['group_id'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Group'),
|
||||
'#options' => $group_options,
|
||||
'#empty_option' => $this->t('- Select a group -'),
|
||||
'#default_value' => $mem['group_id'] ?? '',
|
||||
'#required' => FALSE,
|
||||
];
|
||||
|
||||
// Checkboxes display the full label — no truncation.
|
||||
$form['membership']['memberships'][$m]['roles'] = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Required roles <em>(user must hold at least one)</em>'),
|
||||
'#options' => $role_options,
|
||||
'#default_value' => $mem['roles'] ?? [],
|
||||
];
|
||||
|
||||
$form['membership']['memberships'][$m]['remove'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Remove this group'),
|
||||
'#name' => 'remove_membership_' . $m,
|
||||
'#submit' => ['::removeOneMembership'],
|
||||
'#ajax' => [
|
||||
'callback' => '::updateDependentFields',
|
||||
'wrapper' => 'access-rule-form-wrapper',
|
||||
'effect' => 'fade',
|
||||
],
|
||||
'#limit_validation_errors' => [['memberships']],
|
||||
'#attributes' => ['class' => ['button', 'button--danger', 'button--small']],
|
||||
];
|
||||
}
|
||||
|
||||
$form['membership']['add_membership'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('+ Add group'),
|
||||
'#submit' => ['::addMembership'],
|
||||
'#ajax' => [
|
||||
'callback' => '::updateDependentFields',
|
||||
'wrapper' => 'access-rule-form-wrapper',
|
||||
'effect' => 'fade',
|
||||
],
|
||||
'#limit_validation_errors' => [['memberships']],
|
||||
'#attributes' => ['class' => ['button']],
|
||||
];
|
||||
|
||||
// ---- Field conditions (update / delete / view only) -----------------------
|
||||
$form['field_conditions_wrapper'] = [
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Field conditions <em>(optional — only evaluated for update/delete/view)</em>'),
|
||||
'#open' => !empty($rule['field_conditions']),
|
||||
'#description' => $this->t('All conditions must be true for the rule to apply to an existing entity. For <em>create</em> operations conditions are ignored.'),
|
||||
];
|
||||
|
||||
$field_options = [];
|
||||
if (!empty($entity_type_id)) {
|
||||
$field_options = $this->getFieldOptions($entity_type_id, $bundle ?: NULL);
|
||||
}
|
||||
|
||||
$cond_count = $form_state->get('condition_count');
|
||||
if ($cond_count === NULL) {
|
||||
$cond_count = max(1, count($rule['field_conditions'] ?? []));
|
||||
$form_state->set('condition_count', $cond_count);
|
||||
}
|
||||
|
||||
$existing_conditions = $rule['field_conditions'] ?? [];
|
||||
|
||||
$form['field_conditions_wrapper']['field_conditions'] = [
|
||||
'#type' => 'table',
|
||||
'#header' => [
|
||||
$this->t('Field'),
|
||||
$this->t('Value'),
|
||||
$this->t('Remove'),
|
||||
],
|
||||
];
|
||||
|
||||
for ($c = 0; $c < $cond_count; $c++) {
|
||||
$cond = $existing_conditions[$c] ?? ['field' => '', 'value' => ''];
|
||||
|
||||
if (!empty($field_options)) {
|
||||
$form['field_conditions_wrapper']['field_conditions'][$c]['field'] = [
|
||||
'#type' => 'select',
|
||||
'#options' => $field_options,
|
||||
'#empty_option' => $this->t('- Select field -'),
|
||||
'#default_value' => $cond['field'],
|
||||
];
|
||||
}
|
||||
else {
|
||||
$form['field_conditions_wrapper']['field_conditions'][$c]['field'] = [
|
||||
'#type' => 'textfield',
|
||||
'#placeholder' => $this->t('field_machine_name'),
|
||||
'#default_value' => $cond['field'],
|
||||
'#size' => 24,
|
||||
];
|
||||
}
|
||||
|
||||
$form['field_conditions_wrapper']['field_conditions'][$c]['value'] = [
|
||||
'#type' => 'textfield',
|
||||
'#placeholder' => $this->t('Expected value'),
|
||||
'#default_value' => $cond['value'],
|
||||
'#size' => 24,
|
||||
];
|
||||
|
||||
$form['field_conditions_wrapper']['field_conditions'][$c]['remove'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => FALSE,
|
||||
];
|
||||
}
|
||||
|
||||
$form['field_conditions_wrapper']['condition_actions'] = [
|
||||
'#type' => 'container',
|
||||
];
|
||||
|
||||
$form['field_conditions_wrapper']['condition_actions']['add_condition'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Add condition'),
|
||||
'#submit' => ['::addCondition'],
|
||||
'#ajax' => [
|
||||
'callback' => '::updateDependentFields',
|
||||
'wrapper' => 'access-rule-form-wrapper',
|
||||
'effect' => 'fade',
|
||||
],
|
||||
'#limit_validation_errors' => [['field_conditions']],
|
||||
];
|
||||
|
||||
$form['field_conditions_wrapper']['condition_actions']['remove_conditions'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Remove selected conditions'),
|
||||
'#submit' => ['::removeSelectedConditions'],
|
||||
'#ajax' => [
|
||||
'callback' => '::updateDependentFields',
|
||||
'wrapper' => 'access-rule-form-wrapper',
|
||||
'effect' => 'fade',
|
||||
],
|
||||
'#limit_validation_errors' => [['field_conditions']],
|
||||
];
|
||||
|
||||
// ---- Submit ----------------------------------------------------------------
|
||||
$form['actions'] = ['#type' => 'actions'];
|
||||
|
||||
$form['actions']['submit'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Save rule'),
|
||||
'#button_type' => 'primary',
|
||||
'#ajax' => [
|
||||
'callback' => '::ajaxSave',
|
||||
'wrapper' => 'access-rule-form-wrapper',
|
||||
],
|
||||
];
|
||||
|
||||
$form['actions']['cancel'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Cancel'),
|
||||
'#submit' => [],
|
||||
'#limit_validation_errors' => [],
|
||||
'#ajax' => [
|
||||
'callback' => '::cancelDialog',
|
||||
],
|
||||
'#attributes' => ['class' => ['button']],
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX callback: replaces the entire form wrapper (entity_type/bundle/field
|
||||
* changes and condition add/remove all reuse this single callback).
|
||||
*/
|
||||
public function updateDependentFields(array &$form, FormStateInterface $form_state) {
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit handler: adds a blank membership card.
|
||||
*
|
||||
* Explicitly normalizes current values and appends an empty entry so the
|
||||
* new card has no pre-filled group or roles.
|
||||
*/
|
||||
public function addMembership(array &$form, FormStateInterface $form_state) {
|
||||
$current = $form_state->getValue('memberships') ?? [];
|
||||
|
||||
// Normalize existing entries (checkboxes submit [id => id|0] format).
|
||||
$normalized = [];
|
||||
foreach ($current as $mem) {
|
||||
$normalized[] = [
|
||||
'group_id' => $mem['group_id'] ?? '',
|
||||
'roles' => is_array($mem['roles'] ?? NULL)
|
||||
? array_values(array_filter($mem['roles']))
|
||||
: [],
|
||||
];
|
||||
}
|
||||
|
||||
// Append a truly empty entry for the new card.
|
||||
$normalized[] = ['group_id' => '', 'roles' => []];
|
||||
|
||||
$form_state->setValue('memberships', $normalized);
|
||||
$form_state->set('membership_count', count($normalized));
|
||||
$form_state->setRebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit handler: removes the single membership card whose button was clicked.
|
||||
*/
|
||||
public function removeOneMembership(array &$form, FormStateInterface $form_state) {
|
||||
$trigger = $form_state->getTriggeringElement();
|
||||
preg_match('/remove_membership_(\d+)/', $trigger['#name'] ?? '', $matches);
|
||||
$index = isset($matches[1]) ? (int) $matches[1] : -1;
|
||||
|
||||
$memberships = $form_state->getValue('memberships') ?? [];
|
||||
|
||||
// Normalize and remove the target entry.
|
||||
$normalized = [];
|
||||
foreach ($memberships as $i => $mem) {
|
||||
if ($i === $index) {
|
||||
continue;
|
||||
}
|
||||
$normalized[] = [
|
||||
'group_id' => $mem['group_id'] ?? '',
|
||||
'roles' => is_array($mem['roles'] ?? NULL)
|
||||
? array_values(array_filter($mem['roles']))
|
||||
: [],
|
||||
];
|
||||
}
|
||||
|
||||
$form_state->setValue('memberships', $normalized);
|
||||
$form_state->set('membership_count', max(1, count($normalized)));
|
||||
$form_state->setRebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit handler: adds a blank condition row.
|
||||
*/
|
||||
public function addCondition(array &$form, FormStateInterface $form_state) {
|
||||
$form_state->set('condition_count', ($form_state->get('condition_count') ?? 1) + 1);
|
||||
$form_state->setRebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit handler: removes checked condition rows.
|
||||
*/
|
||||
public function removeSelectedConditions(array &$form, FormStateInterface $form_state) {
|
||||
$conditions = $form_state->getValue('field_conditions') ?? [];
|
||||
$kept = array_values(array_filter($conditions, fn($c) => empty($c['remove'])));
|
||||
$form_state->setValue('field_conditions', $kept);
|
||||
$form_state->set('condition_count', max(1, count($kept)));
|
||||
$form_state->setRebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateForm(array &$form, FormStateInterface $form_state) {
|
||||
$operations = array_filter($form_state->getValue('operations') ?? []);
|
||||
if (empty($operations)) {
|
||||
$form_state->setErrorByName('operations', $this->t('Select at least one operation.'));
|
||||
}
|
||||
|
||||
$memberships = $form_state->getValue('memberships') ?? [];
|
||||
$valid = array_filter($memberships, fn($m) => !empty($m['group_id']));
|
||||
if (empty($valid)) {
|
||||
$form_state->setErrorByName('memberships', $this->t('Add at least one group membership requirement.'));
|
||||
}
|
||||
|
||||
foreach ($valid as $idx => $mem) {
|
||||
if (empty($mem['roles'])) {
|
||||
$form_state->setErrorByName(
|
||||
"memberships][$idx][roles",
|
||||
$this->t('Select at least one required role for each group.')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX callback: closes the modal and redirects after a successful save.
|
||||
*
|
||||
* The actual save is performed by submitForm(), which Drupal always runs
|
||||
* before the AJAX callback. This callback must NOT call saveRule() again
|
||||
* to avoid saving the rule twice.
|
||||
*/
|
||||
public function ajaxSave(array &$form, FormStateInterface $form_state) {
|
||||
if ($form_state->hasAnyErrors()) {
|
||||
// Redisplay the form with error messages.
|
||||
return $form;
|
||||
}
|
||||
|
||||
$response = new AjaxResponse();
|
||||
$response->addCommand(new CloseModalDialogCommand());
|
||||
$response->addCommand(new RedirectCommand(
|
||||
Url::fromRoute($this->getAccessRulesRoute())->toString()
|
||||
));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX callback: closes the modal without saving.
|
||||
*/
|
||||
public function cancelDialog(array &$form, FormStateInterface $form_state): AjaxResponse {
|
||||
$response = new AjaxResponse();
|
||||
$response->addCommand(new CloseModalDialogCommand());
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
// Runs for both AJAX and non-AJAX submissions.
|
||||
$this->saveRule($form_state);
|
||||
$form_state->setRedirectUrl(Url::fromRoute($this->getAccessRulesRoute()));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Loads a rule from config by index ('new' returns defaults).
|
||||
*/
|
||||
protected function loadRule($rule_index): array {
|
||||
if ($rule_index === 'new') {
|
||||
return [];
|
||||
}
|
||||
$rules = $this->configFactory
|
||||
->get($this->getConfigName())
|
||||
->get('access_rules') ?? [];
|
||||
|
||||
return $rules[(int) $rule_index] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the rule to config.
|
||||
*/
|
||||
protected function saveRule(FormStateInterface $form_state): void {
|
||||
$rule_index = $form_state->get('rule_index') ?? 'new';
|
||||
|
||||
$operations = array_values(array_filter($form_state->getValue('operations') ?? []));
|
||||
|
||||
$conditions_raw = $form_state->getValue('field_conditions') ?? [];
|
||||
$conditions = [];
|
||||
foreach ($conditions_raw as $cond) {
|
||||
if (!empty($cond['remove']) || empty($cond['field'])) {
|
||||
continue;
|
||||
}
|
||||
$conditions[] = [
|
||||
'field' => $cond['field'],
|
||||
'value' => $cond['value'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
$memberships_raw = $form_state->getValue('memberships') ?? [];
|
||||
$memberships = [];
|
||||
foreach ($memberships_raw as $mem) {
|
||||
if (empty($mem['group_id'])) {
|
||||
continue;
|
||||
}
|
||||
// Checkboxes return [role_id => role_id] for checked, [role_id => 0] for unchecked.
|
||||
$roles = array_values(array_filter((array) ($mem['roles'] ?? [])));
|
||||
$memberships[] = [
|
||||
'group_id' => (int) $mem['group_id'],
|
||||
'roles' => $roles,
|
||||
];
|
||||
}
|
||||
|
||||
$new_rule = [
|
||||
'label' => $form_state->getValue('label'),
|
||||
'entity_type' => $form_state->getValue('entity_type'),
|
||||
'bundle' => $form_state->getValue('bundle') ?? '',
|
||||
'operations' => $operations,
|
||||
'mode' => $form_state->getValue('mode') ?? 'restrictive',
|
||||
'memberships' => $memberships,
|
||||
'field_conditions' => $conditions,
|
||||
'enabled' => (bool) $form_state->getValue('enabled'),
|
||||
];
|
||||
|
||||
$config = $this->configFactory->getEditable($this->getConfigName());
|
||||
$rules = $config->get('access_rules') ?? [];
|
||||
|
||||
if ($rule_index === 'new') {
|
||||
$rules[] = $new_rule;
|
||||
}
|
||||
else {
|
||||
$rules[(int) $rule_index] = $new_rule;
|
||||
}
|
||||
|
||||
$config->set('access_rules', array_values($rules))->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns content entity type options suitable for a select element.
|
||||
*/
|
||||
protected function getContentEntityTypeOptions(): array {
|
||||
$options = [];
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $id => $definition) {
|
||||
if (!($definition instanceof \Drupal\Core\Entity\ContentEntityTypeInterface)) {
|
||||
continue;
|
||||
}
|
||||
// Skip internal group-related and config entity types.
|
||||
if (in_array($id, ['group_content', 'group_relationship'], TRUE)) {
|
||||
continue;
|
||||
}
|
||||
$options[$id] = (string) $definition->getLabel();
|
||||
}
|
||||
asort($options);
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bundle options for a given entity type.
|
||||
*/
|
||||
protected function getBundleOptions(string $entity_type_id): array {
|
||||
$options = [];
|
||||
try {
|
||||
foreach ($this->bundleInfo->getBundleInfo($entity_type_id) as $bundle_id => $info) {
|
||||
$options[$bundle_id] = (string) ($info['label'] ?? $bundle_id);
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// Return empty if the entity type has no bundles.
|
||||
}
|
||||
asort($options);
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns field options for the given entity type and (optional) bundle.
|
||||
*/
|
||||
protected function getFieldOptions(string $entity_type_id, ?string $bundle): array {
|
||||
$options = [];
|
||||
try {
|
||||
$bundle = $bundle ?: $entity_type_id;
|
||||
$definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle);
|
||||
$skip = ['uuid', 'langcode', 'default_langcode', 'revision_translation_affected'];
|
||||
foreach ($definitions as $name => $def) {
|
||||
if (in_array($name, $skip, TRUE)) {
|
||||
continue;
|
||||
}
|
||||
$options[$name] = (string) $def->getLabel() . ' (' . $name . ')';
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// Return empty on error.
|
||||
}
|
||||
asort($options);
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns options for all groups of the configured group type.
|
||||
*/
|
||||
protected function getGroupOptions(): array {
|
||||
$options = [];
|
||||
try {
|
||||
$group_type_id = $this->configFactory
|
||||
->get($this->getConfigName())
|
||||
->get('group_type_id') ?: $this->getDefaultGroupTypeId();
|
||||
|
||||
$groups = $this->entityTypeManager
|
||||
->getStorage('group')
|
||||
->loadByProperties(['type' => $group_type_id]);
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$options[$group->id()] = $group->label();
|
||||
}
|
||||
asort($options);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// Return empty on error.
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns options for all roles of the configured group type.
|
||||
*/
|
||||
protected function getGroupRoleOptions(): array {
|
||||
$options = [];
|
||||
try {
|
||||
$group_type_id = $this->configFactory
|
||||
->get($this->getConfigName())
|
||||
->get('group_type_id') ?: $this->getDefaultGroupTypeId();
|
||||
|
||||
$roles = $this->entityTypeManager
|
||||
->getStorage('group_role')
|
||||
->loadByProperties(['group_type' => $group_type_id]);
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$scope_labels = [
|
||||
'individual' => $this->t('Individual'),
|
||||
'insider' => $this->t('Insider'),
|
||||
'outsider' => $this->t('Outsider'),
|
||||
];
|
||||
$scope = $role->getScope();
|
||||
$suffix = isset($scope_labels[$scope]) ? ' [' . $scope_labels[$scope] . ']' : '';
|
||||
$options[$role->id()] = $role->label() . $suffix;
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// Return empty on error.
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
}
|
||||
221
src/Form/AccessRulesFormBase.php
Normal file
221
src/Form/AccessRulesFormBase.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\ldap_groups_sync\Form;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Config\TypedConfigManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\ConfigFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Base class for listing and managing access rules in LDAP group sync modules.
|
||||
*
|
||||
* Individual rules are created and edited via a modal (AccessRuleFormBase).
|
||||
* This page shows a summary table and allows bulk removal.
|
||||
*
|
||||
* Subclasses must implement getConfigName(), getFormId(), and
|
||||
* getAccessRuleFormRoute().
|
||||
*/
|
||||
abstract class AccessRulesFormBase extends ConfigFormBase {
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
ConfigFactoryInterface $config_factory,
|
||||
TypedConfigManagerInterface $typed_config_manager,
|
||||
EntityTypeManagerInterface $entity_type_manager
|
||||
) {
|
||||
parent::__construct($config_factory, $typed_config_manager);
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('config.factory'),
|
||||
$container->get('config.typed'),
|
||||
$container->get('entity_type.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the config object name for the implementing module.
|
||||
*
|
||||
* @return string
|
||||
* E.g. 'ldap_departments_sync.settings'.
|
||||
*/
|
||||
abstract protected function getConfigName(): string;
|
||||
|
||||
/**
|
||||
* Returns the route name for the single-rule edit/create form.
|
||||
*
|
||||
* @return string
|
||||
* E.g. 'ldap_departments_sync.access_rule_form'.
|
||||
*/
|
||||
abstract protected function getAccessRuleFormRoute(): string;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditableConfigNames() {
|
||||
return [$this->getConfigName()];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$form['#attached']['library'][] = 'core/drupal.dialog.ajax';
|
||||
|
||||
$form['description'] = [
|
||||
'#type' => 'markup',
|
||||
'#markup' => '<p>' . $this->t(
|
||||
'Define rules that restrict or grant entity operations based on group membership.<br>'
|
||||
. '<strong>Restrictive</strong>: if a matching rule exists and the user does not satisfy it, access is denied.<br>'
|
||||
. '<strong>Additive</strong>: the rule only grants extra access; other users keep their default permissions.<br>'
|
||||
. '<em>Field conditions</em> are only evaluated for update/delete/view (entity already exists).'
|
||||
) . '</p>',
|
||||
];
|
||||
|
||||
$existing_rules = $this->config($this->getConfigName())->get('access_rules') ?? [];
|
||||
|
||||
$form['summary'] = [
|
||||
'#type' => 'table',
|
||||
'#header' => [
|
||||
$this->t('Label'),
|
||||
$this->t('Entity type / Bundle'),
|
||||
$this->t('Operations'),
|
||||
$this->t('Groups / Roles'),
|
||||
$this->t('Mode'),
|
||||
$this->t('Status'),
|
||||
$this->t('Edit'),
|
||||
$this->t('Remove'),
|
||||
],
|
||||
'#empty' => $this->t('No access rules defined. Click "Add Rule" to create the first one.'),
|
||||
'#attributes' => ['class' => ['access-rules-summary-table']],
|
||||
];
|
||||
|
||||
foreach ($existing_rules as $i => $rule) {
|
||||
$bundle_part = !empty($rule['bundle'])
|
||||
? ' / <em>' . $rule['bundle'] . '</em>'
|
||||
: ' / <em>' . $this->t('all bundles') . '</em>';
|
||||
|
||||
$form['summary'][$i]['label'] = [
|
||||
'#markup' => '<strong>' . ($rule['label'] ?: $this->t('(no label)')) . '</strong>',
|
||||
];
|
||||
|
||||
$form['summary'][$i]['entity_bundle'] = [
|
||||
'#markup' => ($rule['entity_type'] ?? '?') . $bundle_part,
|
||||
];
|
||||
|
||||
$form['summary'][$i]['operations'] = [
|
||||
'#markup' => implode(', ', $rule['operations'] ?? []),
|
||||
];
|
||||
|
||||
$membership_lines = [];
|
||||
foreach ($rule['memberships'] ?? [] as $mem) {
|
||||
try {
|
||||
$group = $this->entityTypeManager->getStorage('group')->load($mem['group_id'] ?? 0);
|
||||
$group_label = $group ? $group->label() : '#' . ($mem['group_id'] ?? '?');
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$group_label = '#' . ($mem['group_id'] ?? '?');
|
||||
}
|
||||
$roles = implode(', ', $mem['roles'] ?? []);
|
||||
$membership_lines[] = $group_label . ($roles ? ' <em>(' . $roles . ')</em>' : '');
|
||||
}
|
||||
$form['summary'][$i]['memberships_summary'] = [
|
||||
'#markup' => $membership_lines
|
||||
? implode('<br>', $membership_lines)
|
||||
: '<em>' . $this->t('none') . '</em>',
|
||||
];
|
||||
|
||||
$form['summary'][$i]['mode'] = [
|
||||
'#markup' => ($rule['mode'] ?? 'restrictive') === 'additive'
|
||||
? $this->t('Additive')
|
||||
: $this->t('Restrictive'),
|
||||
];
|
||||
|
||||
$form['summary'][$i]['enabled'] = [
|
||||
'#markup' => ($rule['enabled'] ?? TRUE)
|
||||
? '<span style="color:green">✓ ' . $this->t('On') . '</span>'
|
||||
: '<span style="color:gray">✗ ' . $this->t('Off') . '</span>',
|
||||
];
|
||||
|
||||
$form['summary'][$i]['edit'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Edit'),
|
||||
'#url' => Url::fromRoute($this->getAccessRuleFormRoute(), ['rule_index' => $i]),
|
||||
'#attributes' => [
|
||||
'class' => ['use-ajax', 'button', 'button--small'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => json_encode([
|
||||
'width' => 860,
|
||||
'title' => $this->t('Edit Access Rule'),
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
$form['summary'][$i]['remove'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => FALSE,
|
||||
];
|
||||
}
|
||||
|
||||
$form['actions'] = ['#type' => 'actions'];
|
||||
|
||||
$form['actions']['add_rule'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Add Rule'),
|
||||
'#url' => Url::fromRoute($this->getAccessRuleFormRoute(), ['rule_index' => 'new']),
|
||||
'#attributes' => [
|
||||
'class' => ['use-ajax', 'button', 'button--primary'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => json_encode([
|
||||
'width' => 860,
|
||||
'title' => $this->t('New Access Rule'),
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
$form['actions']['remove_selected'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Remove Selected'),
|
||||
'#limit_validation_errors' => [['summary']],
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$summary_values = $form_state->getValue('summary') ?? [];
|
||||
$config = $this->configFactory()->getEditable($this->getConfigName());
|
||||
$existing_rules = $config->get('access_rules') ?? [];
|
||||
|
||||
$kept = [];
|
||||
foreach ($existing_rules as $i => $rule) {
|
||||
if (empty($summary_values[$i]['remove'])) {
|
||||
$kept[] = $rule;
|
||||
}
|
||||
}
|
||||
|
||||
$config->set('access_rules', array_values($kept))->save();
|
||||
|
||||
$this->messenger()->addStatus($this->t('Access rules updated.'));
|
||||
}
|
||||
|
||||
}
|
||||
276
src/GroupAccessRulesService.php
Normal file
276
src/GroupAccessRulesService.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\ldap_groups_sync;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Service to check entity access based on configured group membership rules.
|
||||
*
|
||||
* Each rule defines:
|
||||
* - Which entity type + bundle it applies to
|
||||
* - Which operations it controls (create, update, delete, view)
|
||||
* - Which group members (identified by group field value) with which roles
|
||||
* are allowed to perform those operations
|
||||
* - Whether the rule is restrictive (deny others) or additive (only grant)
|
||||
* - Optional field conditions on the entity (for update/delete/view)
|
||||
*/
|
||||
class GroupAccessRulesService {
|
||||
|
||||
/**
|
||||
* Config factory.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* Entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The config name for the module using this service.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $configName;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
ConfigFactoryInterface $config_factory,
|
||||
EntityTypeManagerInterface $entity_type_manager,
|
||||
string $configName
|
||||
) {
|
||||
$this->configFactory = $config_factory;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->configName = $configName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access for an operation on an existing entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being accessed.
|
||||
* @param string $operation
|
||||
* The operation: view, update, delete.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The user account.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResult
|
||||
* The access result.
|
||||
*/
|
||||
public function checkAccess(EntityInterface $entity, string $operation, AccountInterface $account): AccessResult {
|
||||
$rules = $this->getRulesForOperation(
|
||||
$entity->getEntityTypeId(),
|
||||
$entity->bundle(),
|
||||
$operation
|
||||
);
|
||||
|
||||
if (empty($rules)) {
|
||||
return AccessResult::neutral();
|
||||
}
|
||||
|
||||
$cache = (new CacheableMetadata())
|
||||
->addCacheContexts(['user'])
|
||||
->addCacheableDependency($entity)
|
||||
->addCacheTags(['config:' . $this->configName]);
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
// For update/delete/view, check optional field conditions on the entity.
|
||||
if (!$this->entityMatchesConditions($entity, $rule['field_conditions'] ?? [])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->userMatchesRule($account, $rule)) {
|
||||
return AccessResult::allowed()->addCacheableDependency($cache);
|
||||
}
|
||||
|
||||
if (($rule['mode'] ?? 'restrictive') === 'restrictive') {
|
||||
return AccessResult::forbidden()->addCacheableDependency($cache);
|
||||
}
|
||||
}
|
||||
|
||||
return AccessResult::neutral()->addCacheableDependency($cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check create access for an entity type and bundle.
|
||||
*
|
||||
* Field conditions are not evaluated here because the entity does not yet
|
||||
* exist at creation time.
|
||||
*
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The user account.
|
||||
* @param string $entity_type
|
||||
* The entity type ID.
|
||||
* @param string $bundle
|
||||
* The bundle.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResult
|
||||
* The access result.
|
||||
*/
|
||||
public function checkCreateAccess(AccountInterface $account, string $entity_type, string $bundle): AccessResult {
|
||||
$rules = $this->getRulesForOperation($entity_type, $bundle, 'create');
|
||||
|
||||
if (empty($rules)) {
|
||||
return AccessResult::neutral();
|
||||
}
|
||||
|
||||
$cache = (new CacheableMetadata())
|
||||
->addCacheContexts(['user'])
|
||||
->addCacheTags(['config:' . $this->configName]);
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
if ($this->userMatchesRule($account, $rule)) {
|
||||
return AccessResult::allowed()->addCacheableDependency($cache);
|
||||
}
|
||||
|
||||
if (($rule['mode'] ?? 'restrictive') === 'restrictive') {
|
||||
return AccessResult::forbidden()->addCacheableDependency($cache);
|
||||
}
|
||||
}
|
||||
|
||||
return AccessResult::neutral()->addCacheableDependency($cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns enabled rules that match the given entity type, bundle, and operation.
|
||||
*
|
||||
* @param string $entity_type
|
||||
* Entity type ID.
|
||||
* @param string $bundle
|
||||
* Bundle machine name.
|
||||
* @param string $operation
|
||||
* Operation name (create, update, delete, view).
|
||||
*
|
||||
* @return array
|
||||
* Matching rules.
|
||||
*/
|
||||
protected function getRulesForOperation(string $entity_type, string $bundle, string $operation): array {
|
||||
$all_rules = $this->configFactory
|
||||
->get($this->configName)
|
||||
->get('access_rules') ?? [];
|
||||
|
||||
return array_values(array_filter($all_rules, function (array $rule) use ($entity_type, $bundle, $operation) {
|
||||
if (!($rule['enabled'] ?? TRUE)) {
|
||||
return FALSE;
|
||||
}
|
||||
if (($rule['entity_type'] ?? '') !== $entity_type) {
|
||||
return FALSE;
|
||||
}
|
||||
// An empty bundle in the rule means "all bundles of this entity type".
|
||||
if (!empty($rule['bundle']) && $rule['bundle'] !== $bundle) {
|
||||
return FALSE;
|
||||
}
|
||||
if (!in_array($operation, $rule['operations'] ?? [], TRUE)) {
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the entity satisfies all field conditions of a rule.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to check.
|
||||
* @param array $conditions
|
||||
* Array of conditions, each with 'field' and 'value' keys.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if all conditions are met or there are no conditions.
|
||||
*/
|
||||
protected function entityMatchesConditions(EntityInterface $entity, array $conditions): bool {
|
||||
foreach ($conditions as $condition) {
|
||||
$field_name = $condition['field'] ?? '';
|
||||
$expected = (string) ($condition['value'] ?? '');
|
||||
|
||||
if (empty($field_name) || !$entity->hasField($field_name)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$field = $entity->get($field_name);
|
||||
// Support simple value fields and entity reference fields.
|
||||
$actual = (string) ($field->value ?? $field->target_id ?? '');
|
||||
|
||||
if ($actual !== $expected) {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the account satisfies the membership requirements of a rule.
|
||||
*
|
||||
* Each rule contains a `memberships` array where every entry pairs a group ID
|
||||
* with its own set of required roles. A user is authorized if they match
|
||||
* at least one entry: member of that group AND holding at least one of the
|
||||
* entry's roles.
|
||||
*
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The account to check.
|
||||
* @param array $rule
|
||||
* The access rule configuration.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the user satisfies at least one membership+role requirement.
|
||||
*/
|
||||
protected function userMatchesRule(AccountInterface $account, array $rule): bool {
|
||||
if (!$account->isAuthenticated()) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$memberships = $rule['memberships'] ?? [];
|
||||
if (empty($memberships)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
try {
|
||||
$storage = $this->entityTypeManager->getStorage('group');
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
foreach ($memberships as $entry) {
|
||||
$group_id = (int) ($entry['group_id'] ?? 0);
|
||||
$required_roles = $entry['roles'] ?? [];
|
||||
|
||||
if (!$group_id || empty($required_roles)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$group = $storage->load($group_id);
|
||||
if (!$group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$membership = $group->getMember($account);
|
||||
if (!$membership) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$member_role_ids = array_keys($membership->getRoles());
|
||||
foreach ($required_roles as $required_role) {
|
||||
if (in_array($required_role, $member_role_ids, TRUE)) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user