Problem/Motivation

This module purports to map SAML attributes to user fields. However, there is no mention that these mappings can only be for single-value fields. Neither in the project documentation nor the UI. I was unpleasantly surprised when I uncovered this, and I desperately need this feature.

Steps to reproduce

Map a multi-value SAML field to a multi-value user field. You will see the first value populated, but no others. There's no warning in the UI when this occurs. Turn on extended logging and you'll see a brief mention of it from modules/samlauth_user_fields/src/EventSubscriber/UserFieldsEventSubscriber.php line 565:

SAML attribute %name has multiple values; we only support using the first one: @values.

Resolution

(Text copied from CHANGELOG.md)

A new option "Synchronize all values of multi-value attributes" (`samlauth_user_fields.mappings:multiple_values`) makes user fields that can hold multiple values mirror all values of the mapped SAML attribute(s). The order of values is ignored when checking whether a field needs to be updated. The option is enabled on new installations. Existing installations keep their previous behavior until it is enabled: such a field is reduced to only the attribute's first value whenever that value changes (and left alone otherwise).

  • If all attributes sent from the IdP (and used by the `samlauth_user_fields` module) are single-value, this option can safely be enabled without causing changes. The option (and its old value 'disabled') will likely be deprecated in the future.
  • Custom event subscribers that have extended UserFieldsEventSubscriber should be audited for impact, before enabling this option. (E.g. if `getUpdatableAttributeValue()` was overridden, check if `getUpdatableAttributeValues()` now needs to be overridden too.)
  • "Use for linking" also follows this option: when disabled / previously, the first value of an attribute would be used for matching users. When enabled, the full attribute value is used.

----

Old text for proposed resolution

Drupal Entity API supports multiple values, so there is no underlying reason why this module cannot sync multiple values. The limitation appears to be around the fact that UserFieldsEventSubscriber::getAttribute only returns one value. This fact is specifically called out in that method's comment documentation, but I'm not sure why:

* This is suitable for single-value attributes. For multi-value attributes,
* we log a debug message to make clear we're dropping data (because this
* indicates that the site owner may need to take care of getting more
* sophisticated path mapping code).

Another aspect of my confusion is why UserFieldsEventSubscriber attempts to link the user based on the mapped values. Unless I'm missing it, this is never mentioned in any documentation. Enabling samlauth_user_fields is supposed to "Maps custom SAML SSO attributes to user fields"

I see that UserFieldsEventSubscriber::getAttribute is used for both use sync and user link, and its use in user link is for building an entity query.

My proposed solution is to either build a separate getAttribute method that supports multiple values for use in user sync, or to update the existing one to support them everywhere it is used.

I would also like clarity on why samlauth_user_fields does user linking as well as syncing.

Issue fork samlauth-3522489

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

pianomansam created an issue. See original summary.

roderik’s picture

Multi-value fields are easily supportable in principle, as you indicate; it just needs some thought and testing. And you are the first one to bring it up in all these years, so that thought/testing hasn't been invested yet.

The challenge for generic support is two fold:

1. On the 'source' side (the SAML message), we get all values as arrays, so there is no way to distinguish between a single value and an array containing a single value. (Feel free to set me straight, if e.g. the attribute data in your SAML responses is somehow marked as "an actual array".)

2. On the 'destination' side, not all fields actually support multi-value. (Case in point: user name, email, language.) And some may be configured/defined to be only single-value, even though the Drupal data model supports multiple. We don't want to just set a multi-value array into these fields, at least not without testing / logging a message.

You're welcome to contribute or sponsor adding this functionality. Even a only-partly-generic patch or extra subscriber helps get it into the general module in time. Failing that: it should be fairly easy to roll your own event subscriber with just a few lines of actual logic. Something like

$new = $event->getAttributes()[YOURATTRIBUTE];
$old = $event->getAccount()->get(YOURFIELD);
if ($new != $old) {
  $event->getAccount()->set(YOURFIELD, $new);
  $event->markAccountChanged();
}
I would also like clarity on why samlauth_user_fields does user linking as well as syncing.

