Files
mattermost/server/build/docker
85b7d85b57 Sync user attributes from OpenID Connect claims (#38939)
* MM: Add PropertySyncer API for LDAP/SAML sync of text, select and multiselect attributes

Introduces the shared sync engine the enterprise LDAP and SAML packages will
call instead of carrying their own text-only logic:

- model: PropertyFieldType.SupportsExternalSync, sync source constants and
  helpers, PropertySync result types, 500-option cap constant.
- app: PropertySyncer (NewPropertySyncer, SyncUser, PruneOrphanedOptions)
  that matches source values to options by exact name, provisions missing
  options with optimistic concurrency, clears values for absent/empty
  attributes, and prunes options no live value references.
- properties: UpdatePropertyFieldsOpts.ExpectedUpdateAts extends optimistic
  concurrency to the caller's own read; validation hook now keeps ldap/saml
  on select/multiselect, accepts empty options on synced fields, locks the
  option set of a synced field to its sync (colors stay editable), and
  grandfathers managed=admin so the sync can provision options; read access
  gives the owning sync the full option list.
- store: PropertyValueStore.GetReferencedOptionIDs plus a partial index on
  PropertyValues(GroupID, FieldID) WHERE DeleteAt = 0.

* Address lint findings in PropertySyncer

Co-authored-by: Ibrahim Serdar Acikgoz <isacikgoz@users.noreply.github.com>

* Drop the option re-provisioning retry from PropertySyncer value writes

The batch upsert already falls back to one write per field so a single bad
value cannot block the rest. The extra path that re-read the field and
re-provisioned options when a concurrent sync pruned one between
provisioning and the write covered a narrow race, was untested, and the
next sync run or login corrects it anyway. Removing it drops ~80 lines and
an i18n key.

Co-authored-by: Ibrahim Serdar Acikgoz <isacikgoz@users.noreply.github.com>

* Give linked fields their template's sync source, and only user fields

createPropertyField copied the template's ldap/saml attrs onto every field
linking to it, whatever its object type, and kept a sync source the caller
sent when the template had none. A channel or post field of an attribute
synced from AD/LDAP or SAML then carried the link, so the sync lock refused
every write to its values -- values no sync ever writes, since an identity
source reports attributes of users.

A linked field's sync source is now exactly its template's, and only a user
field takes it.

Co-authored-by: Ibrahim Serdar Acikgoz <isacikgoz@users.noreply.github.com>

* Refuse property values for a deleted field

UpsertPropertyValues checked that each value's field exists and has the
requested object type, but a soft-deleted field passed both checks: GetMany
returns deleted rows. A writer holding a field it read before the deletion --
the AD/LDAP sync keeps its fields for a whole run -- would write live values
onto it. A deleted field is now reported as not found, like a missing one.

Co-authored-by: Ibrahim Serdar Acikgoz <isacikgoz@users.noreply.github.com>

* Sync custom profile attributes through their Global Attribute definition

Custom profile attributes are now Global Attributes: a template defines the
attribute -- its type, its options and the AD/LDAP or SAML attribute it is
synced from -- and user, channel and post fields link to it, serving its
options. The syncer followed the old shape, a user field holding its own
options, and would have provisioned options onto linked fields (which cannot
own them through a field write), pruned options a channel's value still
references, and missed a sync link an admin added to the template after the
user field existed.

The syncer now resolves each user field to its definition -- the template it
links to, or the field itself -- and takes the external attribute from it.
Options are created on and pruned from the definition through the per-option
API, which compare-and-swaps every change in the property service; a sync that
loses the race re-reads the option list and retries, so the ExpectedUpdateAts
extension to UpdatePropertyFields is no longer needed and is removed. An option
is orphaned only when no live value of the definition or any field linking to
it references it, so GetReferencedOptionIDs takes a set of fields.

Also:
- A definition naming both sources is synced from AD/LDAP only, the only sync
  the value lock admits for it; SAML used to fail on every login.
- Fields and definitions are read from the master: the LDAP job keeps them for
  its whole run.
- A field deleted during a run is dropped after its first failed write rather
  than failing for every remaining user.
- The legacy custom profile attribute event withholds values of restricted
  fields, like every other broadcast.
- The syncer logs each field outcome itself, at the LDAP levels for the AD/LDAP
  source, so the enterprise adapters no longer duplicate that logging.
- A value_type skip no longer quotes the value, and prune results no longer
  carry option names; both are attribute values.

Co-authored-by: Ibrahim Serdar Acikgoz <isacikgoz@users.noreply.github.com>

* Enforce the synced-options rule on templates and the options endpoints

Only an attribute's own sync may add, rename or remove its options. The rule
was checked on field writes only, and on the field carrying the options --
which for a Global Attribute is the template, where Attribute Management edits
them. It now also runs as the PreChangePropertyFieldOptions hook, so the
per-option endpoints are no longer a way around it. That hook sees the field
but not the change, so it refuses every change but the sync's, a recolor
included; recoloring stays possible through the field's option list.

The sync writes options through those endpoints now, so it no longer updates
fields and the managed=admin exemption added for it is reverted.

Co-authored-by: Ibrahim Serdar Acikgoz <isacikgoz@users.noreply.github.com>

* Extract i18n strings for the enterprise sync adapters

The AD/LDAP and SAML adapters no longer build their own attribute-to-field
maps and pass the LDAP entry straight to the shared syncer, so their CPA
field-listing and empty-entry errors are gone.

Co-authored-by: Ibrahim Serdar Acikgoz <isacikgoz@users.noreply.github.com>

* Say what a prune race leaves behind for channel values

A value written between the prune's read of references and its removal keeps
naming a deleted option. A user's is rewritten by the next sync; a channel's
has no sync and stays unset until someone sets it again.

Co-authored-by: Ibrahim Serdar Acikgoz <isacikgoz@users.noreply.github.com>

* Drop retired CPA sync strings from the locale catalogs

The enterprise AD/LDAP and SAML adapters no longer raise
ent.ldap.cpa_field_mapping.list_error, ent.ldap.update_cpa.empty_attribute or
ent.saml.cpa_field_mapping.list_error, so en.json dropped them. The 21 locale
catalogs still carried their translations, which make i18n-verify rejects as
ids en.json no longer has.

* Attribute Management: sync select and multiselect attributes from AD/LDAP and SAML

The server now syncs text, select and multiselect attributes through the
shared PropertySyncer, but the System Console still treated sync as
text-only: linking a source converted the attribute to Text, switching
to any other type cleared the links, and the link modal and the
AD/LDAP/SAML settings section warned that everything else would be
converted.

- supportsExternalSync mirrors PropertyFieldType.SupportsExternalSync.
  Linking keeps a type it accepts (phone, URL and email included, which
  are text on the server); only a type it rejects is converted to Text,
  and only a switch to one clears the links.
- A synced select or multiselect shows its options read-only, with a
  note naming the source that manages them, and no longer requires an
  option before Save, since the sync provisions them.
- Saving an attribute that was already synced leaves its options out of
  the patch: the server refuses anyone but the sync adding, renaming or
  removing one, and a list loaded before the sync added an option would
  read as removing it. Linking for the first time still saves the
  options the admin seeded.
- Adding a link is no longer locked by an Applies-to resource when the
  type would not change.
- The picker tells a Type switch from a chip removal by whether the type
  changed, not by the type being Text.
- The AD/LDAP/SAML "User attributes sync" section patches only the link
  for a syncable type instead of converting it to Text and resending
  every attr, keeps linked select/multiselect fields editable, and links
  to Attribute Management.

* Add OpenID Connect as a user attribute sync source

A user attribute can now be linked to an OpenID Connect claim through
attrs.openid, next to attrs.ldap and attrs.saml. The openid source has
its own sync caller (system:openid_sync) and the same sync lock: only
that sync may write the attribute's values.

The sources are one ordered list (PropertySyncSources), which the sync
source helpers, the sync lock, the owner check, the linked-field copy
and the validation hook now iterate instead of naming ldap and saml, so
the next source is a one-line change. Behaviour for AD/LDAP and SAML is
unchanged.

An OpenID Connect link is exclusive: a definition naming openid may not
also name ldap or saml. OpenID Connect users never go through the
AD/LDAP or SAML sync, so under the AD/LDAP-first precedence such a field
would be locked against the OpenID Connect sync and never written for
them.

FeatureFlags.OpenIdAttributeSync (default on) is a kill switch: while it
is off, a new or changed openid link is refused, and an existing one
rides along untouched.

sanitizeAndValidateFieldAttrs now takes the existing field instead of
three values derived from it, so the rules that compare against the
previous state read it the same way.

* Sync user attributes from OpenID Connect claims at sign-in

AuthorizeOAuthUser now returns an OAuthAuthorization carrying the raw
ID token alongside the userinfo response, which used to be dropped once
the provider had built a user from it. CompleteOAuth reads the userinfo
response once, and after the sign-in, sign-up or switch to OpenID
Connect has resolved the user, and before the session exists, syncs the
user attributes linked to OpenID Connect claims through PropertySyncer.
A switch from OpenID Connect to email does not sync.

The sync runs only for the generic OpenID Connect service, with the
OpenIdAttributeSync flag on and a license with the OpenId feature at
Enterprise tier or above. The claims come from a new optional provider
interface, einterfaces.OAuthClaimsProvider; the provider verifies the ID
token and merges it with the userinfo response. A sync that cannot run,
or whose claims cannot be trusted, is logged and never fails the sign-in
or changes stored values.

OpenIdSettings.AdditionalScopes (through SSOSettings) adds scopes to the
authorization request, for providers that release some claims only
under them. It is validated as an RFC 6749 scope list of at most 1024
characters.

* Add an end-to-end spec for OpenID Connect attribute sync

The mattermost-openid client of both Keycloak realm exports (dev and
Playwright) now releases three user attributes as claims: department as
the nested org.department, clearances as an array, and employeeId as
employee_id in the ID token only.

The Playwright library can give a Keycloak user attributes, replace
them (setKeycloakUserAttributes), and configure OpenID Connect from the
discovery document (ensureKeycloakOpenId({discovery: true})), which the
server needs to verify ID tokens. With the realm frontend URL set,
Keycloak names the browser-facing URL as issuer and authorization
endpoint and the requesting alias for the token, userinfo and JWKS
endpoints, so the split the explicit endpoints set up still holds. The
alias is added to the allowed untrusted internal connections, since the
server fetches the document and the keys from it.

The spec signs a user in through Keycloak, checks that the nested,
array and ID-token-only claims filled their attributes, that a
membership policy on the synced department admits the user, and that
removing the department at Keycloak clears it at the next sign-in and
the policy then removes the user.

* Document syncing user attributes from OpenID Connect claims

The OpenID Connect guide gains a section on linking user attributes to
claims: discovery is required for ID token claims, how claim names and
dot paths resolve, how values are stored, that a missing claim clears
the attribute at the next sign-in, and provider notes (Keycloak
user-editable attributes, Entra ID group overage, scoped claims). The
authentication settings reference documents Additional scopes, and the
user attributes page points to the new section.

* OpenID Connect attribute sync: link user attributes to OpenID Connect claims

An admin can now link a text, select or multiselect user attribute to an
OpenID Connect claim (attrs.openid), next to AD/LDAP and SAML.

- Attribute Management: the external-source helpers move from positional
  ldap/saml arguments to a map keyed by source, shared with
  PROPERTY_SYNC_SOURCES in property_utils. The link picker offers OpenID
  Connect with its own modal copy. It is listed but disabled, with the
  reason inline and in a tooltip beside the entry, when the
  OpenIdAttributeSync flag is off, the license lacks OpenID Connect, or
  OpenID Connect sign-in is off. Existing links keep their chips.
- OpenID Connect is exclusive with AD/LDAP and SAML in both directions,
  matching app.property_field.sync_source_exclusive.app_error on the
  server; AD/LDAP plus SAML stays allowed.
- Saves send attrs.openid like the other sources, null to unlink, and
  cascade it onto the linked Users field.
- The Source column lists every linked source instead of a fixed
  "AD/LDAP, SAML" label; Managed by, the system user detail chips and
  read-only state, isPropertyFieldSourceManaged, the ABAC editor's
  hasControlledAttributeValues and the attribute selector read openid too.
- The OpenID Connect settings page gets the shared "User attributes sync"
  section and an Additional Scopes setting (OpenIdSettings.AdditionalScopes).
  The section now reads the disabled prop its settings pages pass, so a
  read-only admin can no longer edit it.
- The profile editor and the hierarchical attribute editor lock an
  attribute synced from the source the user signs in with, openid included.
- The retired table source labels are dropped from en.json and, as
  check_icu.mjs requires, from the 21 locale catalogs. Test fixtures that
  set attrs.ldap to a boolean now use a mapping string, the shape the
  server stores.

* System user detail: name OpenID Connect and hide chip removal on locked multiselects

The authentication method of an OpenID Connect user read "Openid", and of
a GitLab user "Gitlab", from title-casing the auth service. They now read
"OpenID Connect" and "GitLab".

A multiselect attribute the admin can't edit (synced from an identity
source, or otherwise locked) still showed a remove button on each value,
which did nothing. A disabled select now leaves them out.

