From cf53ca3a0d4bc41a12e27d03c1e9353bb1434f6d Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Thu, 7 May 2026 15:11:23 +0200 Subject: [PATCH] Fix Microsoft 365 SCIM bridge connection and scope Add the missing MICROSOFT_365 value to the connector_provider enum so the connector loader stops failing with SQLSTATE 22P02. Scope each Identity Provider card to its own SCIMBridge type so connecting Microsoft 365 no longer marks Google Workspace as connected (and vice versa). Filter Microsoft Graph /users to userType eq 'Member' so the bridge only syncs home-tenant members and skips B2B guest accounts that were polluting the synced People list. Signed-off-by: Bryan Frimin --- .../_components/GoogleWorkspaceConnector.tsx | 3 +- .../_components/Microsoft365Connector.tsx | 3 +- pkg/coredata/migrations/20260507T150000Z.sql | 15 +++++++ .../bridge/provider/microsoft365/provider.go | 40 ++++++++++++++----- 4 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 pkg/coredata/migrations/20260507T150000Z.sql diff --git a/apps/console/src/pages/iam/organizations/settings/_components/GoogleWorkspaceConnector.tsx b/apps/console/src/pages/iam/organizations/settings/_components/GoogleWorkspaceConnector.tsx index 08cefcf5b..a6d6c26cd 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/GoogleWorkspaceConnector.tsx +++ b/apps/console/src/pages/iam/organizations/settings/_components/GoogleWorkspaceConnector.tsx @@ -40,6 +40,7 @@ const googleWorkspaceConnectorFragment = graphql` id bridge { id + type excludedUserNames connector { id @@ -78,7 +79,7 @@ export function GoogleWorkspaceConnector(props: { }) { const { fKey, oauth2Scopes } = props; const data = useFragment(googleWorkspaceConnectorFragment, fKey); - const bridge = data?.bridge; + const bridge = data?.bridge?.type === "GOOGLE_WORKSPACE" ? data.bridge : null; const connector = bridge?.connector; const scimConfigurationId = data?.id; const bridgeId = bridge?.id; diff --git a/apps/console/src/pages/iam/organizations/settings/_components/Microsoft365Connector.tsx b/apps/console/src/pages/iam/organizations/settings/_components/Microsoft365Connector.tsx index 926526d04..ee55aa660 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/Microsoft365Connector.tsx +++ b/apps/console/src/pages/iam/organizations/settings/_components/Microsoft365Connector.tsx @@ -40,6 +40,7 @@ const microsoft365ConnectorFragment = graphql` id bridge { id + type excludedUserNames connector { id @@ -78,7 +79,7 @@ export function Microsoft365Connector(props: { }) { const { fKey, oauth2Scopes } = props; const data = useFragment(microsoft365ConnectorFragment, fKey); - const bridge = data?.bridge; + const bridge = data?.bridge?.type === "MICROSOFT_365" ? data.bridge : null; const connector = bridge?.connector; const scimConfigurationId = data?.id; const bridgeId = bridge?.id; diff --git a/pkg/coredata/migrations/20260507T150000Z.sql b/pkg/coredata/migrations/20260507T150000Z.sql new file mode 100644 index 000000000..5127fb090 --- /dev/null +++ b/pkg/coredata/migrations/20260507T150000Z.sql @@ -0,0 +1,15 @@ +-- Copyright (c) 2026 Probo Inc . +-- +-- Permission to use, copy, modify, and/or distribute this software for any +-- purpose with or without fee is hereby granted, provided that the above +-- copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +-- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +-- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +-- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +-- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +-- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +-- PERFORMANCE OF THIS SOFTWARE. + +ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'MICROSOFT_365'; diff --git a/pkg/iam/scim/bridge/provider/microsoft365/provider.go b/pkg/iam/scim/bridge/provider/microsoft365/provider.go index 6d952b725..6e26bd73f 100644 --- a/pkg/iam/scim/bridge/provider/microsoft365/provider.go +++ b/pkg/iam/scim/bridge/provider/microsoft365/provider.go @@ -23,6 +23,8 @@ import ( "fmt" "io" "net/http" + "net/url" + "strconv" "strings" scimclient "go.probo.inc/probo/pkg/iam/scim/bridge/client" @@ -47,6 +49,11 @@ const graphPageSize = 999 // API misbehaves. const graphMaxPages = 1000 +// graphMemberFilter restricts /users to home-tenant members and +// excludes B2B guest users invited from external tenants — those guests +// should not be provisioned into the connected organization. +const graphMemberFilter = "userType eq 'Member'" + var _ provider.Provider = (*Provider)(nil) type Provider struct { @@ -102,16 +109,14 @@ type graphUsersResponse struct { } func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { - url := fmt.Sprintf( - "%s/users?$select=%s&$top=%d", - graphBaseURL, - graphUserSelect, - graphPageSize, - ) + endpoint, err := buildListUsersURL() + if err != nil { + return nil, fmt.Errorf("cannot build list users URL: %w", err) + } var allUsers scimclient.Users for range graphMaxPages { - users, next, err := p.fetchPage(ctx, url) + users, next, err := p.fetchPage(ctx, endpoint) if err != nil { return nil, err } @@ -146,14 +151,29 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { if next == "" { return allUsers, nil } - url = next + endpoint = next } return nil, fmt.Errorf("microsoft 365: pagination limit of %d pages reached", graphMaxPages) } -func (p *Provider) fetchPage(ctx context.Context, url string) ([]graphUser, string, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) +func buildListUsersURL() (string, error) { + u, err := url.Parse(graphBaseURL + "/users") + if err != nil { + return "", fmt.Errorf("cannot parse graph base URL: %w", err) + } + + q := u.Query() + q.Set("$select", graphUserSelect) + q.Set("$top", strconv.Itoa(graphPageSize)) + q.Set("$filter", graphMemberFilter) + u.RawQuery = q.Encode() + + return u.String(), nil +} + +func (p *Provider) fetchPage(ctx context.Context, endpoint string) ([]graphUser, string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, "", fmt.Errorf("cannot create graph users request: %w", err) }