Exclude users from google workspace bridge

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-02-09 17:13:32 +01:00
parent 96f1ca8621
commit 1494707cb5
19 changed files with 826 additions and 43 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<3c7c629a3509891b6edc98178a376c2d>> * @generated SignedSource<<b449429f81b74e6da6c7c6ed523ca0ab>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -16,6 +16,8 @@ export type GoogleWorkspaceConnectorFragment$data = {
readonly createdAt: string; readonly createdAt: string;
readonly id: string; readonly id: string;
} | null | undefined; } | null | undefined;
readonly excludedUserNames: ReadonlyArray<string>;
readonly id: string;
} | null | undefined; } | null | undefined;
readonly id: string; readonly id: string;
readonly " $fragmentType": "GoogleWorkspaceConnectorFragment"; readonly " $fragmentType": "GoogleWorkspaceConnectorFragment";
@@ -48,6 +50,14 @@ return {
"name": "bridge", "name": "bridge",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "excludedUserNames",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -76,6 +86,6 @@ return {
}; };
})(); })();
(node as any).hash = "6ce8b7f55ff2a3d0fad04ad6b452a28a"; (node as any).hash = "4f3898fca8d93369a46b50227f3df63d";
export default node; export default node;

View File

@@ -0,0 +1,115 @@
/**
* @generated SignedSource<<41b1b8127b7840bf52b967e212c286ec>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type UpdateSCIMBridgeInput = {
excludedUserNames: ReadonlyArray<string>;
organizationId: string;
scimBridgeId: string;
};
export type GoogleWorkspaceConnectorUpdateSCIMBridgeMutation$variables = {
input: UpdateSCIMBridgeInput;
};
export type GoogleWorkspaceConnectorUpdateSCIMBridgeMutation$data = {
readonly updateSCIMBridge: {
readonly scimBridge: {
readonly excludedUserNames: ReadonlyArray<string>;
readonly id: string;
};
} | null | undefined;
};
export type GoogleWorkspaceConnectorUpdateSCIMBridgeMutation = {
response: GoogleWorkspaceConnectorUpdateSCIMBridgeMutation$data;
variables: GoogleWorkspaceConnectorUpdateSCIMBridgeMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "UpdateSCIMBridgePayload",
"kind": "LinkedField",
"name": "updateSCIMBridge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SCIMBridge",
"kind": "LinkedField",
"name": "scimBridge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "excludedUserNames",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "GoogleWorkspaceConnectorUpdateSCIMBridgeMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "GoogleWorkspaceConnectorUpdateSCIMBridgeMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "5b2fdb1e51ff728727c10809a89ba0ea",
"id": null,
"metadata": {},
"name": "GoogleWorkspaceConnectorUpdateSCIMBridgeMutation",
"operationKind": "mutation",
"text": "mutation GoogleWorkspaceConnectorUpdateSCIMBridgeMutation(\n $input: UpdateSCIMBridgeInput!\n) {\n updateSCIMBridge(input: $input) {\n scimBridge {\n id\n excludedUserNames\n }\n }\n}\n"
}
};
})();
(node as any).hash = "5b4046c71e69bbcc16e3fc533a5467ee";
export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<68578d960865d35a2d46ee7e2c50a054>> * @generated SignedSource<<e59dc83c1f91e928764947766661f044>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -190,6 +190,13 @@ return {
"plural": false, "plural": false,
"selections": [ "selections": [
(v3/*: any*/), (v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "excludedUserNames",
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -409,12 +416,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "e57ee79b79046f9f07c159c7079f766a", "cacheID": "1074234a9a3734270b5da7420dea2ec6",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "SCIMSettingsPageQuery", "name": "SCIMSettingsPageQuery",
"operationKind": "query", "operationKind": "query",
"text": "query SCIMSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n scimConfiguration {\n id\n bridge {\n id\n }\n ...SCIMEventListFragment\n }\n ...SCIMConfigurationFragment\n ...ConnectorListFragment\n }\n id\n }\n}\n\nfragment ConnectorListFragment on Organization {\n scimConfiguration {\n ...GoogleWorkspaceConnectorFragment\n id\n }\n}\n\nfragment GoogleWorkspaceConnectorFragment on SCIMConfiguration {\n id\n bridge {\n connector {\n id\n createdAt\n }\n id\n }\n}\n\nfragment SCIMConfigurationFragment on Organization {\n canCreateSCIMConfiguration: permission(action: \"iam:scim-configuration:create\")\n canDeleteSCIMConfiguration: permission(action: \"iam:scim-configuration:delete\")\n scimConfiguration {\n id\n endpointUrl\n bridge {\n id\n }\n }\n}\n\nfragment SCIMEventListFragment on SCIMConfiguration {\n events(first: 20) {\n edges {\n node {\n id\n ...SCIMEventListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SCIMEventListItemFragment on SCIMEvent {\n id\n method\n path\n statusCode\n errorMessage\n ipAddress\n createdAt\n membership {\n id\n profile {\n fullName\n id\n }\n }\n}\n" "text": "query SCIMSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n scimConfiguration {\n id\n bridge {\n id\n }\n ...SCIMEventListFragment\n }\n ...SCIMConfigurationFragment\n ...ConnectorListFragment\n }\n id\n }\n}\n\nfragment ConnectorListFragment on Organization {\n scimConfiguration {\n ...GoogleWorkspaceConnectorFragment\n id\n }\n}\n\nfragment GoogleWorkspaceConnectorFragment on SCIMConfiguration {\n id\n bridge {\n id\n excludedUserNames\n connector {\n id\n createdAt\n }\n }\n}\n\nfragment SCIMConfigurationFragment on Organization {\n canCreateSCIMConfiguration: permission(action: \"iam:scim-configuration:create\")\n canDeleteSCIMConfiguration: permission(action: \"iam:scim-configuration:delete\")\n scimConfiguration {\n id\n endpointUrl\n bridge {\n id\n }\n }\n}\n\nfragment SCIMEventListFragment on SCIMConfiguration {\n events(first: 20) {\n edges {\n node {\n id\n ...SCIMEventListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SCIMEventListItemFragment on SCIMEvent {\n id\n method\n path\n statusCode\n errorMessage\n ipAddress\n createdAt\n membership {\n id\n profile {\n fullName\n id\n }\n }\n}\n"
} }
}; };
})(); })();

View File

