diff --git a/docker-compose.yml b/docker-compose.yml index 042c251..9176d35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,6 @@ services: selenium: image: selenium/standalone-chrome:4.1.3-20220405 environment: - - DISPLAY=:99 - SCREEN_WIDTH=1280 - SCREEN_HEIGHT=800 - VNC_NO_PASSWORD=1 diff --git a/modules/oe_authentication_corporate_roles/config/install/field.field.user.user.oe_corporate_roles_mappings.yml b/modules/oe_authentication_corporate_roles/config/install/field.field.user.user.oe_corporate_roles_mappings.yml new file mode 100644 index 0000000..9df3afa --- /dev/null +++ b/modules/oe_authentication_corporate_roles/config/install/field.field.user.user.oe_corporate_roles_mappings.yml @@ -0,0 +1,23 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.user.oe_corporate_roles_mappings + module: + - user +id: user.user.oe_corporate_roles_mappings +field_name: oe_corporate_roles_mappings +entity_type: user +bundle: user +label: 'Corporate roles mappings' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + handler: 'default:corporate_roles_mapping' + handler_settings: + target_bundles: null + auto_create: false +field_type: entity_reference diff --git a/modules/oe_authentication_corporate_roles/config/install/field.storage.user.oe_corporate_roles_mappings.yml b/modules/oe_authentication_corporate_roles/config/install/field.storage.user.oe_corporate_roles_mappings.yml new file mode 100644 index 0000000..b96103c --- /dev/null +++ b/modules/oe_authentication_corporate_roles/config/install/field.storage.user.oe_corporate_roles_mappings.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - oe_authentication_corporate_roles + - user +id: user.oe_corporate_roles_mappings +field_name: oe_corporate_roles_mappings +entity_type: user +type: entity_reference +settings: + target_type: corporate_roles_mapping +module: core +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/modules/oe_authentication_corporate_roles/config/schema/oe_authentication_corporate_roles.schema.yml b/modules/oe_authentication_corporate_roles/config/schema/oe_authentication_corporate_roles.schema.yml new file mode 100644 index 0000000..3c22d8f --- /dev/null +++ b/modules/oe_authentication_corporate_roles/config/schema/oe_authentication_corporate_roles.schema.yml @@ -0,0 +1,20 @@ +oe_authentication_corporate_roles.corporate_roles_mapping.*: + type: config_entity + label: Corporate roles mapping + mapping: + id: + type: string + label: ID + label: + type: label + label: Label + uuid: + type: string + matching_value_type: + type: string + value: + type: string + roles: + type: sequence + sequence: + type: string diff --git a/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.info.yml b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.info.yml index 0752791..f67cfb0 100644 --- a/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.info.yml +++ b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.info.yml @@ -6,3 +6,5 @@ core_version_requirement: ^10 dependencies: - cas:cas - oe_authentication:oe_authentication + - oe_authentication:oe_authentication_user_fields + - drupal:field diff --git a/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.links.action.yml b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.links.action.yml new file mode 100644 index 0000000..5d93848 --- /dev/null +++ b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.links.action.yml @@ -0,0 +1,5 @@ +entity.corporate_roles_mapping.add_form: + route_name: 'entity.corporate_roles_mapping.add_form' + title: 'Add corporate roles mapping' + appears_on: + - entity.corporate_roles_mapping.collection diff --git a/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.links.menu.yml b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.links.menu.yml new file mode 100644 index 0000000..29101fc --- /dev/null +++ b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.links.menu.yml @@ -0,0 +1,5 @@ +entity.corporate_roles_mapping.overview: + title: Corporate role mappings + parent: user.admin_index + description: 'List of corporate roles mappings.' + route_name: entity.corporate_roles_mapping.collection diff --git a/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.links.task.yml b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.links.task.yml new file mode 100644 index 0000000..1b6c7e6 --- /dev/null +++ b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.links.task.yml @@ -0,0 +1,5 @@ +entity.corporate_roles_mapping.collection: + title: 'Corporate roles mappings' + route_name: entity.corporate_roles_mapping.collection + base_route: entity.user.collection + weight: 10 diff --git a/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.module b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.module index c44725e..f5af0d0 100644 --- a/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.module +++ b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.module @@ -7,6 +7,9 @@ declare(strict_types=1); +use Drupal\Core\Form\FormStateInterface; +use Drupal\oe_authentication_corporate_roles\Entity\CorporateRolesMapping; +use Drupal\user\Entity\Role; use Drupal\user\UserInterface; /** @@ -32,8 +35,172 @@ function oe_authentication_corporate_roles_user_presave(UserInterface $user) { $original_roles = $original->getRoles(TRUE); $new_roles = $user->getRoles(TRUE); - if ($original_roles !== $new_roles) { - $user->set('oe_manual_roles', $user->getRoles(TRUE)); + if ($original_roles === $new_roles) { + // If the roles are the same, we bail out. + return; + } + + // Determine if there are any roles that have been assigned automatically to + // the user, and if there are, do not include them in the ones that are being + // set as manual. + $automatic_roles = []; + foreach ($user->get('oe_corporate_roles_mappings')->referencedEntities() as $mapping) { + $automatic_roles = array_merge($automatic_roles, $mapping->get('roles')); + } + $automatic_roles = array_unique(array_values($automatic_roles)); + $new_roles = array_filter($new_roles, function ($role) use ($automatic_roles) { + return !in_array($role, $automatic_roles); + }); + + $user->set('oe_manual_roles', $new_roles); +} + +/** + * Implements hook_ENTITY_TYPE_insert(). + * + * When a new mapping is created, we need to find all the users that match it + * and update their roles to whatever it was mapped. We also store a reference + * to the mapping on the user entity. + */ +function oe_authentication_corporate_roles_corporate_roles_mapping_insert(CorporateRolesMapping $mapping) { + $users = \Drupal::service('oe_authentication_corporate_roles.mapping_lookup')->getMatchingUsers($mapping); + + if (!$users) { + \Drupal::messenger()->addStatus(t('No users were found matching these conditions.')); + + return; + } + + foreach ($users as $user) { + $mapping->updateUserRoles($user); + $user->automatic_corporate_roles = TRUE; + $user->save(); + } + + \Drupal::messenger()->addStatus(t('@count users have had their roles updated.', ['@count' => count($users)])); +} + +/** + * Implements hook_ENTITY_TYPE_update(). + * + * When a mapping is updated, we need to do multiple steps: + * - we need to load all the users that reference the mapping and remove their + * roles. This is because the mapping may have changed and the conditions don't + * apply anymore. + * - we need to load all the users that match the conditions and set the roles. + * The same as we do in insert hook. + */ +function oe_authentication_corporate_roles_corporate_roles_mapping_update(CorporateRolesMapping $mapping) { + // Find all the users that are already mapped and clear their roles. + $users = \Drupal::service('oe_authentication_corporate_roles.mapping_lookup')->getUsersWithMapping($mapping); + // We need to use the original to know what roles it had before it was + // changed. + $original = $mapping->original; + foreach ($users as $user) { + // Remove the mapping roles. + $original->removeMappingRoles($user); + // We also need to remove the reference to the current mapping. + $original->removeMappingReference($user); + } + + // Now, find the users that match. + $matched_users = \Drupal::service('oe_authentication_corporate_roles.mapping_lookup')->getMatchingUsers($mapping); + + // Save all the users from the previous array that are not found in this + // list of matched users. This is because for those, the conditions no longer + // match and they need to be saved as-is, after having their roles cleared. + foreach ($users as $user) { + if (!isset($matched_users[$user->id()])) { + $user->automatic_corporate_roles = TRUE; + $user->save(); + } + } + + foreach ($matched_users as $user) { + $mapping->updateUserRoles($user); + $user->automatic_corporate_roles = TRUE; + $user->save(); + } + + \Drupal::messenger()->addStatus(t('@count users have had their roles updated.', ['@count' => count($matched_users)])); +} + +/** + * Implements hook_ENTITY_TYPE_delete(). + */ +function oe_authentication_corporate_roles_corporate_roles_mapping_delete(CorporateRolesMapping $mapping) { + // When we delete a mapping, ensure we load all the users that are mapped + // and clear their roles. + $ids = \Drupal::entityTypeManager()->getStorage('user')->getQuery() + ->condition('oe_corporate_roles_mappings', $mapping->id()) + ->accessCheck(FALSE) + ->execute(); + + if (!$ids) { + \Drupal::messenger()->addStatus(t('No users were updated for with the deletion of this mapping.')); } + $users = \Drupal::entityTypeManager()->getStorage('user')->loadMultiple($ids); + + foreach ($users as $user) { + // Remove the mapping roles. + $mapping->removeMappingRoles($user); + // Remove the reference to the current mapping. + $mapping->removeMappingReference($user); + $user->automatic_corporate_roles = TRUE; + $user->save(); + } + + \Drupal::messenger()->addStatus(t('@count users have had their roles updated.', ['@count' => count($users)])); +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function oe_authentication_corporate_roles_form_user_form_alter(array &$form, FormStateInterface $form_state) { + if (!isset($form['account']['roles']['#access'])) { + return; + } + + if (!$form['account']['roles']['#access']) { + return; + } + + /** @var \Drupal\user\UserInterface $user */ + $user = $form_state->getBuildInfo()['callback_object']->getEntity(); + + $automatic_roles = []; + $mapping_links = []; + foreach ($user->get('oe_corporate_roles_mappings')->referencedEntities() as $mapping) { + $automatic_roles = array_merge($automatic_roles, $mapping->get('roles')); + $mapping_link = $mapping->toLink(rel: 'edit-form')->toRenderable(); + $mapping_links[] = $mapping_link; + } + $automatic_roles = array_unique(array_values($automatic_roles)); + if (!$automatic_roles) { + return; + } + + $roles = Role::loadMultiple($automatic_roles); + $labels = []; + foreach ($roles as $role) { + $labels[] = $role->label(); + } + + $mappings_list = [ + '#theme' => 'item_list', + '#items' => $mapping_links, + ]; + + $roles_list = [ + '#theme' => 'item_list', + '#items' => $labels, + ]; + + $message = t('
The following roles:
@roleshave been assigned to the user automatically via the Corporate role mapping(s):
@mappingsIf you manually remove those roles, they will be added back the next time the user logs in.
', [ + '@roles' => \Drupal::service('renderer')->renderRoot($roles_list), + '@mappings' => \Drupal::service('renderer')->renderRoot($mappings_list), + ]); + + \Drupal::messenger()->addWarning($message); } diff --git a/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.permissions.yml b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.permissions.yml new file mode 100644 index 0000000..2d602d4 --- /dev/null +++ b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.permissions.yml @@ -0,0 +1,2 @@ +manage corporate roles: + title: 'Administer corporate roles mapping' diff --git a/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.routing.yml b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.routing.yml new file mode 100644 index 0000000..d67fd84 --- /dev/null +++ b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.routing.yml @@ -0,0 +1,31 @@ +entity.corporate_roles_mapping.collection: + path: '/admin/people/corporate-roles-mapping' + defaults: + _entity_list: 'corporate_roles_mapping' + _title: 'Corporate roles mapping configuration' + requirements: + _permission: 'manage corporate roles' + +entity.corporate_roles_mapping.add_form: + path: '/admin/people/corporate_roles_mapping/add' + defaults: + _entity_form: 'corporate_roles_mapping.add' + _title: 'Add a corporate roles mapping' + requirements: + _permission: 'manage corporate roles' + +entity.corporate_roles_mapping.edit_form: + path: '/admin/people/corporate-roles-mapping/{corporate_roles_mapping}' + defaults: + _entity_form: 'corporate_roles_mapping.edit' + _title: 'Edit a corporate roles mapping' + requirements: + _permission: 'manage corporate roles' + +entity.corporate_roles_mapping.delete_form: + path: '/admin/people/corporate-roles-mapping/{corporate_roles_mapping}/delete' + defaults: + _entity_form: 'corporate_roles_mapping.delete' + _title: 'Delete a corporate roles mapping' + requirements: + _permission: 'manage corporate roles' diff --git a/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.services.yml b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.services.yml new file mode 100644 index 0000000..41e10b3 --- /dev/null +++ b/modules/oe_authentication_corporate_roles/oe_authentication_corporate_roles.services.yml @@ -0,0 +1,9 @@ +services: + oe_authentication_corporate_roles.mapping_lookup: + class: Drupal\oe_authentication_corporate_roles\CorporateRolesMappingLookup + arguments: ['@entity_type.manager'] + oe_authentication_corporate_roles.event_subscriber: + class: Drupal\oe_authentication_corporate_roles\EventSubscriber\EuLoginSubscriber + arguments: ['@oe_authentication_corporate_roles.mapping_lookup'] + tags: + - { name: event_subscriber } diff --git a/modules/oe_authentication_corporate_roles/src/CorporateRolesMappingInterface.php b/modules/oe_authentication_corporate_roles/src/CorporateRolesMappingInterface.php new file mode 100644 index 0000000..427589b --- /dev/null +++ b/modules/oe_authentication_corporate_roles/src/CorporateRolesMappingInterface.php @@ -0,0 +1,14 @@ +t('Label'); + $header['id'] = $this->t('Machine name'); + $header['matching_value_type'] = $this->t('Matching type'); + $header['value'] = $this->t('Value'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity): array { + /** @var \Drupal\oe_authentication_corporate_roles\CorporateRolesMappingInterface $entity */ + $row['label'] = $entity->label(); + $row['id'] = $entity->id(); + $row['matching_value_type'] = $entity->get('matching_value_type'); + $row['value'] = $entity->get('value'); + return $row + parent::buildRow($entity); + } + +} diff --git a/modules/oe_authentication_corporate_roles/src/CorporateRolesMappingLookup.php b/modules/oe_authentication_corporate_roles/src/CorporateRolesMappingLookup.php new file mode 100644 index 0000000..e72c549 --- /dev/null +++ b/modules/oe_authentication_corporate_roles/src/CorporateRolesMappingLookup.php @@ -0,0 +1,162 @@ +entityTypeManager = $entityTypeManager; + } + + /** + * Gets the matching users of a given mapping. + * + * @param \Drupal\oe_authentication_corporate_roles\Entity\CorporateRolesMapping $mapping + * The mapping. + * + * @return \Drupal\user\UserInterface[] + * The users. + */ + public function getMatchingUsers(CorporateRolesMapping $mapping): array { + $matching_type = $mapping->get('matching_value_type'); + $value = $mapping->get('value'); + + $query = $this->entityTypeManager->getStorage('user')->getQuery() + // This only works for users from the EC. + ->condition('field_oe_organisation', 'eu.europa.ec') + ->accessCheck(FALSE); + + if ($matching_type === CorporateRolesMapping::LDAP_GROUP) { + // For LDAP groups, we can just query using string comparison. + $query->condition('field_oe_ldap_groups', $value); + } + else { + // Otherwise, for departments, we need to use STARTS_WITH because we need + // to include the broader department/unit/sector. + $query->condition('field_oe_department', $value, 'STARTS_WITH'); + } + + $ids = $query->execute(); + if ($ids) { + return $this->entityTypeManager->getStorage('user')->loadMultiple($ids); + } + + return []; + } + + /** + * Gets the users with a given mapping. + * + * @param \Drupal\oe_authentication_corporate_roles\Entity\CorporateRolesMapping $mapping + * The mapping. + * + * @return \Drupal\user\UserInterface[] + * The users. + */ + public function getUsersWithMapping(CorporateRolesMapping $mapping): array { + $ids = $this->entityTypeManager->getStorage('user')->getQuery() + ->condition('oe_corporate_roles_mappings', $mapping->id()) + ->accessCheck(FALSE) + ->execute(); + + if (!$ids) { + return []; + } + + return $this->entityTypeManager->getStorage('user')->loadMultiple($ids); + } + + /** + * Locates potential mappings for a user. + * + * We search both by the LDAP group and by department and find all mappings + * that match. + * + * @param \Drupal\user\UserInterface $user + * The user. + * + * @return \Drupal\oe_authentication_corporate_roles\Entity\CorporateRolesMapping[] + * The mappings. + */ + public function getMappingsForUser(UserInterface $user): array { + if ($user->get('field_oe_ldap_groups')->isEmpty() && $user->get('field_oe_department')->isEmpty()) { + return []; + } + + if ($user->get('field_oe_organisation')->value !== 'eu.europa.ec') { + return []; + } + + $query = $this->entityTypeManager->getStorage('corporate_roles_mapping')->getQuery() + ->accessCheck(FALSE); + + $or_group = $query->orConditionGroup(); + + if (!$user->get('field_oe_ldap_groups')->isEmpty()) { + $ldap_group_condition = $query->andConditionGroup(); + $ldap_group_condition->condition('value', $user->get('field_oe_ldap_groups')->value); + $ldap_group_condition->condition('matching_value_type', CorporateRolesMapping::LDAP_GROUP); + $or_group->condition($ldap_group_condition); + } + + if (!$user->get('field_oe_department')->isEmpty()) { + $department_condition = $query->andConditionGroup(); + $department_condition->condition('value', $this->processDepartmentValue($user->get('field_oe_department')->value), 'IN'); + $department_condition->condition('matching_value_type', CorporateRolesMapping::DEPARTMENT); + $or_group->condition($department_condition); + } + + $query->condition($or_group); + $ids = $query->execute(); + + if (!$ids) { + return []; + } + + return $this->entityTypeManager->getStorage('corporate_roles_mapping')->loadMultiple($ids); + } + + /** + * Processes the department value to turn it into an array of options. + * + * @param string $department + * The department string value. + * + * @return array + * The array of concatenated options. + */ + protected function processDepartmentValue(string $department): array { + $parts = explode('.', $department); + $values = []; + $accumulator = ''; + foreach ($parts as $key => $value) { + $accumulator = ($key === 0) ? $value : $accumulator . '.' . $value; + $values[] = $accumulator; + } + + return $values; + } + +} diff --git a/modules/oe_authentication_corporate_roles/src/Entity/CorporateRolesMapping.php b/modules/oe_authentication_corporate_roles/src/Entity/CorporateRolesMapping.php new file mode 100644 index 0000000..f9cfa5e --- /dev/null +++ b/modules/oe_authentication_corporate_roles/src/Entity/CorporateRolesMapping.php @@ -0,0 +1,152 @@ +get('oe_manual_roles')->getValue(), 'target_id'); + foreach ($user->getRoles(TRUE) as $role) { + if (!in_array($role, $manual_roles) && in_array($role, $this->roles)) { + $user->removeRole($role); + } + } + } + + /** + * Removes the mapping reference from the user. + * + * @param \Drupal\user\UserInterface $user + * The user. + */ + public function removeMappingReference(UserInterface $user): void { + $existing_mappings = array_column($user->get('oe_corporate_roles_mappings')->getValue(), 'target_id'); + $current_mapping_id = $this->id(); + $existing_mappings = array_filter($existing_mappings, function ($id) use ($current_mapping_id) { + return $id !== $current_mapping_id; + }); + $user->set('oe_corporate_roles_mappings', $existing_mappings); + } + + /** + * Updates the roles of the user with the ones from the mapping. + * + * @param \Drupal\user\UserInterface $user + * The user. + */ + public function updateUserRoles(UserInterface $user): void { + // Set the roles. + foreach ($this->roles as $role) { + $user->addRole($role); + } + + // Reference the mapping. + $mappings = array_column($user->get('oe_corporate_roles_mappings')->getValue(), 'target_id'); + $mappings[] = $this->id(); + $user->set('oe_corporate_roles_mappings', array_unique($mappings)); + + // Ensure the account is active. + $user->activate(); + } + +} diff --git a/modules/oe_authentication_corporate_roles/src/EventSubscriber/EuLoginSubscriber.php b/modules/oe_authentication_corporate_roles/src/EventSubscriber/EuLoginSubscriber.php new file mode 100644 index 0000000..b7572db --- /dev/null +++ b/modules/oe_authentication_corporate_roles/src/EventSubscriber/EuLoginSubscriber.php @@ -0,0 +1,147 @@ +mappingLookup = $mappingLookup; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + // We ensure this runs after EuLoginAttributesToUserFieldsSubscriber. + CasHelper::EVENT_POST_LOGIN => ['onPostLogin', -100], + CasHelper::EVENT_PRE_REGISTER => ['postProcessUserProperties', -50], + ]; + } + + /** + * Handles the role assignment once the user logged in. + * + * @param \Drupal\cas\Event\CasPostLoginEvent $event + * The triggered event. + */ + public function onPostLogin(CasPostLoginEvent $event): void { + $account = $event->getAccount(); + // First, if there are any mapping referenced, clear the roles of those + // mappings and the mappings themselves. + $result = $this->clearExistingMappings($account); + + $mappings = $this->mappingLookup->getMappingsForUser($account); + + if (!$mappings) { + // If we don't have any mappings for this user, bail out. But also save + // the user account if a change had been made earlier. + if ($result) { + $account->automatic_corporate_roles = TRUE; + $account->save(); + } + return; + } + + // Add all the mapping roles for the mappings that have been found. + $roles = []; + foreach ($mappings as $mapping) { + $roles = array_merge($roles, $mapping->get('roles')); + } + $roles = array_unique($roles); + foreach ($roles as $role) { + $account->addRole($role); + } + + // Reference the mappings. + $mapping_ids = []; + foreach ($mappings as $mapping) { + $mapping_ids[] = $mapping->id(); + } + $account->set('oe_corporate_roles_mappings', array_unique($mapping_ids)); + + // Ensure the account is active and save. + $account->activate(); + $account->automatic_corporate_roles = TRUE; + $account->save(); + } + + /** + * Acts on the CAS user registration. + * + * If the user registers and they have a potential mapping, set the status + * to active. Apart from the fact that it's needed, it will allow the user + * to log in, and the post login subscriber kicks in and assigns the roles. + * + * @param \Drupal\cas\Event\CasPreRegisterEvent $event + * The triggered event. + */ + public function postProcessUserProperties(CasPreRegisterEvent $event): void { + $properties = EuLoginAttributesHelper::convertEuLoginAttributesToFieldValues($event->getCasPropertyBag()->getAttributes()); + $account = User::create([]); + foreach ($properties as $name => $value) { + if (is_array($value)) { + $value = array_values($value); + } + $account->set($name, $value); + } + $mappings = $this->mappingLookup->getMappingsForUser($account); + if ($mappings) { + $event->setPropertyValue('status', 1); + } + } + + /** + * Clears the existing roles and mappings from the user. + * + * @param \Drupal\user\UserInterface $user + * The user. + * + * @return bool + * Whether a change was made on the user. + */ + protected function clearExistingMappings(UserInterface $user): bool { + if ($user->get('oe_corporate_roles_mappings')->isEmpty()) { + // If there are no mapping referenced, we don't need to do anything. It + // means the user never got any automatic roles. + return FALSE; + } + + /** @var \Drupal\oe_authentication_corporate_roles\Entity\CorporateRolesMapping[] $mappings */ + $mappings = $user->get('oe_corporate_roles_mappings')->referencedEntities(); + foreach ($mappings as $mapping) { + $mapping->removeMappingRoles($user); + $mapping->removeMappingReference($user); + } + + return TRUE; + } + +} diff --git a/modules/oe_authentication_corporate_roles/src/Form/CorporateRolesMappingForm.php b/modules/oe_authentication_corporate_roles/src/Form/CorporateRolesMappingForm.php new file mode 100644 index 0000000..9761888 --- /dev/null +++ b/modules/oe_authentication_corporate_roles/src/Form/CorporateRolesMappingForm.php @@ -0,0 +1,133 @@ +mappingLookup = $mappingLookup; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('oe_authentication_corporate_roles.mapping_lookup') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state): array { + + $form = parent::form($form, $form_state); + + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $this->entity->label(), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $this->entity->id(), + '#machine_name' => [ + 'exists' => [CorporateRolesMapping::class, 'load'], + ], + '#disabled' => !$this->entity->isNew(), + ]; + + $form['matching_value_type'] = [ + '#type' => 'select', + '#title' => $this->t('Matching type'), + '#options' => [ + CorporateRolesMapping::DEPARTMENT => $this->t('Department'), + CorporateRolesMapping::LDAP_GROUP => $this->t('LDAP group'), + ], + '#required' => TRUE, + '#default_value' => $this->entity->get('matching_value_type'), + ]; + + $form['value'] = [ + '#type' => 'textfield', + '#title' => $this->t('Value'), + '#maxlength' => 255, + '#default_value' => $this->entity->get('value'), + '#description' => $this->t('The value to match against: either the LDAP group or the department. If department, you can broaden the match by using parts of the department. For example "COMM.B.3" or "COMM.B".'), + '#required' => TRUE, + ]; + + $roles = $this->entityTypeManager->getStorage('user_role')->loadMultiple(); + unset($roles[RoleInterface::ANONYMOUS_ID]); + unset($roles[RoleInterface::AUTHENTICATED_ID]); + $roles = array_map(fn(RoleInterface $role) => Html::escape($role->label()), $roles); + + $form['account']['roles'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Roles'), + '#default_value' => $this->entity->get('roles'), + '#options' => $roles, + '#required' => TRUE, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $roles = $form_state->getValue('roles'); + $roles = array_filter($roles); + $form_state->setValue('roles', $roles); + parent::submitForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state): int { + $result = parent::save($form, $form_state); + $message_args = ['%label' => $this->entity->label()]; + $this->messenger()->addStatus( + match($result) { + \SAVED_NEW => $this->t('Created new %label.', $message_args), + \SAVED_UPDATED => $this->t('Updated %label.', $message_args), + } + ); + $form_state->setRedirectUrl($this->entity->toUrl('collection')); + + return $result; + } + +} diff --git a/modules/oe_authentication_corporate_roles/tests/src/FunctionalJavascript/AutomaticCorporateRolesTest.php b/modules/oe_authentication_corporate_roles/tests/src/FunctionalJavascript/AutomaticCorporateRolesTest.php new file mode 100644 index 0000000..c8f8d82 --- /dev/null +++ b/modules/oe_authentication_corporate_roles/tests/src/FunctionalJavascript/AutomaticCorporateRolesTest.php @@ -0,0 +1,281 @@ +getEditable('cas.settings')->get('forced_login'); + $forced_login['enabled'] = TRUE; + \Drupal::configFactory()->getEditable('cas.settings')->set('forced_login', $forced_login)->save(); + \Drupal::service('cas_mock_server.server_manager')->start(); + + \Drupal::configFactory()->getEditable('oe_authentication.settings')->set('assurance_level', 'LOW')->save(); + } + + /** + * Tests that upon first login (register), the user gets the relevant roles. + */ + public function testAutomaticRolesOnRegister(): void { + \Drupal::configFactory()->getEditable('user.settings')->set('register', UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)->save(); + + Role::create(['id' => 'test_role', 'label' => 'test role'])->save(); + + $mapping = CorporateRolesMapping::create([ + 'label' => 'test', + 'id' => 'test', + 'matching_value_type' => CorporateRolesMapping::LDAP_GROUP, + 'value' => 'COMM_ONE', + 'roles' => ['test_role'], + ]); + $mapping->save(); + + $this->createUserWithAttributes([ + 'groups' => 'COMM_ONE', + ]); + $this->logUserIn(); + $user = $this->loadTestUser(); + $this->assertTrue($user->isActive()); + $this->assertUsersWithRoles([ + 'test' => ['test_role'], + ]); + + // Without any mapping, the user is not active by default. + $this->logUserOut(); + $this->deleteTestUser(); + $this->createUserWithAttributes(); + $this->logUserIn(FALSE); + $user = $this->loadTestUser(); + $this->assertFalse($user->isActive()); + } + + /** + * Tests that upon login, the user gets the relevant roles. + */ + public function testAutomaticRolesOnLogin(): void { + // Create some roles. + foreach (['role one', 'role two', 'role three', 'role four', 'role five'] as $name) { + Role::create(['id' => str_replace(' ', '_', $name), 'label' => $name])->save(); + } + + // Create some mappings. + $one = CorporateRolesMapping::create([ + 'label' => 'one', + 'id' => 'a', + 'matching_value_type' => CorporateRolesMapping::LDAP_GROUP, + 'value' => 'COMM_ONE', + 'roles' => ['role_one'], + ]); + $one->save(); + + $two = CorporateRolesMapping::create([ + 'label' => 'two', + 'id' => 'b', + 'matching_value_type' => CorporateRolesMapping::LDAP_GROUP, + 'value' => 'DIGIT_TWO', + 'roles' => ['role_two'], + ]); + $two->save(); + + $three = CorporateRolesMapping::create([ + 'label' => 'three', + 'id' => 'c', + 'matching_value_type' => CorporateRolesMapping::DEPARTMENT, + 'value' => 'DIGIT.5', + 'roles' => ['role_three'], + ]); + $three->save(); + + $four = CorporateRolesMapping::create([ + 'label' => 'four', + 'id' => 'd', + 'matching_value_type' => CorporateRolesMapping::DEPARTMENT, + 'value' => 'DIGIT.5.0.003', + 'roles' => ['role_four'], + ]); + $four->save(); + + $this->createUserWithAttributes(); + $this->logUserIn(); + $user = $this->loadTestUser(); + // We have no roles as nothing matched. + $this->assertEmpty($user->getRoles(TRUE)); + + $this->logUserOut(); + $this->deleteTestUser(); + $this->createUserWithAttributes([ + 'departmentNumber' => 'DIGIT.5.0.003', + ]); + $this->logUserIn(); + $user = $this->loadTestUser(); + $this->assertTrue($user->isActive()); + $this->assertUsersWithRoles([ + 'test' => ['role_three', 'role_four'], + ]); + + $this->logUserOut(); + $this->deleteTestUser(); + $this->createUserWithAttributes([ + 'groups' => 'COMM_ONE', + ]); + $this->logUserIn(); + $user = $this->loadTestUser(); + $this->assertTrue($user->isActive()); + $this->assertUsersWithRoles([ + 'test' => ['role_one'], + ]); + + // Log out, delete the mappings and log back in. + $this->logUserOut(); + $one->delete(); + $this->logUserIn(); + $user = $this->loadTestUser(); + $this->assertEmpty($user->getRoles(TRUE)); + + // Delete the user and create another one but add a manual role to it. + $this->deleteTestUser(); + $this->createUserWithAttributes([ + 'groups' => 'DIGIT_TWO', + ]); + } + + /** + * Tests that users can create corporate mappings in the UI. + */ + public function testCorporateMappingCreation(): void { + $forced_login = \Drupal::configFactory()->getEditable('cas.settings')->get('forced_login'); + $forced_login['enabled'] = FALSE; + \Drupal::configFactory()->getEditable('cas.settings')->set('forced_login', $forced_login)->save(); + + $user = $this->drupalCreateUser(); + $this->drupalLogin($user); + + $this->drupalGet('/admin/people/corporate_roles_mapping/add'); + $this->assertSession()->pageTextContains('Access denied'); + $role = Role::create(['label' => 'test', 'id' => 'test']); + $role->grantPermission('manage corporate roles'); + $role->save(); + $user->addRole($role->id()); + $user->save(); + $this->drupalGet('/admin/people/corporate_roles_mapping/add'); + + $this->getSession()->getPage()->fillField('Label', 'Test mapping'); + $this->assertSession()->waitForElement('css', '.machine-name-value'); + $this->getSession()->getPage()->selectFieldOption('Matching type', 'Department'); + $this->getSession()->getPage()->fillField('Value', 'COMM.B.3'); + $this->getSession()->getPage()->checkField('test'); + $this->getSession()->getPage()->pressButton('Save'); + $this->assertSession()->pageTextContains('Created new Test mapping.'); + $this->assertSession()->pageTextContains('No users were found matching these conditions.'); + $mapping = CorporateRolesMapping::load('test_mapping'); + $this->assertEquals('department', $mapping->get('matching_value_type')); + $this->assertEquals('COMM.B.3', $mapping->get('value')); + $this->assertEquals(['test' => 'test'], $mapping->get('roles')); + + } + + /** + * Creates a test user with specific department and group info. + * + * @param array $attributes + * The attributes. + */ + protected function createUserWithAttributes(array $attributes = []): void { + $user = [ + 'username' => 'test', + 'email' => 'test@example.com', + 'password' => 'test', + 'firstName' => 'John', + 'lastName' => 'Rambo', + 'domain' => 'eu.europa.ec', + ] + $attributes; + $user_manager = \Drupal::service('cas_mock_server.user_manager'); + $user_manager->addUser($user); + } + + /** + * Logs the test user in. + */ + protected function logUserIn(bool $active = TRUE): void { + $this->drupalGet('user/login'); + $this->getSession()->getPage()->fillField('Username or e-mail address', 'test@example.com'); + $this->getSession()->getPage()->fillField('Password', 'test'); + $this->getSession()->getPage()->pressButton('Login!'); + if ($active) { + $this->assertSession()->pageTextContains('You have been logged in'); + return; + } + $this->assertSession()->pageTextContains('Thank you for applying for an account. Your account is currently pending approval by the site administrator.'); + + } + + /** + * Deletes the test user. + */ + protected function deleteTestUser(): void { + $user_manager = \Drupal::service('cas_mock_server.user_manager'); + $user_manager->deleteUsers(); + + $user = $this->loadTestUser(); + $user->delete(); + } + + /** + * Loads the test user. + * + * @return \Drupal\user\UserInterface + * The test user. + */ + protected function loadTestUser(): UserInterface { + $storage = \Drupal::entityTypeManager()->getStorage('user'); + $storage->resetCache(); + $users = $storage->loadByProperties(['name' => 'test']); + return reset($users); + } + + /** + * Logs the user out. + * + * We cannot use the regular base method because of CAS redirect. + */ + protected function logUserOut(): void { + $this->drupalGet('/user/logout'); + $this->submitForm([], 'op'); + } + +} diff --git a/modules/oe_authentication_corporate_roles/tests/src/Kernel/CorporateRolesTest.php b/modules/oe_authentication_corporate_roles/tests/src/Kernel/CorporateRolesTest.php index ae5012d..df0380c 100644 --- a/modules/oe_authentication_corporate_roles/tests/src/Kernel/CorporateRolesTest.php +++ b/modules/oe_authentication_corporate_roles/tests/src/Kernel/CorporateRolesTest.php @@ -5,20 +5,26 @@ namespace Drupal\Tests\oe_authentication_corporate_roles\Kernel; use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\oe_authentication_corporate_roles\Traits\CorporateRolesTestTrait; +use Drupal\oe_authentication_corporate_roles\Entity\CorporateRolesMapping; use Drupal\user\Entity\Role; use Drupal\user\Entity\User; +use Drupal\user\UserInterface; /** * Tests the corporate roles. */ class CorporateRolesTest extends KernelTestBase { + use CorporateRolesTestTrait; + /** * {@inheritdoc} */ protected static $modules = [ 'oe_authentication', 'oe_authentication_corporate_roles', + 'oe_authentication_user_fields', 'system', 'user', 'field', @@ -48,6 +54,8 @@ public function testManualRoleAssignment(): void { $user = User::create([ 'name' => 'test_user', 'roles' => [$role_one->id()], + 'field_oe_department' => 'DIGIT.B.3.001', + 'field_oe_organisation' => 'eu.europa.ec', ]); $user->save(); @@ -60,12 +68,20 @@ public function testManualRoleAssignment(): void { $user = User::load($user->id()); $this->assertEmpty($user->get('oe_manual_roles')->getValue()); - // Assign back the role but mimic it was done automatically. - $user->addRole('test_role_one'); - $user->automatic_corporate_roles = TRUE; - $user->save(); + // Create a mapping with the role. This will automatically be added + // to the user. + CorporateRolesMapping::create([ + 'id' => 'test', + 'label' => 'test', + 'matching_value_type' => CorporateRolesMapping::DEPARTMENT, + 'value' => 'DIGIT.B.3.001', + 'roles' => ['test_role_one'], + ])->save(); $user = User::load($user->id()); $this->assertEmpty($user->get('oe_manual_roles')->getValue()); + $this->assertUsersWithRoles([ + 'test_user' => ['test_role_one'], + ]); // Save the user again, but this time don't change the roles. Assert that // because we didn't make a change to the user roles, the existing roles @@ -74,13 +90,276 @@ public function testManualRoleAssignment(): void { $user = User::load($user->id()); $this->assertEmpty($user->get('oe_manual_roles')->getValue()); - // Now add another role and assert it gets added to the manual list. + // Now add another role and assert it gets added to the manual list. But + // only this extra one, the existing one was added automatically, so it + // should not count as a manual one. $user->addRole('test_role_two'); $user->save(); $user = User::load($user->id()); // Both roles are added to the manual list, denoting that the user is aware // of adding the roles. - $this->assertEquals(['test_role_one', 'test_role_two'], array_column($user->get('oe_manual_roles')->getValue(), 'target_id')); + $this->assertEquals(['test_role_two'], array_column($user->get('oe_manual_roles')->getValue(), 'target_id')); + } + + /** + * Tests that when we CRUD corporate mappings, users get updated. + */ + public function testCorporateMappingUserUpdates(): void { + // Create some roles. + foreach (['role one', 'role two', 'role three'] as $name) { + Role::create(['id' => str_replace(' ', '_', $name), 'label' => $name])->save(); + } + + // Create some test users. + $values = [ + [ + 'name' => 'user one', + 'field_oe_ldap_groups' => ['COMM_ONE', 'COMM_TWO'], + 'field_oe_organisation' => 'eu.europa.ec', + ], + [ + 'name' => 'user two', + 'field_oe_ldap_groups' => ['COMM_THREE'], + 'field_oe_organisation' => 'eu.europa.ec', + ], + [ + 'name' => 'user three', + 'field_oe_department' => 'COMM.B.3.003', + 'field_oe_organisation' => 'eu.europa.ec', + ], + [ + 'name' => 'user four', + 'field_oe_department' => 'DIGIT.C.3.001', + 'field_oe_organisation' => 'eu.europa.ec', + ], + [ + 'name' => 'user five', + 'field_oe_department' => 'DIGIT.C.3.001', + 'field_oe_ldap_groups' => ['DIGIT_THREE'], + 'field_oe_organisation' => 'eu.europa.ec', + ], + [ + 'name' => 'user six', + 'field_oe_department' => 'DIGIT.C.3.001', + 'field_oe_ldap_groups' => ['DIGIT_ONE'], + 'field_oe_organisation' => 'external', + ], + ]; + + foreach ($values as $value) { + User::create($value)->save(); + } + + // Create and update a corporate mapping and assert the relevant users get + // updated. + $mapping = CorporateRolesMapping::create([ + 'label' => 'test', + 'id' => 'test', + 'matching_value_type' => CorporateRolesMapping::LDAP_GROUP, + 'value' => 'COMM_ONE', + 'roles' => ['role_one'], + ]); + $mapping->save(); + + $expected = [ + 'user one' => ['role_one'], + ]; + $this->assertUsersWithRoles($expected); + + // Edit the mapping and change a role and a condition. + $mapping->set('roles', ['role_two']); + $mapping->set('matching_value_type', CorporateRolesMapping::DEPARTMENT); + $mapping->set('value', 'COMM.B.3'); + $mapping->save(); + + // Now the user which mapped before, no longer maps. Instead, another one + // does. + $expected = [ + 'user three' => ['role_two'], + ]; + $this->assertUsersWithRoles($expected); + + // Update to map to the entire DIGIT. + $mapping->set('value', 'DIGIT'); + $mapping->save(); + $expected = [ + 'user four' => ['role_two'], + 'user five' => ['role_two'], + ]; + $this->assertUsersWithRoles($expected); + $this->assertCorporateMappingReferences([ + 'user four' => ['test'], + 'user five' => ['test'], + ]); + + // Create another mapping that will overlap. + $another_mapping = CorporateRolesMapping::create([ + 'label' => 'test2', + 'id' => 'test2', + 'matching_value_type' => CorporateRolesMapping::DEPARTMENT, + 'value' => 'DIGIT.C.3.001', + 'roles' => ['role_one'], + ]); + $another_mapping->save(); + + $expected = [ + 'user four' => ['role_two', 'role_one'], + 'user five' => ['role_two', 'role_one'], + ]; + $this->assertUsersWithRoles($expected); + $this->assertCorporateMappingReferences([ + 'user four' => ['test', 'test2'], + 'user five' => ['test', 'test2'], + ]); + + // Update this second mapping to change the conditions. + $another_mapping->set('value', 'COMM.B'); + $another_mapping->save(); + + $expected = [ + 'user three' => ['role_one'], + 'user four' => ['role_two'], + 'user five' => ['role_two'], + ]; + $this->assertUsersWithRoles($expected); + $this->assertCorporateMappingReferences([ + 'user three' => ['test2'], + 'user four' => ['test'], + 'user five' => ['test'], + ]); + + // Delete this second mapping and assert we have the roles cleared. + $another_mapping->delete(); + $expected = [ + 'user four' => ['role_two'], + 'user five' => ['role_two'], + ]; + $this->assertUsersWithRoles($expected); + $this->assertCorporateMappingReferences([ + 'user four' => ['test'], + 'user five' => ['test'], + ]); + } + + /** + * Tests the various cases of looking up mappings for users. + */ + public function testMappingsLookup(): void { + // Create some roles. + foreach (['role one', 'role two', 'role three'] as $name) { + Role::create(['id' => str_replace(' ', '_', $name), 'label' => $name])->save(); + } + + // Create some mappings. + $one = CorporateRolesMapping::create([ + 'label' => 'one', + 'id' => 'a', + 'matching_value_type' => CorporateRolesMapping::LDAP_GROUP, + 'value' => 'COMM_ONE', + 'roles' => ['role_one'], + ]); + $one->save(); + + $two = CorporateRolesMapping::create([ + 'label' => 'two', + 'id' => 'b', + 'matching_value_type' => CorporateRolesMapping::LDAP_GROUP, + 'value' => 'DIGIT_TWO', + 'roles' => ['role_one'], + ]); + $two->save(); + + $three = CorporateRolesMapping::create([ + 'label' => 'three', + 'id' => 'c', + 'matching_value_type' => CorporateRolesMapping::DEPARTMENT, + 'value' => 'DIGIT.5', + 'roles' => ['role_one'], + ]); + $three->save(); + + $four = CorporateRolesMapping::create([ + 'label' => 'four', + 'id' => 'd', + 'matching_value_type' => CorporateRolesMapping::DEPARTMENT, + 'value' => 'DIGIT.5.0.003', + 'roles' => ['role_one'], + ]); + $four->save(); + + // Create a user for which to look for mappings. + $user = User::create([ + 'name' => 'test user', + 'field_oe_department' => 'DIGIT.5.0.003', + 'field_oe_ldap_groups' => ['DIGIT_TWO'], + 'field_oe_organisation' => 'external', + ]); + $user->save(); + + /** @var \Drupal\oe_authentication_corporate_roles\CorporateRolesMappingLookup $lookup_service */ + $lookup_service = \Drupal::service('oe_authentication_corporate_roles.mapping_lookup'); + // The user is external, so no mappings are found. + $this->assertEmpty($lookup_service->getMappingsForUser($user)); + + $user->set('field_oe_organisation', 'eu.europa.ec'); + $user->set('field_oe_ldap_groups', []); + $user->save(); + $mappings = $lookup_service->getMappingsForUser($user); + $this->assertCount(2, $mappings); + $this->assertEquals(['c', 'd'], array_keys($mappings)); + + // Add also a group so we get another mapping. + $user->set('field_oe_ldap_groups', ['DIGIT_TWO']); + $user->save(); + $mappings = $lookup_service->getMappingsForUser($user); + $this->assertCount(3, $mappings); + $this->assertEquals(['b', 'c', 'd'], array_keys($mappings)); + + // Remove the department and change the group. + $user->set('field_oe_ldap_groups', ['COMM_ONE']); + $user->set('field_oe_department', NULL); + $user->save(); + $mappings = $lookup_service->getMappingsForUser($user); + $this->assertCount(1, $mappings); + $this->assertEquals(['a'], array_keys($mappings)); + } + + /** + * Asserts the corporate mapping references on the users. + * + * @param array $expected + * The expected references. + */ + protected function assertCorporateMappingReferences(array $expected): void { + $ids = \Drupal::entityTypeManager()->getStorage('user')->getQuery() + ->accessCheck(FALSE) + ->sort('uid', 'ASC') + ->execute(); + + $users = User::loadMultiple($ids); + $actual = []; + foreach ($users as $user) { + if ($user->get('oe_corporate_roles_mappings')->isEmpty()) { + continue; + } + $actual[$user->label()] = array_column($user->get('oe_corporate_roles_mappings')->getValue(), 'target_id'); + } + + $this->assertEquals($expected, $actual); + } + + /** + * Returns the user by name. + * + * @param string $name + * The name. + * + * @return \Drupal\user\UserInterface + * The user. + */ + protected function getUserByName(string $name): UserInterface { + $users = \Drupal::entityTypeManager()->getStorage('user')->loadByProperties(['name' => $name]); + return reset($users); } } diff --git a/modules/oe_authentication_corporate_roles/tests/src/Traits/CorporateRolesTestTrait.php b/modules/oe_authentication_corporate_roles/tests/src/Traits/CorporateRolesTestTrait.php new file mode 100644 index 0000000..70af07f --- /dev/null +++ b/modules/oe_authentication_corporate_roles/tests/src/Traits/CorporateRolesTestTrait.php @@ -0,0 +1,39 @@ +getStorage('user'); + $storage->resetCache(); + $ids = \Drupal::entityTypeManager()->getStorage('user')->getQuery() + ->accessCheck(FALSE) + ->sort('uid', 'ASC') + ->execute(); + + $users = $storage->loadMultiple($ids); + $actual = []; + foreach ($users as $user) { + $roles = $user->getRoles(TRUE); + if (!$roles) { + continue; + } + $actual[$user->label()] = $user->getRoles(TRUE); + } + + $this->assertEquals($expected, $actual); + } + +} diff --git a/modules/oe_authentication_user_fields/src/EventSubscriber/EuLoginAttributesToUserFieldsSubscriber.php b/modules/oe_authentication_user_fields/src/EventSubscriber/EuLoginAttributesToUserFieldsSubscriber.php index 09152b5..c99c661 100644 --- a/modules/oe_authentication_user_fields/src/EventSubscriber/EuLoginAttributesToUserFieldsSubscriber.php +++ b/modules/oe_authentication_user_fields/src/EventSubscriber/EuLoginAttributesToUserFieldsSubscriber.php @@ -35,6 +35,9 @@ public function updateUserData(CasPostLoginEvent $event): void { $properties = EuLoginAttributesHelper::convertEuLoginAttributesToFieldValues($event->getCasPropertyBag()->getAttributes()); $account = $event->getAccount(); foreach ($properties as $name => $value) { + if (is_array($value)) { + $value = array_values($value); + } $account->set($name, $value); } $account->save(); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1537459..32b5fa3 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,6 +6,7 @@