Both found in a browser pass against a server syncing attributes from
Keycloak over OpenID Connect.

* Profile settings: name OpenID Connect as the login of an OpenID Connect user

The collapsed Email row names the login provider and address for GitLab,
Google, Entra ID, AD/LDAP and SAML users, but had no case for OpenID
Connect, so it was empty. It now reads "Login done through OpenID Connect
(email)", like the others.

Found in a browser pass with a Keycloak OpenID Connect user.

* List the new OpenID Connect settings in config fixtures

The support packet golden config and the Playwright default config spell
out every setting, so they need SSOSettings.AdditionalScopes (GitLab,
Google and OpenID Connect settings) and FeatureFlags.OpenIdAttributeSync
(the golden only). CI failed TestSupportPacketMarshalGolden and the
Playwright type check without them.

* Attribute Management: hold Save on a synced select until it applies to Users

The sync provisions a select or multiselect's options from the values it
writes to users, and the server lets nothing but the sync change the
options of a synced attribute. A synced select that applies only to
Channels or Posts was therefore left with an option list nobody could
add to. Save now waits, with an inline note, until the attribute applies
to Users or the link is removed, including after Users is removed from
an attribute that was already synced.

The locked Type control's accessible name also announces the type the
attribute keeps, instead of always saying Text, now that linking a
select or multiselect no longer converts it.

