Exclude users from google workspace bridge
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user