diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 61b6607a162..9d7778c8f81 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -600,7 +600,6 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api deleteExpiredService := image.ProvideDeleteExpiredService(dBstore) tempuserService := tempuserimpl.ProvideService(sqlStore, cfg) cleanupServiceImpl := annotationsimpl.ProvideCleanupService(sqlStore, cfg) - cleanUpService := cleanup.ProvideService(cfg, featureToggles, serverLockService, shortURLService, sqlStore, queryHistoryService, dashverService, serviceImpl, deleteExpiredService, tempuserService, tracingService, cleanupServiceImpl, dBstore, eventualRestConfigProvider, orgService) secretsKVStore, err := kvstore2.ProvideService(sqlStore, secretsService) if err != nil { return nil, err @@ -612,6 +611,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } + cleanUpService := cleanup.ProvideService(cfg, featureToggles, serverLockService, shortURLService, sqlStore, queryHistoryService, dashverService, serviceImpl, deleteExpiredService, tempuserService, tracingService, cleanupServiceImpl, dBstore, eventualRestConfigProvider, orgService, teamService, service15) correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service15, accessControl, inProcBus, quotaService, cfg) if err != nil { return nil, err @@ -1200,7 +1200,6 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac deleteExpiredService := image.ProvideDeleteExpiredService(dBstore) tempuserService := tempuserimpl.ProvideService(sqlStore, cfg) cleanupServiceImpl := annotationsimpl.ProvideCleanupService(sqlStore, cfg) - cleanUpService := cleanup.ProvideService(cfg, featureToggles, serverLockService, shortURLService, sqlStore, queryHistoryService, dashverService, serviceImpl, deleteExpiredService, tempuserService, tracingService, cleanupServiceImpl, dBstore, eventualRestConfigProvider, orgService) secretsKVStore, err := kvstore2.ProvideService(sqlStore, secretsService) if err != nil { return nil, err @@ -1212,6 +1211,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } + cleanUpService := cleanup.ProvideService(cfg, featureToggles, serverLockService, shortURLService, sqlStore, queryHistoryService, dashverService, serviceImpl, deleteExpiredService, tempuserService, tracingService, cleanupServiceImpl, dBstore, eventualRestConfigProvider, orgService, teamService, service15) correlationsService, err := correlations.ProvideService(sqlStore, routeRegisterImpl, service15, accessControl, inProcBus, quotaService, cfg) if err != nil { return nil, err diff --git a/pkg/services/cleanup/cleanup.go b/pkg/services/cleanup/cleanup.go index 7730ee1dcf4..a4fce5a12de 100644 --- a/pkg/services/cleanup/cleanup.go +++ b/pkg/services/cleanup/cleanup.go @@ -28,11 +28,13 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" + "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert/image" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/queryhistory" "github.com/grafana/grafana/pkg/services/shorturls" + "github.com/grafana/grafana/pkg/services/team" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/setting" ) @@ -58,12 +60,14 @@ type CleanUpService struct { alertRuleService AlertRuleService clientConfigProvider grafanaapiserver.RestConfigProvider orgService org.Service + teamService team.Service + dataSourceService datasources.DataSourceService } func ProvideService(cfg *setting.Cfg, Features featuremgmt.FeatureToggles, serverLockService *serverlock.ServerLockService, shortURLService shorturls.Service, sqlstore db.DB, queryHistoryService queryhistory.Service, dashboardVersionService dashver.Service, dashSnapSvc dashboardsnapshots.Service, deleteExpiredImageService *image.DeleteExpiredService, - tempUserService tempuser.Service, tracer tracing.Tracer, annotationCleaner annotations.Cleaner, service AlertRuleService, clientConfigProvider grafanaapiserver.RestConfigProvider, orgService org.Service) *CleanUpService { + tempUserService tempuser.Service, tracer tracing.Tracer, annotationCleaner annotations.Cleaner, service AlertRuleService, clientConfigProvider grafanaapiserver.RestConfigProvider, orgService org.Service, teamService team.Service, dataSourceService datasources.DataSourceService) *CleanUpService { s := &CleanUpService{ Cfg: cfg, Features: Features, @@ -81,6 +85,8 @@ func ProvideService(cfg *setting.Cfg, Features featuremgmt.FeatureToggles, serve alertRuleService: service, clientConfigProvider: clientConfigProvider, orgService: orgService, + teamService: teamService, + dataSourceService: dataSourceService, } return s } @@ -125,6 +131,7 @@ func (srv *CleanUpService) clean(ctx context.Context) { {"expire old user invites", srv.expireOldUserInvites}, {"delete stale query history", srv.deleteStaleQueryHistory}, {"expire old email verifications", srv.expireOldVerifications}, + {"cleanup stale LBAC rules", srv.cleanupStaleLBACRules}, } if srv.Cfg.ShortLinkExpiration > 0 { @@ -418,3 +425,133 @@ func (srv *CleanUpService) cleanUpTrashAlertRules(ctx context.Context) { logger.Debug("Cleaned up deleted alert rules", "rows affected", affected) } } + +// cleanupStaleLBACRules exists to clean up lbac rules that are stale from teams getting deleted as we do not have +// cascading deletions on teams to delete existing lbac rules +func (srv *CleanUpService) cleanupStaleLBACRules(ctx context.Context) { + logger := srv.log.FromContext(ctx) + + // Get all datasources + allDataSources, err := srv.dataSourceService.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{}) + if err != nil { + logger.Error("Failed to get datasources for LBAC cleanup", "error", err) + return + } + + var totalCleaned int + var totalDataSources int + + for _, ds := range allDataSources { + if ds.JsonData == nil { + continue + } + + // Check if datasource has team LBAC rules + teamHTTPHeaders, err := datasources.GetTeamHTTPHeaders(ds.JsonData) + if err != nil || teamHTTPHeaders == nil { + continue + } + + totalDataSources++ + + // Extract team UIDs and check if teams still exist + cleanedRules, removedCount := srv.getLBACRulesForTeamsStillExisting(ctx, teamHTTPHeaders, ds.OrgID) + + if removedCount > 0 { + // Update the datasource with cleaned rules + err := srv.updateDataSourceLBACRules(ctx, ds, cleanedRules) + if err != nil { + logger.Error("Failed to update datasource LBAC rules", + "datasource", ds.UID, "error", err) + } else { + totalCleaned += removedCount + logger.Debug("Cleaned stale LBAC rules", + "datasource", ds.UID, "removed", removedCount) + } + } + } + + if totalCleaned > 0 { + logger.Info("Cleaned up stale team LBAC rules", + "datasources_processed", totalDataSources, + "total_rules_removed", totalCleaned) + } +} + +func (srv *CleanUpService) getLBACRulesForTeamsStillExisting(ctx context.Context, teamHeaders *datasources.TeamHTTPHeaders, orgID int64) (*datasources.TeamHTTPHeaders, int) { + logger := srv.log.FromContext(ctx) + cleanedHeaders := &datasources.TeamHTTPHeaders{Headers: make(map[string][]datasources.TeamHTTPHeader)} + removedCount := 0 + + for teamIdentifier, headers := range teamHeaders.Headers { + // Determine if this is a UID or ID + var teamUID string + teamID, err := strconv.ParseInt(teamIdentifier, 10, 64) + + if err != nil { + // It's a UID + teamUID = teamIdentifier + } else { + // It's an ID, need to resolve to UID + teamByID, err := srv.teamService.GetTeamByID(ctx, &team.GetTeamByIDQuery{ + OrgID: orgID, + ID: teamID, + }) + if err != nil { + logger.Debug("Team ID no longer exists, removing LBAC rules", + "teamID", teamIdentifier, "orgID", orgID) + removedCount++ + continue + } + teamUID = teamByID.UID + } + + // Check if team still exists by UID + _, err = srv.teamService.GetTeamByID(ctx, &team.GetTeamByIDQuery{ + OrgID: orgID, + UID: teamUID, + }) + + if err != nil { + logger.Debug("Team UID no longer exists, removing LBAC rules", + "teamUID", teamUID, "orgID", orgID) + removedCount++ + continue + } + + // Team exists, keep the rules + cleanedHeaders.Headers[teamIdentifier] = headers + } + + return cleanedHeaders, removedCount +} + +func (srv *CleanUpService) updateDataSourceLBACRules(ctx context.Context, ds *datasources.DataSource, cleanedHeaders *datasources.TeamHTTPHeaders) error { + // Update JsonData with cleaned rules + jsonData := ds.JsonData + jsonData.Set("teamHttpHeaders", cleanedHeaders) + + updateCmd := &datasources.UpdateDataSourceCommand{ + ID: ds.ID, + OrgID: ds.OrgID, + UID: ds.UID, + Name: ds.Name, + Type: ds.Type, + Access: ds.Access, + URL: ds.URL, + User: ds.User, + Database: ds.Database, + BasicAuth: ds.BasicAuth, + BasicAuthUser: ds.BasicAuthUser, + WithCredentials: ds.WithCredentials, + IsDefault: ds.IsDefault, + JsonData: jsonData, + AllowLBACRuleUpdates: true, + Version: ds.Version, + ReadOnly: ds.ReadOnly, + APIVersion: ds.APIVersion, + } + + _, err := srv.dataSourceService.UpdateDataSource(ctx, updateCmd) + return err +}