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 <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-05-07 15:11:23 +02:00
committed by Émile Ré
parent d1f34add6f
commit cf53ca3a0d
4 changed files with 49 additions and 12 deletions

View File

@@ -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$key>(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;

View File

@@ -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$key>(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;

View File

@@ -0,0 +1,15 @@
-- Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
--
-- 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';

View File

@@ -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)
}