* Update the E2E specs for select and multiselect sync

The AD/LDAP and SAML settings page and the Attribute Management form
specs still expected linking a select to convert it to Text. They now
check that a linked select keeps its type and saves only its link, that
a type sync cannot populate (Ranked) is still disabled or converted with
a warning, and that a synced select holds Save until it applies to
Users.

* Docs: document select and multiselect sync in Attribute Management

The Attribute Management page still said linking an external source
converts an attribute to Text. Describe what linking does from v12.1:
Select and Multiselect keep their type and get their options from the
sync, which needs the attribute to apply to Users; a type kept by the
link can be linked while the attribute applies to a resource; and a
value the identity provider stops sending is removed at the next sync.

* E2E: retry the Keycloak realm frontend URL update a parallel worker refused

Tests that point the server at Keycloak each set the realm's frontend URL
on first use. When several run at once against a fresh realm, Keycloak
refuses all but one of the concurrent realm updates with 409 and the
losing tests failed. A refused update is now retried against a fresh
read, which finds the URL the winner set.

* E2E: expect the disabled OpenID Connect source in Attribute Management

With OpenID Connect sync, the external source menu lists OpenID Connect,
disabled with "Can't be combined with ..." while AD/LDAP or SAML is
linked. That label names AD/LDAP, so the unanchored /AD\/LDAP/ locator
matched it, and once both AD/LDAP and SAML are linked the trigger stays
to show it. Anchor the locators and check that only the disabled OpenID
Connect item is left.

