diff --git a/backend/internal/bootstrap/bootstrap.go b/backend/internal/bootstrap/bootstrap.go index 733add97..2b033684 100644 --- a/backend/internal/bootstrap/bootstrap.go +++ b/backend/internal/bootstrap/bootstrap.go @@ -48,8 +48,13 @@ func Bootstrap(ctx context.Context) error { return fmt.Errorf("failed to initialize application images: %w", err) } + scheduler, err := job.NewScheduler() + if err != nil { + return fmt.Errorf("failed to create job scheduler: %w", err) + } + // Create all services - svc, err := initServices(ctx, db, httpClient, imageExtensions, fileStorage) + svc, err := initServices(ctx, db, httpClient, imageExtensions, fileStorage, scheduler) if err != nil { return fmt.Errorf("failed to initialize services: %w", err) } @@ -74,11 +79,7 @@ func Bootstrap(ctx context.Context) error { } shutdownFns = append(shutdownFns, shutdownFn) - // Init the job scheduler - scheduler, err := job.NewScheduler() - if err != nil { - return fmt.Errorf("failed to create job scheduler: %w", err) - } + // Register scheduled jobs err = registerScheduledJobs(ctx, db, svc, httpClient, scheduler) if err != nil { return fmt.Errorf("failed to register scheduled jobs: %w", err) diff --git a/backend/internal/bootstrap/scheduler_bootstrap.go b/backend/internal/bootstrap/scheduler_bootstrap.go index 3c6fce58..0c87d0ee 100644 --- a/backend/internal/bootstrap/scheduler_bootstrap.go +++ b/backend/internal/bootstrap/scheduler_bootstrap.go @@ -35,6 +35,10 @@ func registerScheduledJobs(ctx context.Context, db *gorm.DB, svc *services, http if err != nil { return fmt.Errorf("failed to register analytics job in scheduler: %w", err) } + err = scheduler.RegisterScimJobs(ctx, svc.scimService) + if err != nil { + return fmt.Errorf("failed to register SCIM scheduler job: %w", err) + } return nil } diff --git a/backend/internal/bootstrap/services_bootstrap.go b/backend/internal/bootstrap/services_bootstrap.go index 4288bd9d..30d10419 100644 --- a/backend/internal/bootstrap/services_bootstrap.go +++ b/backend/internal/bootstrap/services_bootstrap.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/pocket-id/pocket-id/backend/internal/job" "gorm.io/gorm" "github.com/pocket-id/pocket-id/backend/internal/service" @@ -12,28 +13,27 @@ import ( ) type services struct { - appConfigService *service.AppConfigService - appImagesService *service.AppImagesService - emailService *service.EmailService - geoLiteService *service.GeoLiteService - auditLogService *service.AuditLogService - jwtService *service.JwtService - webauthnService *service.WebAuthnService - scimService *service.ScimService - scimSchedulerService *service.ScimSchedulerService - userService *service.UserService - customClaimService *service.CustomClaimService - oidcService *service.OidcService - userGroupService *service.UserGroupService - ldapService *service.LdapService - apiKeyService *service.ApiKeyService - versionService *service.VersionService - fileStorage storage.FileStorage - appLockService *service.AppLockService + appConfigService *service.AppConfigService + appImagesService *service.AppImagesService + emailService *service.EmailService + geoLiteService *service.GeoLiteService + auditLogService *service.AuditLogService + jwtService *service.JwtService + webauthnService *service.WebAuthnService + scimService *service.ScimService + userService *service.UserService + customClaimService *service.CustomClaimService + oidcService *service.OidcService + userGroupService *service.UserGroupService + ldapService *service.LdapService + apiKeyService *service.ApiKeyService + versionService *service.VersionService + fileStorage storage.FileStorage + appLockService *service.AppLockService } // Initializes all services -func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, imageExtensions map[string]string, fileStorage storage.FileStorage) (svc *services, err error) { +func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, imageExtensions map[string]string, fileStorage storage.FileStorage, scheduler *job.Scheduler) (svc *services, err error) { svc = &services{} svc.appConfigService, err = service.NewAppConfigService(ctx, db) @@ -63,20 +63,17 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, ima return nil, fmt.Errorf("failed to create WebAuthn service: %w", err) } - svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService, svc.webauthnService, httpClient, fileStorage) + svc.scimService = service.NewScimService(db, scheduler, httpClient) + + svc.oidcService, err = service.NewOidcService(ctx, db, svc.jwtService, svc.appConfigService, svc.auditLogService, svc.customClaimService, svc.webauthnService, svc.scimService, httpClient, fileStorage) if err != nil { return nil, fmt.Errorf("failed to create OIDC service: %w", err) } - svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService) - svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService, svc.appImagesService, fileStorage) + svc.userGroupService = service.NewUserGroupService(db, svc.appConfigService, svc.scimService) + svc.userService = service.NewUserService(db, svc.jwtService, svc.auditLogService, svc.emailService, svc.appConfigService, svc.customClaimService, svc.appImagesService, svc.scimService, fileStorage) svc.ldapService = service.NewLdapService(db, httpClient, svc.appConfigService, svc.userService, svc.userGroupService, fileStorage) svc.apiKeyService = service.NewApiKeyService(db, svc.emailService) - svc.scimService = service.NewScimService(db, httpClient) - svc.scimSchedulerService, err = service.NewScimSchedulerService(ctx, svc.scimService) - if err != nil { - return nil, fmt.Errorf("failed to create SCIM scheduler service: %w", err) - } svc.versionService = service.NewVersionService(httpClient) diff --git a/backend/internal/job/analytics_job.go b/backend/internal/job/analytics_job.go index 6cf2b944..f67e2042 100644 --- a/backend/internal/job/analytics_job.go +++ b/backend/internal/job/analytics_job.go @@ -28,7 +28,7 @@ func (s *Scheduler) RegisterAnalyticsJob(ctx context.Context, appConfig *service appConfig: appConfig, httpClient: httpClient, } - return s.registerJob(ctx, "SendHeartbeat", gocron.DurationJob(24*time.Hour), jobs.sendHeartbeat, true) + return s.RegisterJob(ctx, "SendHeartbeat", gocron.DurationJob(24*time.Hour), jobs.sendHeartbeat, true) } type AnalyticsJob struct { diff --git a/backend/internal/job/api_key_expiry_job.go b/backend/internal/job/api_key_expiry_job.go index 226f9c59..6524089b 100644 --- a/backend/internal/job/api_key_expiry_job.go +++ b/backend/internal/job/api_key_expiry_job.go @@ -22,7 +22,7 @@ func (s *Scheduler) RegisterApiKeyExpiryJob(ctx context.Context, apiKeyService * } // Send every day at midnight - return s.registerJob(ctx, "ExpiredApiKeyEmailJob", gocron.CronJob("0 0 * * *", false), jobs.checkAndNotifyExpiringApiKeys, false) + return s.RegisterJob(ctx, "ExpiredApiKeyEmailJob", gocron.CronJob("0 0 * * *", false), jobs.checkAndNotifyExpiringApiKeys, false) } func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) error { diff --git a/backend/internal/job/db_cleanup_job.go b/backend/internal/job/db_cleanup_job.go index 3566f858..338f989c 100644 --- a/backend/internal/job/db_cleanup_job.go +++ b/backend/internal/job/db_cleanup_job.go @@ -21,13 +21,13 @@ func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) erro // Run every 24 hours (but with some jitter so they don't run at the exact same time), and now def := gocron.DurationRandomJob(24*time.Hour-2*time.Minute, 24*time.Hour+2*time.Minute) return errors.Join( - s.registerJob(ctx, "ClearWebauthnSessions", def, jobs.clearWebauthnSessions, true), - s.registerJob(ctx, "ClearOneTimeAccessTokens", def, jobs.clearOneTimeAccessTokens, true), - s.registerJob(ctx, "ClearSignupTokens", def, jobs.clearSignupTokens, true), - s.registerJob(ctx, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, true), - s.registerJob(ctx, "ClearOidcRefreshTokens", def, jobs.clearOidcRefreshTokens, true), - s.registerJob(ctx, "ClearReauthenticationTokens", def, jobs.clearReauthenticationTokens, true), - s.registerJob(ctx, "ClearAuditLogs", def, jobs.clearAuditLogs, true), + s.RegisterJob(ctx, "ClearWebauthnSessions", def, jobs.clearWebauthnSessions, true), + s.RegisterJob(ctx, "ClearOneTimeAccessTokens", def, jobs.clearOneTimeAccessTokens, true), + s.RegisterJob(ctx, "ClearSignupTokens", def, jobs.clearSignupTokens, true), + s.RegisterJob(ctx, "ClearOidcAuthorizationCodes", def, jobs.clearOidcAuthorizationCodes, true), + s.RegisterJob(ctx, "ClearOidcRefreshTokens", def, jobs.clearOidcRefreshTokens, true), + s.RegisterJob(ctx, "ClearReauthenticationTokens", def, jobs.clearReauthenticationTokens, true), + s.RegisterJob(ctx, "ClearAuditLogs", def, jobs.clearAuditLogs, true), ) } diff --git a/backend/internal/job/file_cleanup_job.go b/backend/internal/job/file_cleanup_job.go index cf517f9e..2b141dac 100644 --- a/backend/internal/job/file_cleanup_job.go +++ b/backend/internal/job/file_cleanup_job.go @@ -19,11 +19,11 @@ import ( func (s *Scheduler) RegisterFileCleanupJobs(ctx context.Context, db *gorm.DB, fileStorage storage.FileStorage) error { jobs := &FileCleanupJobs{db: db, fileStorage: fileStorage} - err := s.registerJob(ctx, "ClearUnusedDefaultProfilePictures", gocron.DurationJob(24*time.Hour), jobs.clearUnusedDefaultProfilePictures, false) + err := s.RegisterJob(ctx, "ClearUnusedDefaultProfilePictures", gocron.DurationJob(24*time.Hour), jobs.clearUnusedDefaultProfilePictures, false) // Only necessary for file system storage if fileStorage.Type() == storage.TypeFileSystem { - err = errors.Join(err, s.registerJob(ctx, "ClearOrphanedTempFiles", gocron.DurationJob(12*time.Hour), jobs.clearOrphanedTempFiles, true)) + err = errors.Join(err, s.RegisterJob(ctx, "ClearOrphanedTempFiles", gocron.DurationJob(12*time.Hour), jobs.clearOrphanedTempFiles, true)) } return err diff --git a/backend/internal/job/geoloite_update_job.go b/backend/internal/job/geoloite_update_job.go index 59419c3c..65353757 100644 --- a/backend/internal/job/geoloite_update_job.go +++ b/backend/internal/job/geoloite_update_job.go @@ -23,7 +23,7 @@ func (s *Scheduler) RegisterGeoLiteUpdateJobs(ctx context.Context, geoLiteServic jobs := &GeoLiteUpdateJobs{geoLiteService: geoLiteService} // Run every 24 hours (and right away) - return s.registerJob(ctx, "UpdateGeoLiteDB", gocron.DurationJob(24*time.Hour), jobs.updateGoeLiteDB, true) + return s.RegisterJob(ctx, "UpdateGeoLiteDB", gocron.DurationJob(24*time.Hour), jobs.updateGoeLiteDB, true) } func (j *GeoLiteUpdateJobs) updateGoeLiteDB(ctx context.Context) error { diff --git a/backend/internal/job/ldap_job.go b/backend/internal/job/ldap_job.go index ecb93212..33646860 100644 --- a/backend/internal/job/ldap_job.go +++ b/backend/internal/job/ldap_job.go @@ -18,7 +18,7 @@ func (s *Scheduler) RegisterLdapJobs(ctx context.Context, ldapService *service.L jobs := &LdapJobs{ldapService: ldapService, appConfigService: appConfigService} // Register the job to run every hour - return s.registerJob(ctx, "SyncLdap", gocron.DurationJob(time.Hour), jobs.syncLdap, true) + return s.RegisterJob(ctx, "SyncLdap", gocron.DurationJob(time.Hour), jobs.syncLdap, true) } func (j *LdapJobs) syncLdap(ctx context.Context) error { diff --git a/backend/internal/job/scheduler.go b/backend/internal/job/scheduler.go index 20461702..2a48c2a8 100644 --- a/backend/internal/job/scheduler.go +++ b/backend/internal/job/scheduler.go @@ -2,6 +2,7 @@ package job import ( "context" + "errors" "fmt" "log/slog" @@ -24,6 +25,26 @@ func NewScheduler() (*Scheduler, error) { }, nil } +func (s *Scheduler) RemoveJob(name string) error { + jobs := s.scheduler.Jobs() + + var errs []error + for _, job := range jobs { + if job.Name() == name { + err := s.scheduler.RemoveJob(job.ID()) + if err != nil { + errs = append(errs, fmt.Errorf("failed to unqueue job %q with ID %q: %w", name, job.ID().String(), err)) + } + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + // Run the scheduler. // This function blocks until the context is canceled. func (s *Scheduler) Run(ctx context.Context) error { @@ -43,9 +64,10 @@ func (s *Scheduler) Run(ctx context.Context) error { return nil } -func (s *Scheduler) registerJob(ctx context.Context, name string, def gocron.JobDefinition, job func(ctx context.Context) error, runImmediately bool) error { +func (s *Scheduler) RegisterJob(ctx context.Context, name string, def gocron.JobDefinition, job func(ctx context.Context) error, runImmediately bool, extraOptions ...gocron.JobOption) error { jobOptions := []gocron.JobOption{ gocron.WithContext(ctx), + gocron.WithName(name), gocron.WithEventListeners( gocron.BeforeJobRuns(func(jobID uuid.UUID, jobName string) { slog.Info("Starting job", @@ -73,6 +95,8 @@ func (s *Scheduler) registerJob(ctx context.Context, name string, def gocron.Job jobOptions = append(jobOptions, gocron.JobOption(gocron.WithStartImmediately())) } + jobOptions = append(jobOptions, extraOptions...) + _, err := s.scheduler.NewJob(def, gocron.NewTask(job), jobOptions...) if err != nil { diff --git a/backend/internal/job/scim_job.go b/backend/internal/job/scim_job.go new file mode 100644 index 00000000..1ea8ee96 --- /dev/null +++ b/backend/internal/job/scim_job.go @@ -0,0 +1,25 @@ +package job + +import ( + "context" + "time" + + "github.com/go-co-op/gocron/v2" + + "github.com/pocket-id/pocket-id/backend/internal/service" +) + +type ScimJobs struct { + scimService *service.ScimService +} + +func (s *Scheduler) RegisterScimJobs(ctx context.Context, scimService *service.ScimService) error { + jobs := &ScimJobs{scimService: scimService} + + // Register the job to run every hour + return s.RegisterJob(ctx, "SyncScim", gocron.DurationJob(time.Hour), jobs.SyncScim, true) +} + +func (j *ScimJobs) SyncScim(ctx context.Context) error { + return j.scimService.SyncAll(ctx) +} diff --git a/backend/internal/service/oidc_service.go b/backend/internal/service/oidc_service.go index 9b194bf1..8253d49c 100644 --- a/backend/internal/service/oidc_service.go +++ b/backend/internal/service/oidc_service.go @@ -56,6 +56,7 @@ type OidcService struct { auditLogService *AuditLogService customClaimService *CustomClaimService webAuthnService *WebAuthnService + scimService *ScimService httpClient *http.Client jwkCache *jwk.Cache @@ -70,6 +71,7 @@ func NewOidcService( auditLogService *AuditLogService, customClaimService *CustomClaimService, webAuthnService *WebAuthnService, + scimService *ScimService, httpClient *http.Client, fileStorage storage.FileStorage, ) (s *OidcService, err error) { @@ -80,6 +82,7 @@ func NewOidcService( auditLogService: auditLogService, customClaimService: customClaimService, webAuthnService: webAuthnService, + scimService: scimService, httpClient: httpClient, fileStorage: fileStorage, } @@ -1088,6 +1091,7 @@ func (s *OidcService) UpdateAllowedUserGroups(ctx context.Context, id string, in return model.OidcClient{}, err } + s.scimService.ScheduleSync() return client, nil } diff --git a/backend/internal/service/scim_scheduler_service.go b/backend/internal/service/scim_scheduler_service.go deleted file mode 100644 index c2c631b8..00000000 --- a/backend/internal/service/scim_scheduler_service.go +++ /dev/null @@ -1,136 +0,0 @@ -package service - -import ( - "context" - "errors" - "log/slog" - "sync" - "time" - - "gorm.io/gorm" -) - -// ScimSchedulerService schedules and triggers periodic synchronization -// of SCIM service providers. Each provider is tracked independently, -// and sync operations are run at or after their scheduled time. -type ScimSchedulerService struct { - scimService *ScimService - providerSyncTime map[string]time.Time - mu sync.RWMutex -} - -func NewScimSchedulerService(ctx context.Context, scimService *ScimService) (*ScimSchedulerService, error) { - s := &ScimSchedulerService{ - scimService: scimService, - providerSyncTime: make(map[string]time.Time), - } - - err := s.start(ctx) - return s, err -} - -// ScheduleSync forces the given provider to be synced soon by -// moving its next scheduled time to 5 minutes from now. -func (s *ScimSchedulerService) ScheduleSync(providerID string) { - s.setSyncTime(providerID, 5*time.Minute) -} - -// start initializes the scheduler and begins the synchronization loop. -// Syncs happen every hour by default, but ScheduleSync can be called to schedule a sync sooner. -func (s *ScimSchedulerService) start(ctx context.Context) error { - if err := s.refreshProviders(ctx); err != nil { - return err - } - - go func() { - const ( - syncCheckInterval = 5 * time.Second - providerRefreshDelay = time.Minute - ) - - ticker := time.NewTicker(syncCheckInterval) - defer ticker.Stop() - lastProviderRefresh := time.Now() - - for { - select { - case <-ctx.Done(): - return - // Runs every 5 seconds to check if any provider is due for sync - case <-ticker.C: - now := time.Now() - if now.Sub(lastProviderRefresh) >= providerRefreshDelay { - err := s.refreshProviders(ctx) - if err != nil { - slog.Error("Error refreshing SCIM service providers", - slog.Any("error", err), - ) - } else { - lastProviderRefresh = now - } - } - - var due []string - s.mu.RLock() - for providerID, syncTime := range s.providerSyncTime { - if !syncTime.After(now) { - due = append(due, providerID) - } - } - s.mu.RUnlock() - - s.syncProviders(ctx, due) - - } - } - }() - - return nil -} - -func (s *ScimSchedulerService) refreshProviders(ctx context.Context) error { - providers, err := s.scimService.ListServiceProviders(ctx) - if err != nil { - return err - } - - inAHour := time.Now().Add(time.Hour) - - s.mu.Lock() - for _, provider := range providers { - if _, exists := s.providerSyncTime[provider.ID]; !exists { - s.providerSyncTime[provider.ID] = inAHour - } - } - s.mu.Unlock() - - return nil -} - -func (s *ScimSchedulerService) syncProviders(ctx context.Context, providerIDs []string) { - for _, providerID := range providerIDs { - err := s.scimService.SyncServiceProvider(ctx, providerID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - // Remove the provider from the schedule if it no longer exists - s.mu.Lock() - delete(s.providerSyncTime, providerID) - s.mu.Unlock() - } else { - slog.Error("Error syncing SCIM client", - slog.String("provider_id", providerID), - slog.Any("error", err), - ) - } - continue - } - // A successful sync schedules the next sync in an hour - s.setSyncTime(providerID, time.Hour) - } -} - -func (s *ScimSchedulerService) setSyncTime(providerID string, t time.Duration) { - s.mu.Lock() - s.providerSyncTime[providerID] = time.Now().Add(t) - s.mu.Unlock() -} diff --git a/backend/internal/service/scim_service.go b/backend/internal/service/scim_service.go index 938174aa..1d8a8736 100644 --- a/backend/internal/service/scim_service.go +++ b/backend/internal/service/scim_service.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "github.com/go-co-op/gocron/v2" "github.com/pocket-id/pocket-id/backend/internal/dto" "github.com/pocket-id/pocket-id/backend/internal/model" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" @@ -32,6 +33,11 @@ const scimErrorBodyLimit = 4096 type scimSyncAction int +type Scheduler interface { + RegisterJob(ctx context.Context, name string, def gocron.JobDefinition, job func(ctx context.Context) error, runImmediately bool, extraOptions ...gocron.JobOption) error + RemoveJob(name string) error +} + const ( scimActionNone scimSyncAction = iota scimActionCreated @@ -48,15 +54,16 @@ type scimSyncStats struct { // ScimService handles SCIM provisioning to external service providers. type ScimService struct { db *gorm.DB + scheduler Scheduler httpClient *http.Client } -func NewScimService(db *gorm.DB, httpClient *http.Client) *ScimService { +func NewScimService(db *gorm.DB, scheduler Scheduler, httpClient *http.Client) *ScimService { if httpClient == nil { httpClient = &http.Client{Timeout: 20 * time.Second} } - return &ScimService{db: db, httpClient: httpClient} + return &ScimService{db: db, scheduler: scheduler, httpClient: httpClient} } func (s *ScimService) GetServiceProvider( @@ -132,6 +139,41 @@ func (s *ScimService) DeleteServiceProvider(ctx context.Context, serviceProvider Error } +//nolint:contextcheck +func (s *ScimService) ScheduleSync() { + jobName := "ScheduledScimSync" + start := time.Now().Add(5 * time.Minute) + + _ = s.scheduler.RemoveJob(jobName) + + err := s.scheduler.RegisterJob( + context.Background(), jobName, + gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(start)), s.SyncAll, false) + + if err != nil { + slog.Error("Failed to schedule SCIM sync", slog.Any("error", err)) + } +} + +func (s *ScimService) SyncAll(ctx context.Context) error { + providers, err := s.ListServiceProviders(ctx) + if err != nil { + return err + } + + var errs []error + for _, provider := range providers { + if ctx.Err() != nil { + errs = append(errs, ctx.Err()) + break + } + if err := s.SyncServiceProvider(ctx, provider.ID); err != nil { + errs = append(errs, fmt.Errorf("failed to sync SCIM provider %s: %w", provider.ID, err)) + } + } + return errors.Join(errs...) +} + func (s *ScimService) SyncServiceProvider(ctx context.Context, serviceProviderID string) error { start := time.Now() provider, err := s.GetServiceProvider(ctx, serviceProviderID) diff --git a/backend/internal/service/user_group_service.go b/backend/internal/service/user_group_service.go index fbbf304e..cc6cb7eb 100644 --- a/backend/internal/service/user_group_service.go +++ b/backend/internal/service/user_group_service.go @@ -16,11 +16,12 @@ import ( type UserGroupService struct { db *gorm.DB + scimService *ScimService appConfigService *AppConfigService } -func NewUserGroupService(db *gorm.DB, appConfigService *AppConfigService) *UserGroupService { - return &UserGroupService{db: db, appConfigService: appConfigService} +func NewUserGroupService(db *gorm.DB, appConfigService *AppConfigService, scimService *ScimService) *UserGroupService { + return &UserGroupService{db: db, appConfigService: appConfigService, scimService: scimService} } func (s *UserGroupService) List(ctx context.Context, name string, listRequestOptions utils.ListRequestOptions) (groups []model.UserGroup, response utils.PaginationResponse, err error) { @@ -90,7 +91,13 @@ func (s *UserGroupService) Delete(ctx context.Context, id string) error { return err } - return tx.Commit().Error + err = tx.Commit().Error + if err != nil { + return err + } + + s.scimService.ScheduleSync() + return nil } func (s *UserGroupService) Create(ctx context.Context, input dto.UserGroupCreateDto) (group model.UserGroup, err error) { @@ -118,6 +125,8 @@ func (s *UserGroupService) createInternal(ctx context.Context, input dto.UserGro } return model.UserGroup{}, err } + + s.scimService.ScheduleSync() return group, nil } @@ -165,6 +174,8 @@ func (s *UserGroupService) updateInternal(ctx context.Context, id string, input } else if err != nil { return model.UserGroup{}, err } + + s.scimService.ScheduleSync() return group, nil } @@ -227,6 +238,7 @@ func (s *UserGroupService) updateUsersInternal(ctx context.Context, id string, u return model.UserGroup{}, err } + s.scimService.ScheduleSync() return group, nil } @@ -303,5 +315,6 @@ func (s *UserGroupService) UpdateAllowedOidcClient(ctx context.Context, id strin return model.UserGroup{}, err } + s.scimService.ScheduleSync() return group, nil } diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index b66ae01b..f13a7921 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -37,10 +37,11 @@ type UserService struct { appConfigService *AppConfigService customClaimService *CustomClaimService appImagesService *AppImagesService + scimService *ScimService fileStorage storage.FileStorage } -func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService, appImagesService *AppImagesService, fileStorage storage.FileStorage) *UserService { +func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService, appConfigService *AppConfigService, customClaimService *CustomClaimService, appImagesService *AppImagesService, scimService *ScimService, fileStorage storage.FileStorage) *UserService { return &UserService{ db: db, jwtService: jwtService, @@ -49,6 +50,7 @@ func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditL appConfigService: appConfigService, customClaimService: customClaimService, appImagesService: appImagesService, + scimService: scimService, fileStorage: fileStorage, } } @@ -226,6 +228,7 @@ func (s *UserService) deleteUserInternal(ctx context.Context, tx *gorm.DB, userI return fmt.Errorf("failed to delete user: %w", err) } + s.scimService.ScheduleSync() return nil } @@ -309,6 +312,7 @@ func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCrea } } + s.scimService.ScheduleSync() return user, nil } @@ -447,6 +451,7 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd return user, err } + s.scimService.ScheduleSync() return user, nil } @@ -663,6 +668,7 @@ func (s *UserService) UpdateUserGroups(ctx context.Context, id string, userGroup return model.User{}, err } + s.scimService.ScheduleSync() return user, nil } @@ -753,12 +759,19 @@ func (s *UserService) ResetProfilePicture(ctx context.Context, userID string) er } func (s *UserService) disableUserInternal(ctx context.Context, tx *gorm.DB, userID string) error { - return tx. + err := tx. WithContext(ctx). Model(&model.User{}). Where("id = ?", userID). Update("disabled", true). Error + + if err != nil { + return err + } + + s.scimService.ScheduleSync() + return nil } func (s *UserService) CreateSignupToken(ctx context.Context, ttl time.Duration, usageLimit int, userGroupIDs []string) (model.SignupToken, error) {