From 76f781ead15d995a34b194dfb17e1d5fdbf7c83d Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Sat, 14 Mar 2026 12:25:13 +0100 Subject: [PATCH] Clear conflicting external_id when enrolling manual profile into SCIM When a SCIM provider sends CreateUser for a user whose email matches an existing manual profile, but another profile already holds that external_id (e.g. created by a prior CreateUser with a different email), clear the conflicting external_id before enrolling the manual profile. Signed-off-by: Bryan Frimin --- pkg/coredata/membership_profile.go | 36 ++++++++++++++++++++++++++++++ pkg/iam/scim/service.go | 12 ++++++++++ 2 files changed, 48 insertions(+) diff --git a/pkg/coredata/membership_profile.go b/pkg/coredata/membership_profile.go index 978d7792a..3f35e74cf 100644 --- a/pkg/coredata/membership_profile.go +++ b/pkg/coredata/membership_profile.go @@ -1396,6 +1396,42 @@ WHERE return nil } +func (p *MembershipProfile) ClearExternalID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + externalID string, + organizationID gid.GID, +) error { + q := ` +UPDATE iam_membership_profiles +SET + external_id = NULL, + updated_at = @updated_at +WHERE + %s + AND external_id = @external_id + AND organization_id = @organization_id + AND id != @exclude_id +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.NamedArgs{ + "external_id": externalID, + "organization_id": organizationID, + "exclude_id": p.ID, + "updated_at": time.Now(), + } + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot clear external id: %w", err) + } + + return nil +} + func (p *MembershipProfile) Delete( ctx context.Context, conn pg.Conn, diff --git a/pkg/iam/scim/service.go b/pkg/iam/scim/service.go index e6c3b42ee..32dd15657 100644 --- a/pkg/iam/scim/service.go +++ b/pkg/iam/scim/service.go @@ -244,6 +244,18 @@ func (s *Service) CreateUser( return scimerrors.ScimErrorUniqueness } + if externalIdPtr != nil { + if err := profile.ClearExternalID( + ctx, + tx, + scope, + *externalIdPtr, + config.OrganizationID, + ); err != nil { + return fmt.Errorf("cannot clear conflicting external id: %w", err) + } + } + profile.Source = coredata.ProfileSourceSCIM profile.State = profileState profile.FullName = attrs.FullName