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

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

View File

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

View File

@@ -180,6 +180,11 @@ var IAMOwnerPolicy = policy.NewPolicy(
policy.Allow("iam:scim-event:*").
WithSID("full-scim-event-access").
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")

View File

@@ -1434,6 +1434,50 @@ func (s OrganizationService) RegenerateSCIMToken(
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(
ctx context.Context,
scimConfigurationID gid.GID,
@@ -1863,6 +1907,7 @@ func (s OrganizationService) CreateSCIMBridge(
ConnectorID: &connectorID,
Type: bridgeType,
State: coredata.SCIMBridgeStateActive, // Active immediately since connector already exists
ExcludedUserNames: []string{},
CreatedAt: now,
UpdatedAt: now,
}

View File

@@ -28,10 +28,11 @@ import (
type (
Bridge struct {
provider provider.Provider
scimClient *scimclient.Client
forceUpdate bool
dryRun bool
provider provider.Provider
scimClient *scimclient.Client
excludedUserNames []string
forceUpdate bool
dryRun bool
}
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 {
s := &Bridge{
provider: provider,
@@ -62,15 +69,15 @@ func NewBridge(provider provider.Provider, scimClient *scimclient.Client, opts .
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)
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)
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)
@@ -125,6 +132,17 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deactivated, skippe
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 {
continue
}
@@ -138,5 +156,14 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deactivated, skippe
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
}
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) {
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Accept", "application/scim+json")

View File

@@ -20,6 +20,7 @@ import (
"context"
"fmt"
"net/http"
"strings"
admin "google.golang.org/api/admin/directory/v1"
"google.golang.org/api/option"
@@ -31,12 +32,14 @@ import (
var _ provider.Provider = (*Provider)(nil)
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{
httpClient: httpClient,
httpClient: httpClient,
excludedUserNames: excludedUserNames,
}
}
@@ -44,6 +47,16 @@ func (p *Provider) Name() string {
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) {
adminService, err := admin.NewService(ctx, option.WithHTTPClient(p.httpClient))
if err != nil {
@@ -65,6 +78,10 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) {
}
for _, u := range resp.Users {
if p.isExcluded(u.PrimaryEmail) {
continue
}
allUsers = append(
allUsers,
scimclient.User{

View File

@@ -27,6 +27,7 @@ import (
type SyncStats struct {
Created int
Updated int
Deleted int
Deactivated int
Skipped int
}
@@ -104,6 +105,7 @@ func (r *BridgeRunner) transitionToSuccess(
log.Duration("sync_duration", duration),
log.Int("users_created", stats.Created),
log.Int("users_updated", stats.Updated),
log.Int("users_deleted", stats.Deleted),
log.Int("users_deactivated", stats.Deactivated),
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)
}
idp, err := r.createProvider(ctx, logger, scimBridge.Type, dbConnector)
idp, err := r.createProvider(ctx, logger, scimBridge.Type, dbConnector, scimBridge.ExcludedUserNames)
if err != nil {
return SyncStats{}, nil, fmt.Errorf("cannot create provider: %w", err)
}
@@ -89,8 +89,8 @@ func (r *BridgeRunner) doSync(
}
scimClient := r.createSCIMClient(logger, token)
syncer := bridge.NewBridge(idp, scimClient)
created, updated, deactivated, skipped, err := syncer.Run(ctx)
syncer := bridge.NewBridge(idp, scimClient, bridge.WithExcludedUserNames(scimBridge.ExcludedUserNames))
created, updated, deleted, deactivated, skipped, err := syncer.Run(ctx)
if err != nil {
return SyncStats{}, nil, fmt.Errorf("sync failed: %w", err)
}
@@ -98,6 +98,7 @@ func (r *BridgeRunner) doSync(
stats := SyncStats{
Created: created,
Updated: updated,
Deleted: deleted,
Deactivated: deactivated,
Skipped: skipped,
}
@@ -121,10 +122,11 @@ func (r *BridgeRunner) createProvider(
logger *log.Logger,
bridgeType coredata.SCIMBridgeType,
dbConnector *coredata.Connector,
excludedUserNames []string,
) (provider.Provider, error) {
switch bridgeType {
case coredata.SCIMBridgeTypeGoogleWorkspace:
return r.createGoogleWorkspaceProvider(ctx, logger, dbConnector)
return r.createGoogleWorkspaceProvider(ctx, logger, dbConnector, excludedUserNames)
default:
return nil, fmt.Errorf("unsupported bridge type: %s", bridgeType)
}
@@ -134,6 +136,7 @@ func (r *BridgeRunner) createGoogleWorkspaceProvider(
ctx context.Context,
logger *log.Logger,
dbConnector *coredata.Connector,
excludedUserNames []string,
) (provider.Provider, error) {
if dbConnector.Connection == nil {
return nil, fmt.Errorf("connector has no connection configured")
@@ -161,7 +164,7 @@ func (r *BridgeRunner) createGoogleWorkspaceProvider(
if err != nil {
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...)
@@ -169,5 +172,5 @@ func (r *BridgeRunner) createGoogleWorkspaceProvider(
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(
input: RegenerateSCIMTokenInput!
): RegenerateSCIMTokenPayload @session(required: PRESENT)
updateSCIMBridge(
input: UpdateSCIMBridgeInput!
): UpdateSCIMBridgePayload @session(required: PRESENT)
}
type Identity implements Node {
@@ -368,6 +371,7 @@ type SCIMBridge implements Node {
scimConfiguration: SCIMConfiguration @goField(forceResolver: true)
connector: Connector @goField(forceResolver: true)
type: SCIMBridgeType!
excludedUserNames: [String!]!
createdAt: Datetime!
updatedAt: Datetime!
@@ -905,6 +909,12 @@ input RegenerateSCIMTokenInput {
scimConfigurationId: ID!
}
input UpdateSCIMBridgeInput {
organizationId: ID!
scimBridgeId: ID!
excludedUserNames: [String!]!
}
type CreateSCIMConfigurationPayload {
scimConfiguration: SCIMConfiguration!
scimBridge: SCIMBridge
@@ -919,3 +929,7 @@ type RegenerateSCIMTokenPayload {
scimConfiguration: SCIMConfiguration!
token: String!
}
type UpdateSCIMBridgePayload {
scimBridge: SCIMBridge!
}

View File

@@ -246,6 +246,7 @@ type ComplexityRoot struct {
UpdateMembership func(childComplexity int, input types.UpdateMembershipInput) int
UpdateOrganization func(childComplexity int, input types.UpdateOrganizationInput) 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
}
@@ -379,6 +380,7 @@ type ComplexityRoot struct {
SCIMBridge struct {
Connector func(childComplexity int) int
CreatedAt func(childComplexity int) int
ExcludedUserNames func(childComplexity int) int
ID func(childComplexity int) int
Permission func(childComplexity int, action string) int
ScimConfiguration func(childComplexity int) int
@@ -474,6 +476,10 @@ type ComplexityRoot struct {
SamlConfiguration func(childComplexity int) int
}
UpdateSCIMBridgePayload struct {
ScimBridge func(childComplexity int) int
}
VerifyEmailPayload struct {
Success func(childComplexity int) int
}
@@ -540,6 +546,7 @@ type MutationResolver interface {
CreateSCIMConfiguration(ctx context.Context, input types.CreateSCIMConfigurationInput) (*types.CreateSCIMConfigurationPayload, error)
DeleteSCIMConfiguration(ctx context.Context, input types.DeleteSCIMConfigurationInput) (*types.DeleteSCIMConfigurationPayload, error)
RegenerateSCIMToken(ctx context.Context, input types.RegenerateSCIMTokenInput) (*types.RegenerateSCIMTokenPayload, error)
UpdateSCIMBridge(ctx context.Context, input types.UpdateSCIMBridgeInput) (*types.UpdateSCIMBridgePayload, error)
}
type OrganizationResolver interface {
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
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":
if e.complexity.Mutation.VerifyEmail == nil {
break
@@ -1918,6 +1936,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
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":
if e.complexity.SCIMBridge.ID == nil {
break
@@ -2264,6 +2288,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
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":
if e.complexity.VerifyEmailPayload.Success == nil {
break
@@ -2310,6 +2341,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputUpdateMembershipInput,
ec.unmarshalInputUpdateOrganizationInput,
ec.unmarshalInputUpdateSAMLConfigurationInput,
ec.unmarshalInputUpdateSCIMBridgeInput,
ec.unmarshalInputVerifyEmailInput,
)
first := true
@@ -2534,6 +2566,9 @@ type Mutation {
regenerateSCIMToken(
input: RegenerateSCIMTokenInput!
): RegenerateSCIMTokenPayload @session(required: PRESENT)
updateSCIMBridge(
input: UpdateSCIMBridgeInput!
): UpdateSCIMBridgePayload @session(required: PRESENT)
}
type Identity implements Node {
@@ -2778,6 +2813,7 @@ type SCIMBridge implements Node {
scimConfiguration: SCIMConfiguration @goField(forceResolver: true)
connector: Connector @goField(forceResolver: true)
type: SCIMBridgeType!
excludedUserNames: [String!]!
createdAt: Datetime!
updatedAt: Datetime!
@@ -3315,6 +3351,12 @@ input RegenerateSCIMTokenInput {
scimConfigurationId: ID!
}
input UpdateSCIMBridgeInput {
organizationId: ID!
scimBridgeId: ID!
excludedUserNames: [String!]!
}
type CreateSCIMConfigurationPayload {
scimConfiguration: SCIMConfiguration!
scimBridge: SCIMBridge
@@ -3329,6 +3371,10 @@ type RegenerateSCIMTokenPayload {
scimConfiguration: SCIMConfiguration!
token: String!
}
type UpdateSCIMBridgePayload {
scimBridge: SCIMBridge!
}
`, BuiltIn: false},
{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.
@@ -3847,6 +3893,17 @@ func (ec *executionContext) field_Mutation_updateSAMLConfiguration_args(ctx cont
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) {
var err error
args := map[string]any{}
@@ -4770,6 +4827,8 @@ func (ec *executionContext) fieldContext_CreateSCIMConfigurationPayload_scimBrid
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":
@@ -8586,6 +8645,69 @@ func (ec *executionContext) fieldContext_Mutation_regenerateSCIMToken(ctx contex
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) {
return graphql.ResolveField(
ctx,
@@ -11337,6 +11459,35 @@ func (ec *executionContext) fieldContext_SCIMBridge_type(_ context.Context, fiel
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) {
return graphql.ResolveField(
ctx,
@@ -11667,6 +11818,8 @@ func (ec *executionContext) fieldContext_SCIMConfiguration_bridge(_ context.Cont
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":
@@ -13217,6 +13370,55 @@ func (ec *executionContext) fieldContext_UpdateSAMLConfigurationPayload_samlConf
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) {
return graphql.ResolveField(
ctx,
@@ -15872,6 +16074,47 @@ func (ec *executionContext) unmarshalInputUpdateSAMLConfigurationInput(ctx conte
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) {
var it types.VerifyEmailInput
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) {
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:
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 {
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":
out.Values[i] = ec._SCIMBridge_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
@@ -20413,6 +20665,45 @@ func (ec *executionContext) _UpdateSAMLConfigurationPayload(ctx context.Context,
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"}
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) {
tmp, err := graphql.UnmarshalString(v)
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
}
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) {
res, err := ec.unmarshalInputUpdateMembershipInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -21915,6 +22246,11 @@ func (ec *executionContext) unmarshalNUpdateSAMLConfigurationInput2goᚗproboᚗ
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) {
res, err := ec.unmarshalInputVerifyEmailInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -22695,6 +23031,13 @@ func (ec *executionContext) marshalOUpdateSAMLConfigurationPayload2ᚖgoᚗprobo
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) {
if v == nil {
return nil, nil

View File

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

View File

@@ -415,6 +415,7 @@ type SCIMBridge struct {
ScimConfiguration *SCIMConfiguration `json:"scimConfiguration,omitempty"`
Connector *Connector `json:"connector,omitempty"`
Type coredata.SCIMBridgeType `json:"type"`
ExcludedUserNames []string `json:"excludedUserNames"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Permission bool `json:"permission"`
@@ -557,6 +558,16 @@ type UpdateSAMLConfigurationPayload struct {
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 {
Token string `json:"token"`
}

View File

@@ -1177,6 +1177,23 @@ func (r *mutationResolver) RegenerateSCIMToken(ctx context.Context, input types.
}, 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.
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 {