* Give the external-source Type lock label a new message id

Its text gained the {value} placeholder, which every existing translation
of the old id lacks, so the translation check rejected them all. The old
id is dropped from the locale catalogs, where it would otherwise be an
extra key.

* Name OpenID Connect in the external-source Type lock label's description

* Fix a race in TestCheckLdapUserPasswordConcurrency's LDAP mock

Each goroutine registered the DoLogin expectation itself. testify adds
an expectation with On() and sets its return values in Return(), so a
goroutine calling DoLogin in between matched an expectation with no
return values and panicked, failing the Postgres shard that ran it.
The expectation is now registered once, before the attempts start.
The check also no longer dereferences an attempt's error before
checking it is set.

* Docs: resolve the OpenID Connect link in Additional scopes through useBaseUrl

The docs build rejects a site-absolute href in raw HTML, since Docusaurus
does not apply baseUrl to it.

* Let a field's own sync clear it when required, and keep a sync off unsynced options

A synced user field marked required (possible through the API) refused
the sync's clearing write, so the user kept a value the directory no
longer has. The field's own sync may now clear it: the source decides
the value, including that there is none. A user field linked to a
template it has no copy of the link of is judged by the template's link.

The options endpoints let any caller change the options of a field that
is not synced, so a sync job that loaded the field before an admin
unlinked it could still add or remove its options. A sync now changes
only the options of a field synced from its own source.

* Clear a synced value the source reports but that cannot be stored

When the sync could not store a user's new value (too long, past the
option cap, or its option could not be created), it skipped the field
and kept the old value, so someone who moved out of Finance could still
pass a Finance-only policy. The stored value is now cleared instead, as
for an attribute the source no longer reports; the outcome still says
skipped or error, and why.

A failure caused by the attribute being unlinked while the syncer ran is
the exception: the field is the admin's again, so its value is left
alone and the syncer stops syncing it. For the same reason the prune now
skips a definition that is no longer synced from its source, instead of
deleting options the admin added after unlinking it.

* Regenerate the Playwright default feature flags for OpenIdAttributeSync

The file is generated from the server's feature flag defaults since
#39091, and the OpenIdAttributeSync flag this branch adds was missing
from it, failing the generated files check.

* Fix the OpenID Connect attribute sync note and the linking sentence in docs

The note on syncing attributes from OpenID Connect claims rendered as one
paragraph with a literal "-", since its list had been collapsed onto one
line. Restore the four bullets. Attribute Management also said AD/LDAP and
SAML can be linked together without saying OpenID Connect can't join them.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Ibrahim Serdar Acikgoz <isacikgoz@users.noreply.github.com>
2026-10-21 15:41:49 +02:00
..
2024-04-03 12:05:31 +02:00
2025-09-15 10:53:28 -03:00