I'm not sure whether you're talking about the boolean map_users config value (which didn't initially exist at all / had its reasons for being introduced, but I'm not sure that's relevant here). Bottom line is: linking is optional.
samlauth_user_fields.field_mapping is a per-field mapping, and no field is linked unless you explicitly set the optional 'link_user_order' sub-value for the specific field.

(From just reviewing the config schema, I'm not sure from the top of my head if saving the samlauth_user_fields.field_mapping value always saves a 'link_user_order' with a default value of 0. It wasn't supposed to. If it does: that's a bug. If it doesn't: I don't see any issue with this.)

pianomansam’s picture

@roderik thanks for the extensive reply. Unfortunately, `UserFieldsEventSubscriber` has proven to be obtuse and IMHO perhaps overengineered. So I'll most likely be writing a one-off data sync using the SamlauthEvents::USER_SYNC event for my purposes.

I would also like clarity on why samlauth_user_fields does user linking as well as syncing.
I'm not sure whether you're talking about the boolean map_users config value (which didn't initially exist at all / had its reasons for being introduced, but I'm not sure that's relevant here). Bottom line is: linking is optional.

I'm referencing the SamlauthEvents::USER_LINK event within UserFieldsEventSubscriber:

  public static function getSubscribedEvents(): array {
    $events[SamlauthEvents::USER_LINK][] = ['onUserLink'];
    $events[SamlauthEvents::USER_SYNC][] = ['onUserSync'];
    return $events;
  }

  /**
   * Tries to link an existing user based on SAML attribute values.
   *
   * @param \Drupal\samlauth\Event\SamlauthUserLinkEvent $event
   *   The event being dispatched.
   */
  public function onUserLink(SamlauthUserLinkEvent $event) {
pianomansam’s picture

A few more responses...

1. On the 'source' side (the SAML message), we get all values as arrays, so there is no way to distinguish between a single value and an array containing a single value. (Feel free to set me straight, if e.g. the attribute data in your SAML responses is somehow marked as "an actual array".)

I'm not sure why that matters, if we can solve for knowing whether or not the destination of the mapping handles arrays.

2. On the 'destination' side, not all fields actually support multi-value. (Case in point: user name, email, language.) And some may be configured/defined to be only single-value, even though the Drupal data model supports multiple.

This is true, but isn't it possible to know if the destination supports multiple values? If it's an entity property (like username, email, language, etc), shouldn't that be accessible from the entity definition? If it's a field on the user, shouldn't that be accessible from the field's configuration?

roderik’s picture

Unfortunately, `UserFieldsEventSubscriber` has proven to be obtuse and IMHO perhaps overengineered. So I'll most likely be writing a one-off data sync using the SamlauthEvents::USER_SYNC event for my purposes.

That's fair. If there are any others who have needed this multi-value functionality, they have apparently also done that.

This is true, but isn't it possible to know if the destination supports multiple values?

Very possibly yes. With some hardcoding for field types probably, but that's OK. That is why I said [anyone is] welcome to contribute or sponsor adding this functionality.

So this issue stays open for that.

Despite the "obtuse and perhaps overengineered" comment, I do think it is clear enough where to add code for this.

roderik’s picture

I've added a nots to the bottom of the project README.

kfolsom’s picture

@roderik, the SAML attributes that my institution's IdP returns includes a multi-value attribute that was mapped to a multi-value user field when we used the simplesamlphp_auth module, via the simplesamlphp_custom_attributes module. One of our website maintainers who recently migrated to samlauth noticed the missing data and came upon this issue thread.

I have used Claude Code to help me develop an update for the samlauth module that successfully handles multi-value mapping. I have done a code review and testing, as well as patching a production site running samlauth 3.14, and I have found no problems with the update.

Do you accept AI-developed code in an MR, with clear indication that it was developed with the assistance of AI? If so, I will submit it and be ready to answer questions and make changes, if required. Thanks much.

roderik’s picture

@kfolsom I am curious to see the MR. I still don't have much experience reviewing AI code, but in principle I'd like to include it. if I can understand it / vouch for it, it can go in. (Which has been the case with the few Claude-assisted MRs that I've tried to read so far.)

Final judgment comes after review. I really want a module like this one (auth related) to keep containing human readable code. On the other hand, if this is going to be a separate (perhaps separately enable-able) subscriber, it may not be a big deal.

kfolsom’s picture

Final judgment comes after review. I really want a module like this one (auth related) to keep containing human readable code. On the other hand, if this is going to be a separate (perhaps separately enable-able) subscriber, it may not be a big deal.

Currently, this is not a separate subscriber but modifications made to the 'UserFieldsEventSubscriber'. If you believe that enabling this for everyone is problematic, it could be made an optional feature. One of the consequences for those who already have multi-value user fields mapped to multi-value SAML attributes would be that those fields will be updated with all attribute values, replacing the single value already there.

I'll submit the MR and we can go from there. Thanks much.

roderik’s picture

Status: Active » Needs work

Reviewed. All good. Needs Work for Q/A 1.

Added a note on AI to the project page.

Apart from that, I'm going to take a closer look at the linking. I suspect the following remark may not be true:

Only the first value is used when [...] linking users (the "Use for linking" option).

I'm planning to

  • First squash the giant @todo in onUserLink() in a separate issue, which I suspect I should have done 2 years ago. Should not cause merge conflicts.
  • Then poke at it (write tests)... and hold up merging this MR/issue, until the point that I'm confident that I'm not going to want to change its multi-value related behavior with another config option. (After that, this can be merged and those tests can be added in another issue.)
roderik’s picture

FWIW:

Apart from that, I'm going to take a closer look at the linking. I suspect the following remark may not be true:

Of course it's true. (I didn't fully read back my own ~6-year-old code, which I never used / noone else commented on.)

EDIT -
Like I said, I'm going to think about how I might evolve the linking behavior in the multi-value case... if there is a logical case to be made for it. The only reason why I might bolt it onto this PR, is because of the config option that it will introduce. If the linking behavior can/should change, based on the same option, I might commit it at the same time.

But it's not your problem.

kfolsom’s picture

Thanks for the thorough review, @roderik. I will work on Q/A 1 and update the MR.

kfolsom’s picture

Status: Needs work » Needs review

Hi, @roderik. I've updated the MR. The opt-in has been added: a per-mapping "Synchronize all values" option, on by default for new mappings and off for existing ones (no update hook needed). I also made the comparison ignore value order, since our IdP sends the same values in a different order between logins; duplicates are kept as agreed. The branch is rebased on current 8.x-3.x and I updated the expected debug message in the new UserFieldsLinkTest, because this MR rewords that message. The pipeline is green. MR description updated with details and two small questions. Setting to Needs review.

roderik’s picture

Status: Needs review » Needs work

Status: Needs work for 1 requested code change + needs review+maybe-work for 1 proposal.

On me:

  • I'm still going to add the change to the linking behavior (+ extend my recently-added tests) when multivalue=on. It isn't strictly necessary, but IMO will be more intuitive if it follows the new areInputValuesEqual() logic (ex. cardinality differences) that is also used for syncing.
  • I reserve the right to do non-invasive last minute changes to logs/warnings/UI/changelog text, before merging. Because I haven't checked all in detail. Also: made a note-to-self to update the issue text, so that it's useful if someone gets here from the CHANGELOG.md link.
  • (Later: I'll un-indent / switch some of my own 6-year-old code in the big loop in the event subscriber. Separately from this MR, because I'm sure it'll get less readable.)
kfolsom’s picture

Status: Needs work » Needs review

Thanks for the feedback, @roderik. I've made the changes you suggested, described below.

Done: areInputValuesEqual() now receives the untruncated values plus the field's cardinality, and array_slice() happens only when writing. A field holding as many values as it can, all of which are among the attribute values, is now left unchanged (your [1,2,3] → [2,3] example, cardinality 2), so it no longer oscillates when the IdP reorders values. The debug log stays where it was.

Note: I read "should return FALSE" as "should not be updated", so the comparison returns TRUE ("equal") in that case. Correct me if you meant otherwise. I added an optional $cardinality parameter because the field name alone doesn't tell full from incomplete (e.g. [2,3] is complete for cardinality 2 but not 3). Added 7 test cases.

I'm leaving the option-off (single-value) behavior as is, per your comment.

roderik’s picture

Thank you. One line of feedback, but I can implement that.

I'll move the README section somewhere else, because I don't think this is "incomplete functionality" anymore.

I happen to need the new multi-value-linking code for something else... so... if I manage to finish that code with good test coverage, I'll merge this issue with that code committed to the same MR.

  • roderik committed 27bbcb35 on 8.x-3.x authored by kfolsom
    feat: #3522489 Synchronize multi-value SAML attributes into multi-value...
roderik’s picture

Issue summary: View changes
Status: Needs review » Fixed

Thanks again for seeing this through.

Merged, so I can build on my linking code for something else (which may be included before a next release). Feedback still welcome, but I'll set Fixed.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

kfolsom’s picture

Thank you for your feedback on this MR and continuing support for the samlauth package.