#3522489 Synchronize multi-value SAML attributes into multi-value user fields

Closes #3522489.

What this does

samlauth_user_fields currently writes only the first value of a SAML attribute, even when the mapped user field can hold several. This adds an option, "Synchronize all values of multi-value attributes", that writes all values of the attribute into user fields whose storage allows more than one value (isMultiple()).

Opt-in, as a single option (as proposed):

  • New multiple_values boolean in samlauth_user_fields.mappings, next to link_first_user / ignore_blocked. It applies to all mappings.
  • Enabled on new installations via config/install/samlauth_user_fields.mappings.yml (same pattern as samlauth_user_roles.mapping:log_unknown). Existing installations don't have the key, which means off, so existing sites see no behavior change and no update hook is needed.
  • The checkbox is in a new "Configuration for synchronization" fieldset on the mapping list screen (where the @todo suggested such a section). It's always shown, also when linking is disabled.

With the option on:

  • Empty-string values are skipped. An attribute with no non-empty values leaves the field untouched (never cleared), same as the single-value path.
  • Values beyond the field's cardinality are dropped when writing, with a debug log message, instead of failing validation for the whole list.
  • No oscillation when the cardinality is too low. The field is compared with all attribute values before they're truncated. A field that already holds as many values as it can, all of which are among the attribute's values, is left unchanged, so it doesn't switch to a different selection when the IdP reorders the values. (E.g. attribute [1,2,3], existing [2,3], cardinality 2: no update.) The debug log message is still logged in that case.
  • Several attributes mapped to the same field are combined, in mapping order.
  • Order is ignored when deciding whether to update. SAML Core doesn't make <AttributeValue> order significant (and §1.3.1 says implementations "MUST NOT depend on specific sorting orders for values"). For LDAP/AD-sourced attributes, RFC 4511 §4.1.7 says "The set of attribute values is unordered. … Implementations MUST NOT rely upon the ordering being repeatable." I saw a production IdP send the same values in a different order between logins. So the comparison treats the lists as multisets: a reordering doesn't rewrite the field or save the account. When the values do change, they're written in the order received.
  • Duplicates are kept, as agreed. The comparison counts occurrences, so x, x, y and x, y, y are different.
  • The new list is validated with the existing validateAccountFieldValue() before it's set.
  • The current value is read from the field's main property, so types whose main property isn't value (e.g. link → uri) compare correctly.

Unchanged: all mappings while the option is off, single-value fields (including name/mail), compound sub-fields and user linking (onUserLink()) still use the first value, exactly as before.

Implementation notes

  • onUserSync(): field-name parsing moved before reading the attribute, because the mapping's option and the field's cardinality decide how the attribute is read. Multi-value fields are collected during the loop and written afterwards, alongside the existing compound-field step.
  • New protected helpers: getUpdatableAttributeValues() (list equivalent of getUpdatableAttributeValue()) and areInputValuesEqual() (order-insensitive, occurrence-counting comparison built on isInputValueEqual()). areInputValuesEqual() receives the untruncated values plus an optional $cardinality parameter: the field name alone doesn't tell a full field from an incomplete one ([2,3] is complete for cardinality 2, but not for 3). array_slice() happens only when the field is written.
  • Small fix: invalid mappings (empty or non-string attribute_name/field_name) now continue after the warning, instead of being processed anyway.
  • Small fix in SamlauthMappingListForm::submitForm(): link_first_user / ignore_blocked are only saved if they're on the form. Before, submitting the form while linking was disabled overwrote them with NULL. That rarely mattered while the form had no other options, but the new checkbox makes the form useful when linking is disabled.
  • The mapping edit form is unchanged from 8.x-3.x (the per-mapping checkbox from an earlier revision is removed again).
  • README "Known-incomplete functionality" section and a CHANGELOG entry under 8.x-3.15 are updated, including the note about auditing custom subclasses of UserFieldsEventSubscriber before enabling the option.

Testing

  • New kernel test tests/src/Kernel/UserFieldsSyncTest.php: 23 sync cases (option on/off/missing, single vs. multi-value fields, empty values, replacement, cardinality truncation, no oscillation with too-low cardinality, no-change detection, reordering, duplicates, combining attributes) plus tests of the list form (option saved; linking options not overwritten when not on the form) and of the default on new installations (which also validates the new config file against the schema). I checked that the tests fail when the option is ignored, when the list form overwrites the linking options, when the install default is off, when the comparison is order-sensitive, when it ignores occurrence counts, when values are truncated before comparing, and when any subset of the values is accepted.
  • Rebased on current 8.x-3.x. Full kernel suite (81 tests) and functional tests pass locally on Drupal 11.4 / PHP 8.3, including the new UserFieldsLinkTest, whose expected debug message is updated here because this MR rewords that message. phpcs/phpstan: no new findings.
  • Manual: full SAML logins against a SimpleSAMLphp test IdP (DDEV), and against a production IdP using a backport to 3.14 of an earlier revision (per-mapping opt-in, order-insensitive comparison). The too-low-cardinality change and the single option are covered by the kernel tests.
  • Not tested locally on Drupal 10; relying on CI for that.
Edited by Keith Folsom

Merge request reports

Loading
Loading