@@ -8,12 +8,16 @@ import {
DialogContent, DialogContent,
DialogFooter, DialogFooter,
Google, Google,
IconSettingsGear2,
Input,
useDialogRef, useDialogRef,
} from "@probo/ui"; } from "@probo/ui";
import { useState } from "react";
import { graphql, useFragment } from "react-relay"; import { graphql, useFragment } from "react-relay";
import type { GoogleWorkspaceConnectorDeleteMutation } from "#/__generated__/iam/GoogleWorkspaceConnectorDeleteMutation.graphql"; import type { GoogleWorkspaceConnectorDeleteMutation } from "#/__generated__/iam/GoogleWorkspaceConnectorDeleteMutation.graphql";
import type { GoogleWorkspaceConnectorFragment$key } from "#/__generated__/iam/GoogleWorkspaceConnectorFragment.graphql"; import type { GoogleWorkspaceConnectorFragment$key } from "#/__generated__/iam/GoogleWorkspaceConnectorFragment.graphql";
import type { GoogleWorkspaceConnectorUpdateSCIMBridgeMutation } from "#/__generated__/iam/GoogleWorkspaceConnectorUpdateSCIMBridgeMutation.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
@@ -21,6 +25,8 @@ const googleWorkspaceConnectorFragment = graphql`
fragment GoogleWorkspaceConnectorFragment on SCIMConfiguration { fragment GoogleWorkspaceConnectorFragment on SCIMConfiguration {
id id
bridge { bridge {
id
excludedUserNames
connector { connector {
id id
createdAt createdAt
@@ -39,17 +45,35 @@ const deleteSCIMConfigurationMutation = graphql`
} }
`; `;
const updateSCIMBridgeMutation = graphql`
mutation GoogleWorkspaceConnectorUpdateSCIMBridgeMutation(
$input: UpdateSCIMBridgeInput!
) {
updateSCIMBridge(input: $input) {
scimBridge {
id
excludedUserNames
}
}
}
`;
export function GoogleWorkspaceConnector(props: { export function GoogleWorkspaceConnector(props: {
fKey: GoogleWorkspaceConnectorFragment$key | null; fKey: GoogleWorkspaceConnectorFragment$key | null;
}) { }) {
const { fKey } = props; const { fKey } = props;
const data = useFragment<GoogleWorkspaceConnectorFragment$key>(googleWorkspaceConnectorFragment, fKey); const data = useFragment<GoogleWorkspaceConnectorFragment$key>(googleWorkspaceConnectorFragment, fKey);
const connector = data?.bridge?.connector; const bridge = data?.bridge;
const connector = bridge?.connector;
const scimConfigurationId = data?.id; const scimConfigurationId = data?.id;
const bridgeId = bridge?.id;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __, dateTimeFormat } = useTranslate(); const { __, dateTimeFormat } = useTranslate();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const excludedUserNamesDialogRef = useDialogRef();
const [newUser, setNewUser] = useState("");
const [deleteSCIMConfiguration, isDeleting] const [deleteSCIMConfiguration, isDeleting]
= useMutationWithToasts<GoogleWorkspaceConnectorDeleteMutation>( = useMutationWithToasts<GoogleWorkspaceConnectorDeleteMutation>(
@@ -60,6 +84,15 @@ export function GoogleWorkspaceConnector(props: {
}, },
); );
const [updateSCIMBridge, isUpdating]
= useMutationWithToasts<GoogleWorkspaceConnectorUpdateSCIMBridgeMutation>(
updateSCIMBridgeMutation,
{
successMessage: __("Excluded user names updated successfully"),
errorMessage: __("Failed to update excluded user names"),
},
);
const handleConnect = () => { const handleConnect = () => {
const baseUrl = import.meta.env.VITE_API_URL || window.location.origin; const baseUrl = import.meta.env.VITE_API_URL || window.location.origin;
const url = new URL("/api/console/v1/connectors/initiate", baseUrl); const url = new URL("/api/console/v1/connectors/initiate", baseUrl);
@@ -92,6 +125,34 @@ export function GoogleWorkspaceConnector(props: {
}); });
}; };
const currentExcludedUserNames = bridge?.excludedUserNames ? [...bridge.excludedUserNames] : [];
const saveExcludedUserNames = (newList: string[]) => {
if (!bridgeId) return;
void updateSCIMBridge({
variables: {
input: {
organizationId: organizationId,
scimBridgeId: bridgeId,
excludedUserNames: newList,
},
},
});
};
const handleAddUser = () => {
const user = newUser.trim().toLowerCase();
if (user && !currentExcludedUserNames.includes(user)) {
saveExcludedUserNames([...currentExcludedUserNames, user]);
setNewUser("");
}
};
const handleRemoveUser = (user: string) => {
saveExcludedUserNames(currentExcludedUserNames.filter(e => e !== user));
};
// Not connected state // Not connected state
if (!connector) { if (!connector) {
return ( return (
@@ -130,9 +191,75 @@ export function GoogleWorkspaceConnector(props: {
{__("Connected")} {__("Connected")}
</Badge> </Badge>
<Dialog <Dialog
ref={dialogRef} ref={excludedUserNamesDialogRef}
trigger={( trigger={(
<Button variant="secondary"> <Button variant="secondary">
<IconSettingsGear2 size={16} />
{__("Settings")}
</Button>
)}
title={__("Google Workspace Settings")}
className="max-w-lg"
>
<DialogContent padded className="space-y-6">
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium">{__("Excluded user names")}</h4>
<p className="text-sm text-txt-secondary mt-1">
{__("Users with these user names will not be synced from Google Workspace.")}
</p>
</div>
<div className="flex gap-2">
<Input
type="text"
value={newUser}
onChange={e => setNewUser(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddUser();
}
}}
placeholder="user@example.com"
className="flex-1"
/>
<Button onClick={handleAddUser} variant="secondary" disabled={isUpdating}>
{__("Add")}
</Button>
</div>
{currentExcludedUserNames.length > 0 && (
<div className="space-y-2">
{currentExcludedUserNames.map((user: string) => (
<div
key={user}
className="flex items-center justify-between p-2 bg-subtle rounded"
>
<span className="text-sm">{user}</span>
<Button
variant="quaternary"
onClick={() => handleRemoveUser(user)}
disabled={isUpdating}
>
{__("Remove")}
</Button>
</div>
))}
</div>
)}
{currentExcludedUserNames.length === 0 && (
<p className="text-sm text-txt-secondary text-center py-4">
{__("No excluded user names. All Google Workspace users will be synced.")}
</p>
)}
</div>
</DialogContent>
</Dialog>
<Dialog
ref={dialogRef}
trigger={(
<Button variant="danger">
{__("Disconnect")} {__("Disconnect")}
</Button> </Button>
)} )}
@@ -155,7 +282,9 @@ export function GoogleWorkspaceConnector(props: {
onClick={handleDisconnect} onClick={handleDisconnect}
disabled={isDeleting} disabled={isDeleting}
> >
{isDeleting ? __("Disconnecting...") : __("Disconnect")} {isDeleting
? __("Disconnecting...")
: __("Disconnect")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</Dialog> </Dialog>

View File

@@ -0,0 +1,3 @@
ALTER TABLE iam_scim_bridges ADD COLUMN excluded_user_names TEXT[] NOT NULL DEFAULT '{}';
ALTER TABLE iam_scim_bridges ALTER COLUMN excluded_user_names DROP DEFAULT;

View File

@@ -29,20 +29,21 @@ import (
type ( type (
SCIMBridge struct { SCIMBridge struct {
ID gid.GID `db:"id"` ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"` OrganizationID gid.GID `db:"organization_id"`
ScimConfigurationID gid.GID `db:"scim_configuration_id"` ScimConfigurationID gid.GID `db:"scim_configuration_id"`
ConnectorID *gid.GID `db:"connector_id"` ConnectorID *gid.GID `db:"connector_id"`
Type SCIMBridgeType `db:"type"` Type SCIMBridgeType `db:"type"`
State SCIMBridgeState `db:"state"` State SCIMBridgeState `db:"state"`
LastSyncedAt *time.Time `db:"last_synced_at"` ExcludedUserNames []string `db:"excluded_user_names"`
NextSyncAt *time.Time `db:"next_sync_at"` LastSyncedAt *time.Time `db:"last_synced_at"`
SyncError *string `db:"sync_error"` NextSyncAt *time.Time `db:"next_sync_at"`
ConsecutiveFailures int `db:"consecutive_failures"` SyncError *string `db:"sync_error"`
TotalSyncCount int `db:"total_sync_count"` ConsecutiveFailures int `db:"consecutive_failures"`
TotalFailureCount int `db:"total_failure_count"` TotalSyncCount int `db:"total_sync_count"`
CreatedAt time.Time `db:"created_at"` TotalFailureCount int `db:"total_failure_count"`
UpdatedAt time.Time `db:"updated_at"` CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
} }
SCIMBridges []*SCIMBridge SCIMBridges []*SCIMBridge
@@ -87,6 +88,7 @@ SELECT
connector_id, connector_id,
type, type,
state, state,
excluded_user_names,
last_synced_at, last_synced_at,
next_sync_at, next_sync_at,
sync_error, sync_error,
@@ -141,6 +143,7 @@ SELECT
connector_id, connector_id,
type, type,
state, state,
excluded_user_names,
last_synced_at, last_synced_at,
next_sync_at, next_sync_at,
sync_error, sync_error,
@@ -195,6 +198,7 @@ SELECT
connector_id, connector_id,
type, type,
state, state,
excluded_user_names,
last_synced_at, last_synced_at,
next_sync_at, next_sync_at,
sync_error, sync_error,
@@ -249,6 +253,7 @@ INSERT INTO iam_scim_bridges (
connector_id, connector_id,
type, type,
state, state,
excluded_user_names,
last_synced_at, last_synced_at,
next_sync_at, next_sync_at,
sync_error, sync_error,
@@ -265,6 +270,7 @@ INSERT INTO iam_scim_bridges (
@connector_id, @connector_id,
@type, @type,
@state, @state,
@excluded_user_names,
@last_synced_at, @last_synced_at,
@next_sync_at, @next_sync_at,
@sync_error, @sync_error,
@@ -284,6 +290,7 @@ INSERT INTO iam_scim_bridges (
"connector_id": s.ConnectorID, "connector_id": s.ConnectorID,
"type": s.Type, "type": s.Type,
"state": s.State, "state": s.State,
"excluded_user_names": s.ExcludedUserNames,
"last_synced_at": s.LastSyncedAt, "last_synced_at": s.LastSyncedAt,
"next_sync_at": s.NextSyncAt, "next_sync_at": s.NextSyncAt,
"sync_error": s.SyncError, "sync_error": s.SyncError,
@@ -312,6 +319,7 @@ UPDATE iam_scim_bridges
SET SET
connector_id = @connector_id, connector_id = @connector_id,
state = @state, state = @state,
excluded_user_names = @excluded_user_names,
last_synced_at = @last_synced_at, last_synced_at = @last_synced_at,
next_sync_at = @next_sync_at, next_sync_at = @next_sync_at,
sync_error = @sync_error, sync_error = @sync_error,
@@ -330,6 +338,7 @@ WHERE
"id": s.ID, "id": s.ID,
"connector_id": s.ConnectorID, "connector_id": s.ConnectorID,
"state": s.State, "state": s.State,
"excluded_user_names": s.ExcludedUserNames,
"last_synced_at": s.LastSyncedAt, "last_synced_at": s.LastSyncedAt,
"next_sync_at": s.NextSyncAt, "next_sync_at": s.NextSyncAt,
"sync_error": s.SyncError, "sync_error": s.SyncError,
@@ -364,6 +373,7 @@ SELECT
connector_id, connector_id,
type, type,
state, state,
excluded_user_names,
last_synced_at, last_synced_at,
next_sync_at, next_sync_at,
sync_error, sync_error,

View File

@@ -81,6 +81,7 @@ const (
// SCIM Bridge actions // SCIM Bridge actions
ActionSCIMBridgeGet = "iam:scim-bridge:get" ActionSCIMBridgeGet = "iam:scim-bridge:get"
ActionSCIMBridgeCreate = "iam:scim-bridge:create" ActionSCIMBridgeCreate = "iam:scim-bridge:create"
ActionSCIMBridgeUpdate = "iam:scim-bridge:update"
ActionSCIMBridgeDelete = "iam:scim-bridge:delete" ActionSCIMBridgeDelete = "iam:scim-bridge:delete"
// Connector actions // Connector actions

View File

@@ -180,6 +180,11 @@ var IAMOwnerPolicy = policy.NewPolicy(
policy.Allow("iam:scim-event:*"). policy.Allow("iam:scim-event:*").
WithSID("full-scim-event-access"). WithSID("full-scim-event-access").
When(policy.Equals("principal.organization_id", "resource.organization_id")), When(policy.Equals("principal.organization_id", "resource.organization_id")),
// Allow updating SCIM bridge settings (scoped to own organization)
policy.Allow(ActionSCIMBridgeUpdate).
WithSID("scim-bridge-update-access").
When(policy.Equals("principal.organization_id", "resource.organization_id")),
). ).
WithDescription("Full IAM access for organization owners") WithDescription("Full IAM access for organization owners")

View File

@@ -1434,6 +1434,50 @@ func (s OrganizationService) RegenerateSCIMToken(
return config, token, nil return config, token, nil
} }
func (s OrganizationService) UpdateSCIMBridge(
ctx context.Context,
organizationID gid.GID,
bridgeID gid.GID,
excludedUserNames []string,
) (*coredata.SCIMBridge, error) {
bridge := &coredata.SCIMBridge{}
scope := coredata.NewScopeFromObjectID(bridgeID)
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
err := bridge.LoadByID(ctx, tx, scope, bridgeID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return fmt.Errorf("SCIM bridge not found")
}
return fmt.Errorf("cannot load SCIM bridge: %w", err)
}
if bridge.OrganizationID != organizationID {
return fmt.Errorf("SCIM bridge not found")
}
bridge.ExcludedUserNames = excludedUserNames
bridge.UpdatedAt = time.Now()
err = bridge.Update(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot update SCIM bridge: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return bridge, nil
}
func (s OrganizationService) ListSCIMEventsByConfigID( func (s OrganizationService) ListSCIMEventsByConfigID(
ctx context.Context, ctx context.Context,
scimConfigurationID gid.GID, scimConfigurationID gid.GID,
@@ -1863,6 +1907,7 @@ func (s OrganizationService) CreateSCIMBridge(
ConnectorID: &connectorID, ConnectorID: &connectorID,
Type: bridgeType, Type: bridgeType,
State: coredata.SCIMBridgeStateActive, // Active immediately since connector already exists State: coredata.SCIMBridgeStateActive, // Active immediately since connector already exists
ExcludedUserNames: []string{},
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }

View File

@@ -28,10 +28,11 @@ import (
type ( type (
Bridge struct { Bridge struct {
provider provider.Provider provider provider.Provider
scimClient *scimclient.Client scimClient *scimclient.Client
forceUpdate bool excludedUserNames []string
dryRun bool forceUpdate bool
dryRun bool
} }
Option func(*Bridge) Option func(*Bridge)
@@ -49,6 +50,12 @@ func WithForceUpdate(forceUpdate bool) Option {
} }
} }
func WithExcludedUserNames(excludedUserNames []string) Option {
return func(s *Bridge) {
s.excludedUserNames = excludedUserNames
}
}
func NewBridge(provider provider.Provider, scimClient *scimclient.Client, opts ...Option) *Bridge { func NewBridge(provider provider.Provider, scimClient *scimclient.Client, opts ...Option) *Bridge {
s := &Bridge{ s := &Bridge{
provider: provider, provider: provider,
@@ -62,15 +69,15 @@ func NewBridge(provider provider.Provider, scimClient *scimclient.Client, opts .
return s return s
} }
func (s *Bridge) Run(ctx context.Context) (created, updated, deactivated, skipped int, err error) { func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivated, skipped int, err error) {
providerUsers, err := s.provider.ListUsers(ctx) providerUsers, err := s.provider.ListUsers(ctx)
if err != nil { if err != nil {
return 0, 0, 0, 0, fmt.Errorf("cannot list provider users: %w", err) return 0, 0, 0, 0, 0, fmt.Errorf("cannot list provider users: %w", err)
} }
scimUsers, err := s.scimClient.ListUsers(ctx) scimUsers, err := s.scimClient.ListUsers(ctx)
if err != nil { if err != nil {
return 0, 0, 0, 0, fmt.Errorf("cannot list scim users: %w", err) return 0, 0, 0, 0, 0, fmt.Errorf("cannot list scim users: %w", err)
} }
scimUsersByEmail := make(map[string]*scimclient.User) scimUsersByEmail := make(map[string]*scimclient.User)
@@ -125,6 +132,17 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deactivated, skippe
continue continue
} }
if s.isExcluded(email) {
if !s.dryRun {
if err := s.scimClient.DeleteUser(ctx, scimUser.ID); err != nil {
errs = append(errs, fmt.Errorf("cannot delete user %q: %w", email, err))
continue
}
}
deleted++
continue
}
if !scimUser.Active { if !scimUser.Active {
continue continue
} }
@@ -138,5 +156,14 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deactivated, skippe
deactivated++ deactivated++
} }
return created, updated, deactivated, skipped, errors.Join(errs...) return created, updated, deleted, deactivated, skipped, errors.Join(errs...)
}
func (s *Bridge) isExcluded(email string) bool {
for _, excluded := range s.excludedUserNames {
if strings.EqualFold(excluded, email) {
return true
}
}
return false
} }

View File

@@ -248,6 +248,29 @@ func (c *Client) DeactivateUser(ctx context.Context, userID string) error {
return nil return nil
} }
func (c *Client) DeleteUser(ctx context.Context, userID string) error {
reqURL := fmt.Sprintf("%s/Users/%s", c.endpoint, url.PathEscape(userID))
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, reqURL, nil)
if err != nil {
return fmt.Errorf("cannot create request: %w", err)
}
c.setHeaders(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("cannot delete user: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("SCIM API error: status %d, body: %s", resp.StatusCode, string(respBody))
}
return nil
}
func (c *Client) setHeaders(req *http.Request) { func (c *Client) setHeaders(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Accept", "application/scim+json") req.Header.Set("Accept", "application/scim+json")

View File

@@ -20,6 +20,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"strings"
admin "google.golang.org/api/admin/directory/v1" admin "google.golang.org/api/admin/directory/v1"
"google.golang.org/api/option" "google.golang.org/api/option"
@@ -31,12 +32,14 @@ import (
var _ provider.Provider = (*Provider)(nil) var _ provider.Provider = (*Provider)(nil)
type Provider struct { type Provider struct {
httpClient *http.Client httpClient *http.Client
excludedUserNames []string
} }
func New(httpClient *http.Client) *Provider { func New(httpClient *http.Client, excludedUserNames []string) *Provider {
return &Provider{ return &Provider{
httpClient: httpClient, httpClient: httpClient,
excludedUserNames: excludedUserNames,
} }
} }
@@ -44,6 +47,16 @@ func (p *Provider) Name() string {
return "google-workspace" return "google-workspace"
} }
func (p *Provider) isExcluded(email string) bool {
emailLower := strings.ToLower(email)
for _, excluded := range p.excludedUserNames {
if strings.ToLower(excluded) == emailLower {
return true
}
}
return false
}
func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) {
adminService, err := admin.NewService(ctx, option.WithHTTPClient(p.httpClient)) adminService, err := admin.NewService(ctx, option.WithHTTPClient(p.httpClient))
if err != nil { if err != nil {
@@ -65,6 +78,10 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) {
} }
for _, u := range resp.Users { for _, u := range resp.Users {
if p.isExcluded(u.PrimaryEmail) {
continue
}
allUsers = append( allUsers = append(
allUsers, allUsers,
scimclient.User{ scimclient.User{

View File

@@ -27,6 +27,7 @@ import (
type SyncStats struct { type SyncStats struct {
Created int Created int
Updated int Updated int
Deleted int
Deactivated int Deactivated int
Skipped int Skipped int
} }
@@ -104,6 +105,7 @@ func (r *BridgeRunner) transitionToSuccess(
log.Duration("sync_duration", duration), log.Duration("sync_duration", duration),
log.Int("users_created", stats.Created), log.Int("users_created", stats.Created),
log.Int("users_updated", stats.Updated), log.Int("users_updated", stats.Updated),
log.Int("users_deleted", stats.Deleted),
log.Int("users_deactivated", stats.Deactivated), log.Int("users_deactivated", stats.Deactivated),
log.Int("users_skipped", stats.Skipped), log.Int("users_skipped", stats.Skipped),
) )

View File

@@ -67,7 +67,7 @@ func (r *BridgeRunner) doSync(
return SyncStats{}, nil, fmt.Errorf("cannot load connector: %w", err) return SyncStats{}, nil, fmt.Errorf("cannot load connector: %w", err)
} }
idp, err := r.createProvider(ctx, logger, scimBridge.Type, dbConnector) idp, err := r.createProvider(ctx, logger, scimBridge.Type, dbConnector, scimBridge.ExcludedUserNames)
if err != nil { if err != nil {
return SyncStats{}, nil, fmt.Errorf("cannot create provider: %w", err) return SyncStats{}, nil, fmt.Errorf("cannot create provider: %w", err)
} }
@@ -89,8 +89,8 @@ func (r *BridgeRunner) doSync(
} }
scimClient := r.createSCIMClient(logger, token) scimClient := r.createSCIMClient(logger, token)
syncer := bridge.NewBridge(idp, scimClient) syncer := bridge.NewBridge(idp, scimClient, bridge.WithExcludedUserNames(scimBridge.ExcludedUserNames))
created, updated, deactivated, skipped, err := syncer.Run(ctx) created, updated, deleted, deactivated, skipped, err := syncer.Run(ctx)
if err != nil { if err != nil {
return SyncStats{}, nil, fmt.Errorf("sync failed: %w", err) return SyncStats{}, nil, fmt.Errorf("sync failed: %w", err)
} }
@@ -98,6 +98,7 @@ func (r *BridgeRunner) doSync(
stats := SyncStats{ stats := SyncStats{
Created: created, Created: created,
Updated: updated, Updated: updated,
Deleted: deleted,
Deactivated: deactivated, Deactivated: deactivated,
Skipped: skipped, Skipped: skipped,
} }
@@ -121,10 +122,11 @@ func (r *BridgeRunner) createProvider(
logger *log.Logger, logger *log.Logger,
bridgeType coredata.SCIMBridgeType, bridgeType coredata.SCIMBridgeType,
dbConnector *coredata.Connector, dbConnector *coredata.Connector,
excludedUserNames []string,
) (provider.Provider, error) { ) (provider.Provider, error) {
switch bridgeType { switch bridgeType {
case coredata.SCIMBridgeTypeGoogleWorkspace: case coredata.SCIMBridgeTypeGoogleWorkspace:
return r.createGoogleWorkspaceProvider(ctx, logger, dbConnector) return r.createGoogleWorkspaceProvider(ctx, logger, dbConnector, excludedUserNames)
default: default:
return nil, fmt.Errorf("unsupported bridge type: %s", bridgeType) return nil, fmt.Errorf("unsupported bridge type: %s", bridgeType)
} }
@@ -134,6 +136,7 @@ func (r *BridgeRunner) createGoogleWorkspaceProvider(
ctx context.Context, ctx context.Context,
logger *log.Logger, logger *log.Logger,
dbConnector *coredata.Connector, dbConnector *coredata.Connector,
excludedUserNames []string,
) (provider.Provider, error) { ) (provider.Provider, error) {
if dbConnector.Connection == nil { if dbConnector.Connection == nil {
return nil, fmt.Errorf("connector has no connection configured") return nil, fmt.Errorf("connector has no connection configured")
@@ -161,7 +164,7 @@ func (r *BridgeRunner) createGoogleWorkspaceProvider(
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot create HTTP client: %w", err) return nil, fmt.Errorf("cannot create HTTP client: %w", err)
} }
return googleworkspace.New(httpClient), nil return googleworkspace.New(httpClient, excludedUserNames), nil
} }
httpClient, err := oauth2Conn.RefreshableClient(ctx, *refreshCfg, httpClientOpts...) httpClient, err := oauth2Conn.RefreshableClient(ctx, *refreshCfg, httpClientOpts...)
@@ -169,5 +172,5 @@ func (r *BridgeRunner) createGoogleWorkspaceProvider(
return nil, fmt.Errorf("cannot create refreshable HTTP client: %w", err) return nil, fmt.Errorf("cannot create refreshable HTTP client: %w", err)
} }
return googleworkspace.New(httpClient), nil return googleworkspace.New(httpClient, excludedUserNames), nil
} }

View File

@@ -124,6 +124,9 @@ type Mutation {
regenerateSCIMToken( regenerateSCIMToken(
input: RegenerateSCIMTokenInput! input: RegenerateSCIMTokenInput!
): RegenerateSCIMTokenPayload @session(required: PRESENT) ): RegenerateSCIMTokenPayload @session(required: PRESENT)
updateSCIMBridge(
input: UpdateSCIMBridgeInput!
): UpdateSCIMBridgePayload @session(required: PRESENT)
} }
type Identity implements Node { type Identity implements Node {
@@ -368,6 +371,7 @@ type SCIMBridge implements Node {
scimConfiguration: SCIMConfiguration @goField(forceResolver: true) scimConfiguration: SCIMConfiguration @goField(forceResolver: true)
connector: Connector @goField(forceResolver: true) connector: Connector @goField(forceResolver: true)
type: SCIMBridgeType! type: SCIMBridgeType!
excludedUserNames: [String!]!
createdAt: Datetime! createdAt: Datetime!
updatedAt: Datetime! updatedAt: Datetime!
@@ -905,6 +909,12 @@ input RegenerateSCIMTokenInput {
scimConfigurationId: ID! scimConfigurationId: ID!
} }
input UpdateSCIMBridgeInput {
organizationId: ID!
scimBridgeId: ID!
excludedUserNames: [String!]!
}
type CreateSCIMConfigurationPayload { type CreateSCIMConfigurationPayload {
scimConfiguration: SCIMConfiguration! scimConfiguration: SCIMConfiguration!
scimBridge: SCIMBridge scimBridge: SCIMBridge
@@ -919,3 +929,7 @@ type RegenerateSCIMTokenPayload {
scimConfiguration: SCIMConfiguration! scimConfiguration: SCIMConfiguration!
token: String! token: String!
} }
type UpdateSCIMBridgePayload {
scimBridge: SCIMBridge!
}

View File

@@ -246,6 +246,7 @@ type ComplexityRoot struct {
UpdateMembership func(childComplexity int, input types.UpdateMembershipInput) int UpdateMembership func(childComplexity int, input types.UpdateMembershipInput) int
UpdateOrganization func(childComplexity int, input types.UpdateOrganizationInput) int UpdateOrganization func(childComplexity int, input types.UpdateOrganizationInput) int
UpdateSAMLConfiguration func(childComplexity int, input types.UpdateSAMLConfigurationInput) int UpdateSAMLConfiguration func(childComplexity int, input types.UpdateSAMLConfigurationInput) int
UpdateSCIMBridge func(childComplexity int, input types.UpdateSCIMBridgeInput) int
VerifyEmail func(childComplexity int, input types.VerifyEmailInput) int VerifyEmail func(childComplexity int, input types.VerifyEmailInput) int
} }
@@ -379,6 +380,7 @@ type ComplexityRoot struct {
SCIMBridge struct { SCIMBridge struct {
Connector func(childComplexity int) int Connector func(childComplexity int) int
CreatedAt func(childComplexity int) int CreatedAt func(childComplexity int) int
ExcludedUserNames func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
Permission func(childComplexity int, action string) int Permission func(childComplexity int, action string) int
ScimConfiguration func(childComplexity int) int ScimConfiguration func(childComplexity int) int
@@ -474,6 +476,10 @@ type ComplexityRoot struct {
SamlConfiguration func(childComplexity int) int SamlConfiguration func(childComplexity int) int
} }
UpdateSCIMBridgePayload struct {
ScimBridge func(childComplexity int) int
}
VerifyEmailPayload struct { VerifyEmailPayload struct {
Success func(childComplexity int) int Success func(childComplexity int) int
} }
@@ -540,6 +546,7 @@ type MutationResolver interface {
CreateSCIMConfiguration(ctx context.Context, input types.CreateSCIMConfigurationInput) (*types.CreateSCIMConfigurationPayload, error) CreateSCIMConfiguration(ctx context.Context, input types.CreateSCIMConfigurationInput) (*types.CreateSCIMConfigurationPayload, error)
DeleteSCIMConfiguration(ctx context.Context, input types.DeleteSCIMConfigurationInput) (*types.DeleteSCIMConfigurationPayload, error) DeleteSCIMConfiguration(ctx context.Context, input types.DeleteSCIMConfigurationInput) (*types.DeleteSCIMConfigurationPayload, error)
RegenerateSCIMToken(ctx context.Context, input types.RegenerateSCIMTokenInput) (*types.RegenerateSCIMTokenPayload, error) RegenerateSCIMToken(ctx context.Context, input types.RegenerateSCIMTokenInput) (*types.RegenerateSCIMTokenPayload, error)
UpdateSCIMBridge(ctx context.Context, input types.UpdateSCIMBridgeInput) (*types.UpdateSCIMBridgePayload, error)
} }
type OrganizationResolver interface { type OrganizationResolver interface {
LogoURL(ctx context.Context, obj *types.Organization) (*string, error) LogoURL(ctx context.Context, obj *types.Organization) (*string, error)
@@ -1415,6 +1422,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
} }
return e.complexity.Mutation.UpdateSAMLConfiguration(childComplexity, args["input"].(types.UpdateSAMLConfigurationInput)), true return e.complexity.Mutation.UpdateSAMLConfiguration(childComplexity, args["input"].(types.UpdateSAMLConfigurationInput)), true
case "Mutation.updateSCIMBridge":
if e.complexity.Mutation.UpdateSCIMBridge == nil {
break
}
args, err := ec.field_Mutation_updateSCIMBridge_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UpdateSCIMBridge(childComplexity, args["input"].(types.UpdateSCIMBridgeInput)), true
case "Mutation.verifyEmail": case "Mutation.verifyEmail":
if e.complexity.Mutation.VerifyEmail == nil { if e.complexity.Mutation.VerifyEmail == nil {
break break
@@ -1918,6 +1936,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
} }
return e.complexity.SCIMBridge.CreatedAt(childComplexity), true return e.complexity.SCIMBridge.CreatedAt(childComplexity), true
case "SCIMBridge.excludedUserNames":
if e.complexity.SCIMBridge.ExcludedUserNames == nil {
break
}
return e.complexity.SCIMBridge.ExcludedUserNames(childComplexity), true
case "SCIMBridge.id": case "SCIMBridge.id":
if e.complexity.SCIMBridge.ID == nil { if e.complexity.SCIMBridge.ID == nil {
break break
@@ -2264,6 +2288,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.UpdateSAMLConfigurationPayload.SamlConfiguration(childComplexity), true return e.complexity.UpdateSAMLConfigurationPayload.SamlConfiguration(childComplexity), true
case "UpdateSCIMBridgePayload.scimBridge":
if e.complexity.UpdateSCIMBridgePayload.ScimBridge == nil {
break
}
return e.complexity.UpdateSCIMBridgePayload.ScimBridge(childComplexity), true
case "VerifyEmailPayload.success": case "VerifyEmailPayload.success":
if e.complexity.VerifyEmailPayload.Success == nil { if e.complexity.VerifyEmailPayload.Success == nil {
break break
@@ -2310,6 +2341,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputUpdateMembershipInput, ec.unmarshalInputUpdateMembershipInput,
ec.unmarshalInputUpdateOrganizationInput, ec.unmarshalInputUpdateOrganizationInput,
ec.unmarshalInputUpdateSAMLConfigurationInput, ec.unmarshalInputUpdateSAMLConfigurationInput,
ec.unmarshalInputUpdateSCIMBridgeInput,
ec.unmarshalInputVerifyEmailInput, ec.unmarshalInputVerifyEmailInput,
) )
first := true first := true
@@ -2534,6 +2566,9 @@ type Mutation {
regenerateSCIMToken( regenerateSCIMToken(
input: RegenerateSCIMTokenInput! input: RegenerateSCIMTokenInput!
): RegenerateSCIMTokenPayload @session(required: PRESENT) ): RegenerateSCIMTokenPayload @session(required: PRESENT)
updateSCIMBridge(
input: UpdateSCIMBridgeInput!
): UpdateSCIMBridgePayload @session(required: PRESENT)
} }
type Identity implements Node { type Identity implements Node {
@@ -2778,6 +2813,7 @@ type SCIMBridge implements Node {
scimConfiguration: SCIMConfiguration @goField(forceResolver: true) scimConfiguration: SCIMConfiguration @goField(forceResolver: true)
connector: Connector @goField(forceResolver: true) connector: Connector @goField(forceResolver: true)
type: SCIMBridgeType! type: SCIMBridgeType!
excludedUserNames: [String!]!
createdAt: Datetime! createdAt: Datetime!
updatedAt: Datetime! updatedAt: Datetime!
@@ -3315,6 +3351,12 @@ input RegenerateSCIMTokenInput {
scimConfigurationId: ID! scimConfigurationId: ID!
} }
input UpdateSCIMBridgeInput {
organizationId: ID!
scimBridgeId: ID!
excludedUserNames: [String!]!
}
type CreateSCIMConfigurationPayload { type CreateSCIMConfigurationPayload {
scimConfiguration: SCIMConfiguration! scimConfiguration: SCIMConfiguration!
scimBridge: SCIMBridge scimBridge: SCIMBridge
@@ -3329,6 +3371,10 @@ type RegenerateSCIMTokenPayload {
scimConfiguration: SCIMConfiguration! scimConfiguration: SCIMConfiguration!
token: String! token: String!
} }
type UpdateSCIMBridgePayload {
scimBridge: SCIMBridge!
}
`, BuiltIn: false}, `, BuiltIn: false},
{Name: "../../../../gqlutils/directives/session/schema.graphql", Input: `# Session directive for GraphQL APIs {Name: "../../../../gqlutils/directives/session/schema.graphql", Input: `# Session directive for GraphQL APIs
# Include this schema in your gqlgen configuration to enable session-based access control. # Include this schema in your gqlgen configuration to enable session-based access control.
@@ -3847,6 +3893,17 @@ func (ec *executionContext) field_Mutation_updateSAMLConfiguration_args(ctx cont
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_updateSCIMBridge_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNUpdateSCIMBridgeInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateSCIMBridgeInput)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_verifyEmail_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Mutation_verifyEmail_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@@ -4770,6 +4827,8 @@ func (ec *executionContext) fieldContext_CreateSCIMConfigurationPayload_scimBrid
return ec.fieldContext_SCIMBridge_connector(ctx, field) return ec.fieldContext_SCIMBridge_connector(ctx, field)
case "type": case "type":
return ec.fieldContext_SCIMBridge_type(ctx, field) return ec.fieldContext_SCIMBridge_type(ctx, field)
case "excludedUserNames":
return ec.fieldContext_SCIMBridge_excludedUserNames(ctx, field)
case "createdAt": case "createdAt":
return ec.fieldContext_SCIMBridge_createdAt(ctx, field) return ec.fieldContext_SCIMBridge_createdAt(ctx, field)
case "updatedAt": case "updatedAt":
@@ -8586,6 +8645,69 @@ func (ec *executionContext) fieldContext_Mutation_regenerateSCIMToken(ctx contex
return fc, nil return fc, nil
} }
func (ec *executionContext) _Mutation_updateSCIMBridge(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Mutation_updateSCIMBridge,
func(ctx context.Context) (any, error) {
fc := graphql.GetFieldContext(ctx)
return ec.resolvers.Mutation().UpdateSCIMBridge(ctx, fc.Args["input"].(types.UpdateSCIMBridgeInput))
},
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
directive0 := next
directive1 := func(ctx context.Context) (any, error) {
required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "PRESENT")
if err != nil {
var zeroVal *types.UpdateSCIMBridgePayload
return zeroVal, err
}
if ec.directives.Session == nil {
var zeroVal *types.UpdateSCIMBridgePayload
return zeroVal, errors.New("directive session is not implemented")
}
return ec.directives.Session(ctx, nil, directive0, required)
}
next = directive1
return next
},
ec.marshalOUpdateSCIMBridgePayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateSCIMBridgePayload,
true,
false,
)
}
func (ec *executionContext) fieldContext_Mutation_updateSCIMBridge(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "scimBridge":
return ec.fieldContext_UpdateSCIMBridgePayload_scimBridge(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type UpdateSCIMBridgePayload", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_updateSCIMBridge_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Organization_id(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { func (ec *executionContext) _Organization_id(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -11337,6 +11459,35 @@ func (ec *executionContext) fieldContext_SCIMBridge_type(_ context.Context, fiel
return fc, nil return fc, nil
} }
func (ec *executionContext) _SCIMBridge_excludedUserNames(ctx context.Context, field graphql.CollectedField, obj *types.SCIMBridge) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_SCIMBridge_excludedUserNames,
func(ctx context.Context) (any, error) {
return obj.ExcludedUserNames, nil
},
nil,
ec.marshalNString2ᚕstringᚄ,
true,
true,
)
}
func (ec *executionContext) fieldContext_SCIMBridge_excludedUserNames(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "SCIMBridge",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _SCIMBridge_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.SCIMBridge) (ret graphql.Marshaler) { func (ec *executionContext) _SCIMBridge_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.SCIMBridge) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -11667,6 +11818,8 @@ func (ec *executionContext) fieldContext_SCIMConfiguration_bridge(_ context.Cont
return ec.fieldContext_SCIMBridge_connector(ctx, field) return ec.fieldContext_SCIMBridge_connector(ctx, field)
case "type": case "type":
return ec.fieldContext_SCIMBridge_type(ctx, field) return ec.fieldContext_SCIMBridge_type(ctx, field)
case "excludedUserNames":
return ec.fieldContext_SCIMBridge_excludedUserNames(ctx, field)
case "createdAt": case "createdAt":
return ec.fieldContext_SCIMBridge_createdAt(ctx, field) return ec.fieldContext_SCIMBridge_createdAt(ctx, field)
case "updatedAt": case "updatedAt":
@@ -13217,6 +13370,55 @@ func (ec *executionContext) fieldContext_UpdateSAMLConfigurationPayload_samlConf
return fc, nil return fc, nil
} }
func (ec *executionContext) _UpdateSCIMBridgePayload_scimBridge(ctx context.Context, field graphql.CollectedField, obj *types.UpdateSCIMBridgePayload) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_UpdateSCIMBridgePayload_scimBridge,
func(ctx context.Context) (any, error) {
return obj.ScimBridge, nil
},
nil,
ec.marshalNSCIMBridge2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSCIMBridge,
true,
true,
)
}
func (ec *executionContext) fieldContext_UpdateSCIMBridgePayload_scimBridge(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UpdateSCIMBridgePayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_SCIMBridge_id(ctx, field)
case "state":
return ec.fieldContext_SCIMBridge_state(ctx, field)
case "scimConfiguration":
return ec.fieldContext_SCIMBridge_scimConfiguration(ctx, field)
case "connector":
return ec.fieldContext_SCIMBridge_connector(ctx, field)
case "type":
return ec.fieldContext_SCIMBridge_type(ctx, field)
case "excludedUserNames":
return ec.fieldContext_SCIMBridge_excludedUserNames(ctx, field)
case "createdAt":
return ec.fieldContext_SCIMBridge_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_SCIMBridge_updatedAt(ctx, field)
case "permission":
return ec.fieldContext_SCIMBridge_permission(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type SCIMBridge", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _VerifyEmailPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.VerifyEmailPayload) (ret graphql.Marshaler) { func (ec *executionContext) _VerifyEmailPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.VerifyEmailPayload) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -15872,6 +16074,47 @@ func (ec *executionContext) unmarshalInputUpdateSAMLConfigurationInput(ctx conte
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputUpdateSCIMBridgeInput(ctx context.Context, obj any) (types.UpdateSCIMBridgeInput, error) {
var it types.UpdateSCIMBridgeInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"organizationId", "scimBridgeId", "excludedUserNames"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "organizationId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId"))
data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.OrganizationID = data
case "scimBridgeId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("scimBridgeId"))
data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.ScimBridgeID = data
case "excludedUserNames":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("excludedUserNames"))
data, err := ec.unmarshalNString2ᚕstringᚄ(ctx, v)
if err != nil {
return it, err
}
it.ExcludedUserNames = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputVerifyEmailInput(ctx context.Context, obj any) (types.VerifyEmailInput, error) { func (ec *executionContext) unmarshalInputVerifyEmailInput(ctx context.Context, obj any) (types.VerifyEmailInput, error) {
var it types.VerifyEmailInput var it types.VerifyEmailInput
asMap := map[string]any{} asMap := map[string]any{}
@@ -17804,6 +18047,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_regenerateSCIMToken(ctx, field) return ec._Mutation_regenerateSCIMToken(ctx, field)
}) })
case "updateSCIMBridge":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_updateSCIMBridge(ctx, field)
})
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@@ -19350,6 +19597,11 @@ func (ec *executionContext) _SCIMBridge(ctx context.Context, sel ast.SelectionSe
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1) atomic.AddUint32(&out.Invalids, 1)
} }
case "excludedUserNames":
out.Values[i] = ec._SCIMBridge_excludedUserNames(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "createdAt": case "createdAt":
out.Values[i] = ec._SCIMBridge_createdAt(ctx, field, obj) out.Values[i] = ec._SCIMBridge_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@@ -20413,6 +20665,45 @@ func (ec *executionContext) _UpdateSAMLConfigurationPayload(ctx context.Context,
return out return out
} }
var updateSCIMBridgePayloadImplementors = []string{"UpdateSCIMBridgePayload"}
func (ec *executionContext) _UpdateSCIMBridgePayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateSCIMBridgePayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, updateSCIMBridgePayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("UpdateSCIMBridgePayload")
case "scimBridge":
out.Values[i] = ec._UpdateSCIMBridgePayload_scimBridge(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var verifyEmailPayloadImplementors = []string{"VerifyEmailPayload"} var verifyEmailPayloadImplementors = []string{"VerifyEmailPayload"}
func (ec *executionContext) _VerifyEmailPayload(ctx context.Context, sel ast.SelectionSet, obj *types.VerifyEmailPayload) graphql.Marshaler { func (ec *executionContext) _VerifyEmailPayload(ctx context.Context, sel ast.SelectionSet, obj *types.VerifyEmailPayload) graphql.Marshaler {
@@ -21575,6 +21866,16 @@ var (
} }
) )
func (ec *executionContext) marshalNSCIMBridge2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSCIMBridge(ctx context.Context, sel ast.SelectionSet, v *types.SCIMBridge) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._SCIMBridge(ctx, sel, v)
}
func (ec *executionContext) unmarshalNSCIMBridgeState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐSCIMBridgeState(ctx context.Context, v any) (coredata.SCIMBridgeState, error) { func (ec *executionContext) unmarshalNSCIMBridgeState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐSCIMBridgeState(ctx context.Context, v any) (coredata.SCIMBridgeState, error) {
tmp, err := graphql.UnmarshalString(v) tmp, err := graphql.UnmarshalString(v)
res := unmarshalNSCIMBridgeState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐSCIMBridgeState[tmp] res := unmarshalNSCIMBridgeState2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐSCIMBridgeState[tmp]
@@ -21886,6 +22187,36 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S
return res return res
} }
func (ec *executionContext) unmarshalNString2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) {
var vSlice []any
vSlice = graphql.CoerceList(v)
var err error
res := make([]string, len(vSlice))
for i := range vSlice {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i))
res[i], err = ec.unmarshalNString2string(ctx, vSlice[i])
if err != nil {
return nil, err
}
}
return res, nil
}
func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler {
ret := make(graphql.Array, len(v))
for i := range v {
ret[i] = ec.marshalNString2string(ctx, sel, v[i])
}
for _, e := range ret {
if e == graphql.Null {
return graphql.Null
}
}
return ret
}
func (ec *executionContext) unmarshalNUpdateMembershipInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateMembershipInput(ctx context.Context, v any) (types.UpdateMembershipInput, error) { func (ec *executionContext) unmarshalNUpdateMembershipInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateMembershipInput(ctx context.Context, v any) (types.UpdateMembershipInput, error) {
res, err := ec.unmarshalInputUpdateMembershipInput(ctx, v) res, err := ec.unmarshalInputUpdateMembershipInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
@@ -21915,6 +22246,11 @@ func (ec *executionContext) unmarshalNUpdateSAMLConfigurationInput2goᚗproboᚗ
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
} }
func (ec *executionContext) unmarshalNUpdateSCIMBridgeInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateSCIMBridgeInput(ctx context.Context, v any) (types.UpdateSCIMBridgeInput, error) {
res, err := ec.unmarshalInputUpdateSCIMBridgeInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalNVerifyEmailInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐVerifyEmailInput(ctx context.Context, v any) (types.VerifyEmailInput, error) { func (ec *executionContext) unmarshalNVerifyEmailInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐVerifyEmailInput(ctx context.Context, v any) (types.VerifyEmailInput, error) {
res, err := ec.unmarshalInputVerifyEmailInput(ctx, v) res, err := ec.unmarshalInputVerifyEmailInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
@@ -22695,6 +23031,13 @@ func (ec *executionContext) marshalOUpdateSAMLConfigurationPayload2ᚖgoᚗprobo
return ec._UpdateSAMLConfigurationPayload(ctx, sel, v) return ec._UpdateSAMLConfigurationPayload(ctx, sel, v)
} }
func (ec *executionContext) marshalOUpdateSCIMBridgePayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateSCIMBridgePayload(ctx context.Context, sel ast.SelectionSet, v *types.UpdateSCIMBridgePayload) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._UpdateSCIMBridgePayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalOUpload2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v any) (*graphql.Upload, error) { func (ec *executionContext) unmarshalOUpload2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v any) (*graphql.Upload, error) {
if v == nil { if v == nil {
return nil, nil return nil, nil

View File

@@ -30,9 +30,10 @@ func NewSCIMBridge(bridge *coredata.SCIMBridge) *SCIMBridge {
ScimConfiguration: &SCIMConfiguration{ ScimConfiguration: &SCIMConfiguration{
ID: bridge.ScimConfigurationID, ID: bridge.ScimConfigurationID,
}, },
Connector: connector, Connector: connector,
Type: bridge.Type, Type: bridge.Type,
CreatedAt: bridge.CreatedAt, ExcludedUserNames: bridge.ExcludedUserNames,
UpdatedAt: bridge.UpdatedAt, CreatedAt: bridge.CreatedAt,
UpdatedAt: bridge.UpdatedAt,
} }
} }

View File

@@ -415,6 +415,7 @@ type SCIMBridge struct {
ScimConfiguration *SCIMConfiguration `json:"scimConfiguration,omitempty"` ScimConfiguration *SCIMConfiguration `json:"scimConfiguration,omitempty"`
Connector *Connector `json:"connector,omitempty"` Connector *Connector `json:"connector,omitempty"`
Type coredata.SCIMBridgeType `json:"type"` Type coredata.SCIMBridgeType `json:"type"`
ExcludedUserNames []string `json:"excludedUserNames"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
Permission bool `json:"permission"` Permission bool `json:"permission"`
@@ -557,6 +558,16 @@ type UpdateSAMLConfigurationPayload struct {
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration,omitempty"` SamlConfiguration *SAMLConfiguration `json:"samlConfiguration,omitempty"`
} }
type UpdateSCIMBridgeInput struct {
OrganizationID gid.GID `json:"organizationId"`
ScimBridgeID gid.GID `json:"scimBridgeId"`
ExcludedUserNames []string `json:"excludedUserNames"`
}
type UpdateSCIMBridgePayload struct {
ScimBridge *SCIMBridge `json:"scimBridge"`
}
type VerifyEmailInput struct { type VerifyEmailInput struct {
Token string `json:"token"` Token string `json:"token"`
} }

View File

@@ -1177,6 +1177,23 @@ func (r *mutationResolver) RegenerateSCIMToken(ctx context.Context, input types.
}, nil }, nil
} }
// UpdateSCIMBridge is the resolver for the updateSCIMBridge field.
func (r *mutationResolver) UpdateSCIMBridge(ctx context.Context, input types.UpdateSCIMBridgeInput) (*types.UpdateSCIMBridgePayload, error) {
if err := r.authorize(ctx, input.ScimBridgeID, iam.ActionSCIMBridgeUpdate); err != nil {
return nil, err
}
bridge, err := r.iam.OrganizationService.UpdateSCIMBridge(ctx, input.OrganizationID, input.ScimBridgeID, input.ExcludedUserNames)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot update scim bridge excluded user names", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.UpdateSCIMBridgePayload{
ScimBridge: types.NewSCIMBridge(bridge),
}, nil
}
// LogoURL is the resolver for the logoUrl field. // LogoURL is the resolver for the logoUrl field.
func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) { func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
if err := r.authorize(ctx, obj.ID, iam.ActionOrganizationGet, authz.WithSkipAssumptionCheck()); err != nil { if err := r.authorize(ctx, obj.ID, iam.ActionOrganizationGet, authz.WithSkipAssumptionCheck()); err != nil {