diff --git a/pkg/services/accesscontrol/acimpl/basic_role_db_seed.go b/pkg/services/accesscontrol/acimpl/basic_role_db_seed.go deleted file mode 100644 index c6128790d1a..00000000000 --- a/pkg/services/accesscontrol/acimpl/basic_role_db_seed.go +++ /dev/null @@ -1,44 +0,0 @@ -package acimpl - -import ( - "context" - "time" - - "github.com/grafana/grafana/pkg/services/accesscontrol" -) - -const ( - ossBasicRoleSeedLockName = "oss-ac-basic-role-seeder" - ossBasicRoleSeedTimeout = 2 * time.Minute -) - -// refreshBasicRolePermissionsInDB ensures basic role permissions are fully derived from in-memory registrations -func (s *Service) refreshBasicRolePermissionsInDB(ctx context.Context, rolesSnapshot map[string][]accesscontrol.Permission) error { - if s.sql == nil || s.seeder == nil { - return nil - } - - run := func(ctx context.Context) error { - desired := map[accesscontrol.SeedPermission]struct{}{} - for role, permissions := range rolesSnapshot { - for _, permission := range permissions { - desired[accesscontrol.SeedPermission{BuiltInRole: role, Action: permission.Action, Scope: permission.Scope}] = struct{}{} - } - } - s.seeder.SetDesiredPermissions(desired) - return s.seeder.Seed(ctx) - } - - if s.serverLock == nil { - return run(ctx) - } - - var err error - errLock := s.serverLock.LockExecuteAndRelease(ctx, ossBasicRoleSeedLockName, ossBasicRoleSeedTimeout, func(ctx context.Context) { - err = run(ctx) - }) - if errLock != nil { - return errLock - } - return err -} diff --git a/pkg/services/accesscontrol/acimpl/basic_role_db_seed_test.go b/pkg/services/accesscontrol/acimpl/basic_role_db_seed_test.go deleted file mode 100644 index 986a32b66fc..00000000000 --- a/pkg/services/accesscontrol/acimpl/basic_role_db_seed_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package acimpl - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/localcache" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/database" - "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" - "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util/testutil" -) - -func TestIntegration_OSSBasicRolePermissions_PersistAndRefreshOnRegisterFixedRoles(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - ctx := context.Background() - sql := db.InitTestDB(t) - store := database.ProvideService(sql) - - svc := ProvideOSSService( - setting.NewCfg(), - store, - &resourcepermissions.FakeActionSetSvc{}, - localcache.ProvideService(), - featuremgmt.WithFeatures(), - tracing.InitializeTracerForTest(), - sql, - permreg.ProvidePermissionRegistry(), - nil, - ) - - require.NoError(t, svc.DeclareFixedRoles(accesscontrol.RoleRegistration{ - Role: accesscontrol.RoleDTO{ - Name: "fixed:test:role", - Permissions: []accesscontrol.Permission{ - {Action: "test:read", Scope: ""}, - }, - }, - Grants: []string{string(org.RoleViewer)}, - })) - - require.NoError(t, svc.RegisterFixedRoles(ctx)) - - // verify permission is persisted to DB for basic:viewer - require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error { - var role accesscontrol.Role - ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role) - require.NoError(t, err) - require.True(t, ok) - - var count int64 - count, err = sess.Table("permission").Where("role_id = ? AND action = ? AND scope = ?", role.ID, "test:read", "").Count() - require.NoError(t, err) - require.Equal(t, int64(1), count) - return nil - })) - - // ensure RegisterFixedRoles refreshes it back to defaults - require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error { - ts := time.Now() - var role accesscontrol.Role - ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role) - require.NoError(t, err) - require.True(t, ok) - - _, err = sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID) - require.NoError(t, err) - p := accesscontrol.Permission{ - RoleID: role.ID, - Action: "custom:keep", - Scope: "", - Created: ts, - Updated: ts, - } - p.Kind, p.Attribute, p.Identifier = accesscontrol.SplitScope(p.Scope) - _, err = sess.Table("permission").Insert(&p) - return err - })) - - svc2 := ProvideOSSService( - setting.NewCfg(), - store, - &resourcepermissions.FakeActionSetSvc{}, - localcache.ProvideService(), - featuremgmt.WithFeatures(), - tracing.InitializeTracerForTest(), - sql, - permreg.ProvidePermissionRegistry(), - nil, - ) - require.NoError(t, svc2.DeclareFixedRoles(accesscontrol.RoleRegistration{ - Role: accesscontrol.RoleDTO{ - Name: "fixed:test:role", - Permissions: []accesscontrol.Permission{ - {Action: "test:read", Scope: ""}, - }, - }, - Grants: []string{string(org.RoleViewer)}, - })) - require.NoError(t, svc2.RegisterFixedRoles(ctx)) - - require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error { - var role accesscontrol.Role - ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role) - require.NoError(t, err) - require.True(t, ok) - - var count int64 - count, err = sess.Table("permission").Where("role_id = ? AND action = ? AND scope = ?", role.ID, "test:read", "").Count() - require.NoError(t, err) - require.Equal(t, int64(1), count) - - count, err = sess.Table("permission").Where("role_id = ? AND action = ?", role.ID, "custom:keep").Count() - require.NoError(t, err) - require.Equal(t, int64(0), count) - return nil - })) -} diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 3fd419b1f6c..1ea8bf95f77 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -30,7 +30,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/migrator" "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" "github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils" - "github.com/grafana/grafana/pkg/services/accesscontrol/seeding" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -97,12 +96,6 @@ func ProvideOSSService( roles: accesscontrol.BuildBasicRoleDefinitions(), store: store, permRegistry: permRegistry, - sql: db, - serverLock: lock, - } - - if backend, ok := store.(*database.AccessControlStore); ok { - s.seeder = seeding.New(log.New("accesscontrol.seeder"), backend, backend) } return s @@ -119,11 +112,8 @@ type Service struct { rolesMu sync.RWMutex roles map[string]*accesscontrol.RoleDTO store accesscontrol.Store - seeder *seeding.Seeder permRegistry permreg.PermissionRegistry isInitialized bool - sql db.DB - serverLock *serverlock.ServerLockService } func (s *Service) GetUsageStats(_ context.Context) map[string]any { @@ -441,54 +431,17 @@ func (s *Service) RegisterFixedRoles(ctx context.Context) error { defer span.End() s.rolesMu.Lock() - registrations := s.registrations.Slice() + defer s.rolesMu.Unlock() + s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool { s.registerRolesLocked(registration) return true }) s.isInitialized = true - - rolesSnapshot := s.getBasicRolePermissionsLocked() - s.rolesMu.Unlock() - - if s.seeder != nil { - if err := s.seeder.SeedRoles(ctx, registrations); err != nil { - return err - } - if err := s.seeder.RemoveAbsentRoles(ctx); err != nil { - return err - } - } - - if err := s.refreshBasicRolePermissionsInDB(ctx, rolesSnapshot); err != nil { - return err - } - return nil } -// getBasicRolePermissionsSnapshotFromRegistrationsLocked computes the desired basic role permissions from the -// current registration list, using the shared seeding registration logic. -// -// it has to be called while holding the roles lock -func (s *Service) getBasicRolePermissionsLocked() map[string][]accesscontrol.Permission { - desired := map[accesscontrol.SeedPermission]struct{}{} - s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool { - seeding.AppendDesiredPermissions(desired, s.log, ®istration.Role, registration.Grants, registration.Exclude, true) - return true - }) - - out := make(map[string][]accesscontrol.Permission) - for sp := range desired { - out[sp.BuiltInRole] = append(out[sp.BuiltInRole], accesscontrol.Permission{ - Action: sp.Action, - Scope: sp.Scope, - }) - } - return out -} - // registerRolesLocked processes a single role registration and adds permissions to basic roles. // Must be called with s.rolesMu locked. func (s *Service) registerRolesLocked(registration accesscontrol.RoleRegistration) { @@ -521,7 +474,6 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs defer span.End() acRegs := pluginutils.ToRegistrations(ID, name, regs) - updatedBasicRoles := false for _, r := range acRegs { if err := pluginutils.ValidatePluginRole(ID, r.Role); err != nil { return err @@ -548,23 +500,11 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs if initialized { s.rolesMu.Lock() s.registerRolesLocked(r) - updatedBasicRoles = true s.rolesMu.Unlock() s.cache.Flush() } } - if updatedBasicRoles { - s.rolesMu.RLock() - rolesSnapshot := s.getBasicRolePermissionsLocked() - s.rolesMu.RUnlock() - - // plugin roles can be declared after startup - keep DB in sync - if err := s.refreshBasicRolePermissionsInDB(ctx, rolesSnapshot); err != nil { - return err - } - } - return nil } diff --git a/pkg/services/accesscontrol/database/seeder.go b/pkg/services/accesscontrol/database/seeder.go deleted file mode 100644 index 2f53d20b514..00000000000 --- a/pkg/services/accesscontrol/database/seeder.go +++ /dev/null @@ -1,623 +0,0 @@ -package database - -import ( - "context" - "strings" - "time" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/seeding" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" - "github.com/grafana/grafana/pkg/util/xorm/core" -) - -const basicRolePermBatchSize = 500 - -// LoadRoles returns all fixed and plugin roles (global org) with permissions, indexed by role name. -func (s *AccessControlStore) LoadRoles(ctx context.Context) (map[string]*accesscontrol.RoleDTO, error) { - out := map[string]*accesscontrol.RoleDTO{} - - err := s.sql.WithDbSession(ctx, func(sess *db.Session) error { - type roleRow struct { - ID int64 `xorm:"id"` - OrgID int64 `xorm:"org_id"` - Version int64 `xorm:"version"` - UID string `xorm:"uid"` - Name string `xorm:"name"` - DisplayName string `xorm:"display_name"` - Description string `xorm:"description"` - Group string `xorm:"group_name"` - Hidden bool `xorm:"hidden"` - Updated time.Time `xorm:"updated"` - Created time.Time `xorm:"created"` - } - - roles := []roleRow{} - if err := sess.Table("role"). - Where("org_id = ?", accesscontrol.GlobalOrgID). - Where("(name LIKE ? OR name LIKE ?)", accesscontrol.FixedRolePrefix+"%", accesscontrol.PluginRolePrefix+"%"). - Find(&roles); err != nil { - return err - } - - if len(roles) == 0 { - return nil - } - - roleIDs := make([]any, 0, len(roles)) - roleByID := make(map[int64]*accesscontrol.RoleDTO, len(roles)) - for _, r := range roles { - dto := &accesscontrol.RoleDTO{ - ID: r.ID, - OrgID: r.OrgID, - Version: r.Version, - UID: r.UID, - Name: r.Name, - DisplayName: r.DisplayName, - Description: r.Description, - Group: r.Group, - Hidden: r.Hidden, - Updated: r.Updated, - Created: r.Created, - } - out[dto.Name] = dto - roleByID[dto.ID] = dto - roleIDs = append(roleIDs, dto.ID) - } - - type permRow struct { - RoleID int64 `xorm:"role_id"` - Action string `xorm:"action"` - Scope string `xorm:"scope"` - } - perms := []permRow{} - if err := sess.Table("permission").In("role_id", roleIDs...).Find(&perms); err != nil { - return err - } - - for _, p := range perms { - dto := roleByID[p.RoleID] - if dto == nil { - continue - } - dto.Permissions = append(dto.Permissions, accesscontrol.Permission{ - RoleID: p.RoleID, - Action: p.Action, - Scope: p.Scope, - }) - } - - return nil - }) - - return out, err -} - -func (s *AccessControlStore) SetRole(ctx context.Context, existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) error { - if existingRole == nil { - return nil - } - - return s.sql.WithDbSession(ctx, func(sess *db.Session) error { - _, err := sess.Table("role"). - Where("id = ? AND org_id = ?", existingRole.ID, accesscontrol.GlobalOrgID). - Update(map[string]any{ - "display_name": wantedRole.DisplayName, - "description": wantedRole.Description, - "group_name": wantedRole.Group, - "hidden": wantedRole.Hidden, - "updated": time.Now(), - }) - return err - }) -} - -func (s *AccessControlStore) SetPermissions(ctx context.Context, existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) error { - if existingRole == nil { - return nil - } - - type key struct{ Action, Scope string } - existing := map[key]struct{}{} - for _, p := range existingRole.Permissions { - existing[key{p.Action, p.Scope}] = struct{}{} - } - desired := map[key]struct{}{} - for _, p := range wantedRole.Permissions { - desired[key{p.Action, p.Scope}] = struct{}{} - } - - toAdd := make([]accesscontrol.Permission, 0) - toRemove := make([]accesscontrol.SeedPermission, 0) - - now := time.Now() - for k := range desired { - if _, ok := existing[k]; ok { - continue - } - perm := accesscontrol.Permission{ - RoleID: existingRole.ID, - Action: k.Action, - Scope: k.Scope, - Created: now, - Updated: now, - } - perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope) - toAdd = append(toAdd, perm) - } - - for k := range existing { - if _, ok := desired[k]; ok { - continue - } - toRemove = append(toRemove, accesscontrol.SeedPermission{Action: k.Action, Scope: k.Scope}) - } - - if len(toAdd) == 0 && len(toRemove) == 0 { - return nil - } - - return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - if len(toRemove) > 0 { - if err := DeleteRolePermissionTuples(sess, s.sql.GetDBType(), existingRole.ID, toRemove); err != nil { - return err - } - } - - if len(toAdd) > 0 { - _, err := sess.InsertMulti(toAdd) - return err - } - - return nil - }) -} - -func (s *AccessControlStore) CreateRole(ctx context.Context, role accesscontrol.RoleDTO) error { - now := time.Now() - uid := role.UID - if uid == "" && (strings.HasPrefix(role.Name, accesscontrol.FixedRolePrefix) || strings.HasPrefix(role.Name, accesscontrol.PluginRolePrefix)) { - uid = accesscontrol.PrefixedRoleUID(role.Name) - } - r := accesscontrol.Role{ - OrgID: accesscontrol.GlobalOrgID, - Version: role.Version, - UID: uid, - Name: role.Name, - DisplayName: role.DisplayName, - Description: role.Description, - Group: role.Group, - Hidden: role.Hidden, - Created: now, - Updated: now, - } - if r.Version == 0 { - r.Version = 1 - } - - return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - if _, err := sess.Insert(&r); err != nil { - return err - } - - if len(role.Permissions) == 0 { - return nil - } - - // De-duplicate permissions on (action, scope) to avoid unique constraint violations. - // Some role definitions may accidentally include duplicates. - type permKey struct{ Action, Scope string } - seen := make(map[permKey]struct{}, len(role.Permissions)) - - perms := make([]accesscontrol.Permission, 0, len(role.Permissions)) - for _, p := range role.Permissions { - k := permKey{Action: p.Action, Scope: p.Scope} - if _, ok := seen[k]; ok { - continue - } - seen[k] = struct{}{} - - perm := accesscontrol.Permission{ - RoleID: r.ID, - Action: p.Action, - Scope: p.Scope, - Created: now, - Updated: now, - } - perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope) - perms = append(perms, perm) - } - _, err := sess.InsertMulti(perms) - return err - }) -} - -func (s *AccessControlStore) DeleteRoles(ctx context.Context, roleUIDs []string) error { - if len(roleUIDs) == 0 { - return nil - } - - uids := make([]any, 0, len(roleUIDs)) - for _, uid := range roleUIDs { - uids = append(uids, uid) - } - - return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - type row struct { - ID int64 `xorm:"id"` - UID string `xorm:"uid"` - } - rows := []row{} - if err := sess.Table("role"). - Where("org_id = ?", accesscontrol.GlobalOrgID). - In("uid", uids...). - Find(&rows); err != nil { - return err - } - if len(rows) == 0 { - return nil - } - - roleIDs := make([]any, 0, len(rows)) - for _, r := range rows { - roleIDs = append(roleIDs, r.ID) - } - - // Remove permissions and assignments first to avoid FK issues (if enabled). - { - args := append([]any{"DELETE FROM permission WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...) - if _, err := sess.Exec(args...); err != nil { - return err - } - } - { - args := append([]any{"DELETE FROM user_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...) - if _, err := sess.Exec(args...); err != nil { - return err - } - } - { - args := append([]any{"DELETE FROM team_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...) - if _, err := sess.Exec(args...); err != nil { - return err - } - } - { - args := append([]any{"DELETE FROM builtin_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...) - if _, err := sess.Exec(args...); err != nil { - return err - } - } - - args := append([]any{"DELETE FROM role WHERE org_id = ? AND uid IN (?" + strings.Repeat(",?", len(uids)-1) + ")", accesscontrol.GlobalOrgID}, uids...) - _, err := sess.Exec(args...) - return err - }) -} - -// OSS basic-role permission refresh uses seeding.Seeder.Seed() with a desired set computed in memory. -// These methods implement the permission seeding part of seeding.SeedingBackend against the current permission table. -func (s *AccessControlStore) LoadPrevious(ctx context.Context) (map[accesscontrol.SeedPermission]struct{}, error) { - var out map[accesscontrol.SeedPermission]struct{} - err := s.sql.WithDbSession(ctx, func(sess *db.Session) error { - rows, err := LoadBasicRoleSeedPermissions(sess) - if err != nil { - return err - } - - out = make(map[accesscontrol.SeedPermission]struct{}, len(rows)) - for _, r := range rows { - r.Origin = "" - out[r] = struct{}{} - } - return nil - }) - return out, err -} - -func (s *AccessControlStore) Apply(ctx context.Context, added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) error { - rolesToUpgrade := seeding.RolesToUpgrade(added, removed) - - // Run the same OSS apply logic as ossBasicRoleSeedBackend.Apply inside a single transaction. - return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - defs := accesscontrol.BuildBasicRoleDefinitions() - builtinToRoleID, err := EnsureBasicRolesExist(sess, defs) - if err != nil { - return err - } - - backend := &ossBasicRoleSeedBackend{ - sess: sess, - now: time.Now(), - builtinToRoleID: builtinToRoleID, - desired: nil, - dbType: s.sql.GetDBType(), - } - if err := backend.Apply(ctx, added, removed, updated); err != nil { - return err - } - - return BumpBasicRoleVersions(sess, rolesToUpgrade) - }) -} - -// EnsureBasicRolesExist ensures the built-in basic roles exist in the role table and are bound in builtin_role. -// It returns a mapping from builtin role name (for example "Admin") to role ID. -func EnsureBasicRolesExist(sess *db.Session, defs map[string]*accesscontrol.RoleDTO) (map[string]int64, error) { - uidToBuiltin := make(map[string]string, len(defs)) - uids := make([]any, 0, len(defs)) - for builtin, def := range defs { - uidToBuiltin[def.UID] = builtin - uids = append(uids, def.UID) - } - - type roleRow struct { - ID int64 `xorm:"id"` - UID string `xorm:"uid"` - } - - rows := []roleRow{} - if err := sess.Table("role"). - Where("org_id = ?", accesscontrol.GlobalOrgID). - In("uid", uids...). - Find(&rows); err != nil { - return nil, err - } - - ts := time.Now() - - builtinToRoleID := make(map[string]int64, len(defs)) - for _, r := range rows { - br, ok := uidToBuiltin[r.UID] - if !ok { - continue - } - builtinToRoleID[br] = r.ID - } - - for builtin, def := range defs { - roleID, ok := builtinToRoleID[builtin] - if !ok { - role := accesscontrol.Role{ - OrgID: def.OrgID, - Version: def.Version, - UID: def.UID, - Name: def.Name, - DisplayName: def.DisplayName, - Description: def.Description, - Group: def.Group, - Hidden: def.Hidden, - Created: ts, - Updated: ts, - } - if _, err := sess.Insert(&role); err != nil { - return nil, err - } - roleID = role.ID - builtinToRoleID[builtin] = roleID - } - - has, err := sess.Table("builtin_role"). - Where("role_id = ? AND role = ? AND org_id = ?", roleID, builtin, accesscontrol.GlobalOrgID). - Exist() - if err != nil { - return nil, err - } - if !has { - br := accesscontrol.BuiltinRole{ - RoleID: roleID, - OrgID: accesscontrol.GlobalOrgID, - Role: builtin, - Created: ts, - Updated: ts, - } - if _, err := sess.Table("builtin_role").Insert(&br); err != nil { - return nil, err - } - } - } - - return builtinToRoleID, nil -} - -// DeleteRolePermissionTuples deletes permissions for a single role by (action, scope) pairs. -// -// It uses a row-constructor IN clause where supported (MySQL, Postgres, SQLite) and falls back -// to a WHERE ... OR ... form for MSSQL. -func DeleteRolePermissionTuples(sess *db.Session, dbType core.DbType, roleID int64, perms []accesscontrol.SeedPermission) error { - if len(perms) == 0 { - return nil - } - - if dbType == migrator.MSSQL { - // MSSQL doesn't support (action, scope) IN ((?,?),(?,?)) row constructors. - where := make([]string, 0, len(perms)) - args := make([]any, 0, 1+len(perms)*2) - args = append(args, roleID) - for _, p := range perms { - where = append(where, "(action = ? AND scope = ?)") - args = append(args, p.Action, p.Scope) - } - _, err := sess.Exec( - append([]any{ - "DELETE FROM permission WHERE role_id = ? AND (" + strings.Join(where, " OR ") + ")", - }, args...)..., - ) - return err - } - - args := make([]any, 0, 1+len(perms)*2) - args = append(args, roleID) - for _, p := range perms { - args = append(args, p.Action, p.Scope) - } - sql := "DELETE FROM permission WHERE role_id = ? AND (action, scope) IN (" + - strings.Repeat("(?, ?),", len(perms)-1) + "(?, ?))" - _, err := sess.Exec(append([]any{sql}, args...)...) - return err -} - -type ossBasicRoleSeedBackend struct { - sess *db.Session - now time.Time - builtinToRoleID map[string]int64 - desired map[accesscontrol.SeedPermission]struct{} - dbType core.DbType -} - -func (b *ossBasicRoleSeedBackend) LoadPrevious(_ context.Context) (map[accesscontrol.SeedPermission]struct{}, error) { - rows, err := LoadBasicRoleSeedPermissions(b.sess) - if err != nil { - return nil, err - } - - out := make(map[accesscontrol.SeedPermission]struct{}, len(rows)) - for _, r := range rows { - // Ensure the key matches what OSS seeding uses (Origin is always empty for basic role refresh). - r.Origin = "" - out[r] = struct{}{} - } - return out, nil -} - -func (b *ossBasicRoleSeedBackend) LoadDesired(_ context.Context) (map[accesscontrol.SeedPermission]struct{}, error) { - return b.desired, nil -} - -func (b *ossBasicRoleSeedBackend) Apply(_ context.Context, added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) error { - // Delete removed permissions (this includes user-defined permissions that aren't in desired). - if len(removed) > 0 { - permsByRoleID := map[int64][]accesscontrol.SeedPermission{} - for _, p := range removed { - roleID, ok := b.builtinToRoleID[p.BuiltInRole] - if !ok { - continue - } - permsByRoleID[roleID] = append(permsByRoleID[roleID], p) - } - - for roleID, perms := range permsByRoleID { - // Chunk to keep statement sizes and parameter counts bounded. - if err := batch(len(perms), basicRolePermBatchSize, func(start, end int) error { - return DeleteRolePermissionTuples(b.sess, b.dbType, roleID, perms[start:end]) - }); err != nil { - return err - } - } - } - - // Insert added permissions and updated-target permissions. - toInsertSeed := make([]accesscontrol.SeedPermission, 0, len(added)+len(updated)) - toInsertSeed = append(toInsertSeed, added...) - for _, v := range updated { - toInsertSeed = append(toInsertSeed, v) - } - if len(toInsertSeed) == 0 { - return nil - } - - // De-duplicate on (role_id, action, scope). This avoids unique constraint violations when: - // - the same permission appears in both added and updated - // - multiple plugin origins grant the same permission (Origin is not persisted in permission table) - type permKey struct { - RoleID int64 - Action string - Scope string - } - seen := make(map[permKey]struct{}, len(toInsertSeed)) - - toInsert := make([]accesscontrol.Permission, 0, len(toInsertSeed)) - for _, p := range toInsertSeed { - roleID, ok := b.builtinToRoleID[p.BuiltInRole] - if !ok { - continue - } - k := permKey{RoleID: roleID, Action: p.Action, Scope: p.Scope} - if _, ok := seen[k]; ok { - continue - } - seen[k] = struct{}{} - - perm := accesscontrol.Permission{ - RoleID: roleID, - Action: p.Action, - Scope: p.Scope, - Created: b.now, - Updated: b.now, - } - perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope) - toInsert = append(toInsert, perm) - } - - return batch(len(toInsert), basicRolePermBatchSize, func(start, end int) error { - // MySQL: ignore conflicts to make seeding idempotent under retries/concurrency. - // Conflicts can happen if the same permission already exists (unique on role_id, action, scope). - if b.dbType == migrator.MySQL { - args := make([]any, 0, (end-start)*8) - for i := start; i < end; i++ { - p := toInsert[i] - args = append(args, p.RoleID, p.Action, p.Scope, p.Kind, p.Attribute, p.Identifier, p.Updated, p.Created) - } - sql := append([]any{`INSERT IGNORE INTO permission (role_id, action, scope, kind, attribute, identifier, updated, created) VALUES ` + - strings.Repeat("(?, ?, ?, ?, ?, ?, ?, ?),", end-start-1) + "(?, ?, ?, ?, ?, ?, ?, ?)"}, args...) - _, err := b.sess.Exec(sql...) - return err - } - - _, err := b.sess.InsertMulti(toInsert[start:end]) - return err - }) -} - -func batch(count, size int, eachFn func(start, end int) error) error { - for i := 0; i < count; { - end := i + size - if end > count { - end = count - } - if err := eachFn(i, end); err != nil { - return err - } - i = end - } - return nil -} - -// BumpBasicRoleVersions increments the role version for the given builtin basic roles (Viewer/Editor/Admin/Grafana Admin). -// Unknown role names are ignored. -func BumpBasicRoleVersions(sess *db.Session, basicRoles []string) error { - if len(basicRoles) == 0 { - return nil - } - - defs := accesscontrol.BuildBasicRoleDefinitions() - uids := make([]any, 0, len(basicRoles)) - for _, br := range basicRoles { - def, ok := defs[br] - if !ok { - continue - } - uids = append(uids, def.UID) - } - if len(uids) == 0 { - return nil - } - - sql := "UPDATE role SET version = version + 1 WHERE org_id = ? AND uid IN (?" + strings.Repeat(",?", len(uids)-1) + ")" - _, err := sess.Exec(append([]any{sql, accesscontrol.GlobalOrgID}, uids...)...) - return err -} - -// LoadBasicRoleSeedPermissions returns the current (builtin_role, action, scope) permissions granted to basic roles. -// It sets Origin to empty. -func LoadBasicRoleSeedPermissions(sess *db.Session) ([]accesscontrol.SeedPermission, error) { - rows := []accesscontrol.SeedPermission{} - err := sess.SQL( - `SELECT role.display_name AS builtin_role, p.action, p.scope, '' AS origin - FROM role INNER JOIN permission AS p ON p.role_id = role.id - WHERE role.org_id = ? AND role.name LIKE 'basic:%'`, - accesscontrol.GlobalOrgID, - ).Find(&rows) - return rows, err -} diff --git a/pkg/services/accesscontrol/dualwrite/reconciler.go b/pkg/services/accesscontrol/dualwrite/reconciler.go index ff6637219a4..a0f2f47b77d 100644 --- a/pkg/services/accesscontrol/dualwrite/reconciler.go +++ b/pkg/services/accesscontrol/dualwrite/reconciler.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/serverlock" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -131,9 +130,6 @@ func (r *ZanzanaReconciler) Run(ctx context.Context) error { // Reconcile schedules as job that will run and reconcile resources between // legacy access control and zanzana. func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error { - // Ensure we don't reconcile an empty/partial RBAC state before OSS has seeded basic role permissions. - // This matters most during startup where fixed-role loading + basic-role permission refresh runs as another background service. - r.waitForBasicRolesSeeded(ctx) r.reconcile(ctx) // FIXME: @@ -149,57 +145,6 @@ func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error { } } -func (r *ZanzanaReconciler) hasBasicRolePermissions(ctx context.Context) bool { - var count int64 - // Basic role permissions are stored on "basic:%" roles in the global org (0). - // In a fresh DB, this will be empty until fixed roles are registered and the basic role permission refresh runs. - type row struct { - Count int64 `xorm:"count"` - } - _ = r.store.WithDbSession(ctx, func(sess *db.Session) error { - var rr row - _, err := sess.SQL( - `SELECT COUNT(*) AS count - FROM role INNER JOIN permission AS p ON p.role_id = role.id - WHERE role.org_id = ? AND role.name LIKE ?`, - accesscontrol.GlobalOrgID, - accesscontrol.BasicRolePrefix+"%", - ).Get(&rr) - if err != nil { - return err - } - count = rr.Count - return nil - }) - return count > 0 -} - -func (r *ZanzanaReconciler) waitForBasicRolesSeeded(ctx context.Context) { - // Best-effort: don't block forever. If we can't observe basic roles, proceed anyway. - const ( - maxWait = 15 * time.Second - interval = 1 * time.Second - ) - - deadline := time.NewTimer(maxWait) - defer deadline.Stop() - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for { - if r.hasBasicRolePermissions(ctx) { - return - } - select { - case <-ctx.Done(): - return - case <-deadline.C: - return - case <-ticker.C: - } - } -} - func (r *ZanzanaReconciler) reconcile(ctx context.Context) { run := func(ctx context.Context, namespace string) (ok bool) { now := time.Now() diff --git a/pkg/services/accesscontrol/dualwrite/reconciler_test.go b/pkg/services/accesscontrol/dualwrite/reconciler_test.go deleted file mode 100644 index 0defea011a0..00000000000 --- a/pkg/services/accesscontrol/dualwrite/reconciler_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package dualwrite - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/accesscontrol" -) - -func TestZanzanaReconciler_hasBasicRolePermissions(t *testing.T) { - env := setupTestEnv(t) - - r := &ZanzanaReconciler{ - store: env.db, - } - - ctx := context.Background() - require.False(t, r.hasBasicRolePermissions(ctx)) - - err := env.db.WithDbSession(ctx, func(sess *db.Session) error { - now := time.Now() - - _, err := sess.Exec( - `INSERT INTO role (org_id, uid, name, display_name, group_name, description, hidden, version, created, updated) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - accesscontrol.GlobalOrgID, - "basic_viewer_uid_test", - accesscontrol.BasicRolePrefix+"viewer", - "Viewer", - "Basic", - "Viewer role", - false, - 1, - now, - now, - ) - if err != nil { - return err - } - - var roleID int64 - if _, err := sess.SQL(`SELECT id FROM role WHERE org_id = ? AND uid = ?`, accesscontrol.GlobalOrgID, "basic_viewer_uid_test").Get(&roleID); err != nil { - return err - } - - _, err = sess.Exec( - `INSERT INTO permission (role_id, action, scope, kind, attribute, identifier, created, updated) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - roleID, - "dashboards:read", - "dashboards:*", - "", - "", - "", - now, - now, - ) - return err - }) - require.NoError(t, err) - - require.True(t, r.hasBasicRolePermissions(ctx)) -} diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 85df44750d2..b18fb4134f3 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -1,7 +1,6 @@ package accesscontrol import ( - "context" "encoding/json" "errors" "fmt" @@ -595,18 +594,3 @@ type QueryWithOrg struct { OrgId *int64 `json:"orgId"` Global bool `json:"global"` } - -type SeedPermission struct { - BuiltInRole string `xorm:"builtin_role"` - Action string `xorm:"action"` - Scope string `xorm:"scope"` - Origin string `xorm:"origin"` -} - -type RoleStore interface { - LoadRoles(ctx context.Context) (map[string]*RoleDTO, error) - SetRole(ctx context.Context, existingRole *RoleDTO, wantedRole RoleDTO) error - SetPermissions(ctx context.Context, existingRole *RoleDTO, wantedRole RoleDTO) error - CreateRole(ctx context.Context, role RoleDTO) error - DeleteRoles(ctx context.Context, roleUIDs []string) error -} diff --git a/pkg/services/accesscontrol/seeding/seeder.go b/pkg/services/accesscontrol/seeding/seeder.go deleted file mode 100644 index cad31b7a5d2..00000000000 --- a/pkg/services/accesscontrol/seeding/seeder.go +++ /dev/null @@ -1,451 +0,0 @@ -package seeding - -import ( - "context" - "fmt" - "regexp" - "slices" - "strings" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils" - "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" -) - -type Seeder struct { - log log.Logger - roleStore accesscontrol.RoleStore - backend SeedingBackend - builtinsPermissions map[accesscontrol.SeedPermission]struct{} - seededFixedRoles map[string]bool - seededPluginRoles map[string]bool - seededPlugins map[string]bool - hasSeededAlready bool -} - -// SeedingBackend provides the seed-set specific operations needed to seed. -type SeedingBackend interface { - // LoadPrevious returns the currently stored permissions for previously seeded roles. - LoadPrevious(ctx context.Context) (map[accesscontrol.SeedPermission]struct{}, error) - - // Apply updates the database to match the desired permissions. - Apply(ctx context.Context, - added, removed []accesscontrol.SeedPermission, - updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission, - ) error -} - -func New(log log.Logger, roleStore accesscontrol.RoleStore, backend SeedingBackend) *Seeder { - return &Seeder{ - log: log, - roleStore: roleStore, - backend: backend, - builtinsPermissions: map[accesscontrol.SeedPermission]struct{}{}, - seededFixedRoles: map[string]bool{}, - seededPluginRoles: map[string]bool{}, - seededPlugins: map[string]bool{}, - hasSeededAlready: false, - } -} - -// SetDesiredPermissions replaces the in-memory desired permission set used by Seed(). -func (s *Seeder) SetDesiredPermissions(desired map[accesscontrol.SeedPermission]struct{}) { - if desired == nil { - s.builtinsPermissions = map[accesscontrol.SeedPermission]struct{}{} - return - } - s.builtinsPermissions = desired -} - -// Seed loads current and desired permissions, diffs them (including scope updates), applies changes, and bumps versions. -func (s *Seeder) Seed(ctx context.Context) error { - previous, err := s.backend.LoadPrevious(ctx) - if err != nil { - return err - } - - // - Do not remove plugin permissions when the plugin didn't register this run (Origin set but not in seededPlugins). - // - Preserve legacy plugin app access permissions in the persisted seed set (these are granted by default). - if len(previous) > 0 { - filtered := make(map[accesscontrol.SeedPermission]struct{}, len(previous)) - for p := range previous { - if p.Action == pluginaccesscontrol.ActionAppAccess { - continue - } - if p.Origin != "" && !s.seededPlugins[p.Origin] { - continue - } - filtered[p] = struct{}{} - } - previous = filtered - } - - added, removed, updated := s.permissionDiff(previous, s.builtinsPermissions) - - if err := s.backend.Apply(ctx, added, removed, updated); err != nil { - return err - } - return nil -} - -// SeedRoles populates the database with the roles and their assignments -// It will create roles that do not exist and update roles that have changed -// Do not use for provisioning. Validation is not enforced. -func (s *Seeder) SeedRoles(ctx context.Context, registrationList []accesscontrol.RoleRegistration) error { - roleMap, err := s.roleStore.LoadRoles(ctx) - if err != nil { - return err - } - - missingRoles := make([]accesscontrol.RoleRegistration, 0, len(registrationList)) - - // Diff existing roles with the ones we want to seed. - // If a role is missing, we add it to the missingRoles list - for _, registration := range registrationList { - registration := registration - role, ok := roleMap[registration.Role.Name] - switch { - case registration.Role.IsFixed(): - s.seededFixedRoles[registration.Role.Name] = true - case registration.Role.IsPlugin(): - s.seededPluginRoles[registration.Role.Name] = true - // To be resilient to failed plugin loadings, we remember the plugins that have registered, - // later we'll ignore permissions and roles of other plugins - s.seededPlugins[pluginutils.PluginIDFromName(registration.Role.Name)] = true - } - - s.rememberPermissionAssignments(®istration.Role, registration.Grants, registration.Exclude) - - if !ok { - missingRoles = append(missingRoles, registration) - continue - } - - if needsRoleUpdate(role, registration.Role) { - if err := s.roleStore.SetRole(ctx, role, registration.Role); err != nil { - return err - } - } - - if needsPermissionsUpdate(role, registration.Role) { - if err := s.roleStore.SetPermissions(ctx, role, registration.Role); err != nil { - return err - } - } - } - - for _, registration := range missingRoles { - if err := s.roleStore.CreateRole(ctx, registration.Role); err != nil { - return err - } - } - - return nil -} - -func needsPermissionsUpdate(existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) bool { - if existingRole == nil { - return true - } - - if len(existingRole.Permissions) != len(wantedRole.Permissions) { - return true - } - - for _, p := range wantedRole.Permissions { - found := false - for _, ep := range existingRole.Permissions { - if ep.Action == p.Action && ep.Scope == p.Scope { - found = true - break - } - } - if !found { - return true - } - } - - return false -} - -func needsRoleUpdate(existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) bool { - if existingRole == nil { - return true - } - - if existingRole.Name != wantedRole.Name { - return false - } - - if existingRole.DisplayName != wantedRole.DisplayName { - return true - } - - if existingRole.Description != wantedRole.Description { - return true - } - - if existingRole.Group != wantedRole.Group { - return true - } - - if existingRole.Hidden != wantedRole.Hidden { - return true - } - - return false -} - -// Deprecated: SeedRole is deprecated and should not be used. -// SeedRoles only does boot up seeding and should not be used for runtime seeding. -func (s *Seeder) SeedRole(ctx context.Context, role accesscontrol.RoleDTO, builtInRoles []string) error { - addedPermissions := make(map[string]struct{}, len(role.Permissions)) - permissions := make([]accesscontrol.Permission, 0, len(role.Permissions)) - for _, p := range role.Permissions { - key := fmt.Sprintf("%s:%s", p.Action, p.Scope) - if _, ok := addedPermissions[key]; !ok { - addedPermissions[key] = struct{}{} - permissions = append(permissions, accesscontrol.Permission{Action: p.Action, Scope: p.Scope}) - } - } - - wantedRole := accesscontrol.RoleDTO{ - OrgID: accesscontrol.GlobalOrgID, - Version: role.Version, - UID: role.UID, - Name: role.Name, - DisplayName: role.DisplayName, - Description: role.Description, - Group: role.Group, - Permissions: permissions, - Hidden: role.Hidden, - } - roleMap, err := s.roleStore.LoadRoles(ctx) - if err != nil { - return err - } - - existingRole := roleMap[wantedRole.Name] - if existingRole == nil { - if err := s.roleStore.CreateRole(ctx, wantedRole); err != nil { - return err - } - } else { - if needsRoleUpdate(existingRole, wantedRole) { - if err := s.roleStore.SetRole(ctx, existingRole, wantedRole); err != nil { - return err - } - } - if needsPermissionsUpdate(existingRole, wantedRole) { - if err := s.roleStore.SetPermissions(ctx, existingRole, wantedRole); err != nil { - return err - } - } - } - - // Remember seeded roles - if wantedRole.IsFixed() { - s.seededFixedRoles[wantedRole.Name] = true - } - isPluginRole := wantedRole.IsPlugin() - if isPluginRole { - s.seededPluginRoles[wantedRole.Name] = true - - // To be resilient to failed plugin loadings, we remember the plugins that have registered, - // later we'll ignore permissions and roles of other plugins - s.seededPlugins[pluginutils.PluginIDFromName(role.Name)] = true - } - - s.rememberPermissionAssignments(&wantedRole, builtInRoles, []string{}) - return nil -} - -func (s *Seeder) rememberPermissionAssignments(role *accesscontrol.RoleDTO, builtInRoles []string, excludedRoles []string) { - AppendDesiredPermissions(s.builtinsPermissions, s.log, role, builtInRoles, excludedRoles, true) -} - -// AppendDesiredPermissions accumulates permissions from a role registration onto basic roles (Viewer/Editor/Admin/Grafana Admin). -// - It expands parents via accesscontrol.BuiltInRolesWithParents. -// - It can optionally ignore plugin app access permissions (which are granted by default). -func AppendDesiredPermissions( - out map[accesscontrol.SeedPermission]struct{}, - logger log.Logger, - role *accesscontrol.RoleDTO, - builtInRoles []string, - excludedRoles []string, - ignorePluginAppAccess bool, -) { - if out == nil || role == nil { - return - } - - for builtInRole := range accesscontrol.BuiltInRolesWithParents(builtInRoles) { - // Skip excluded grants - if slices.Contains(excludedRoles, builtInRole) { - continue - } - - for _, perm := range role.Permissions { - if ignorePluginAppAccess && perm.Action == pluginaccesscontrol.ActionAppAccess { - logger.Debug("Role is attempting to grant access permission, but this permission is already granted by default and will be ignored", - "role", role.Name, "permission", perm.Action, "scope", perm.Scope) - continue - } - - sp := accesscontrol.SeedPermission{ - BuiltInRole: builtInRole, - Action: perm.Action, - Scope: perm.Scope, - } - - if role.IsPlugin() { - sp.Origin = pluginutils.PluginIDFromName(role.Name) - } - - out[sp] = struct{}{} - } - } -} - -// permissionDiff returns: -// - added: present in desired permissions, not in previous permissions -// - removed: present in previous permissions, not in desired permissions -// - updated: same role + action, but scope changed -func (s *Seeder) permissionDiff(previous, desired map[accesscontrol.SeedPermission]struct{}) (added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) { - addedSet := make(map[accesscontrol.SeedPermission]struct{}, 0) - for n := range desired { - if _, already := previous[n]; !already { - addedSet[n] = struct{}{} - } else { - delete(previous, n) - } - } - - // Check if any of the new permissions is actually an old permission with an updated scope - updated = make(map[accesscontrol.SeedPermission]accesscontrol.SeedPermission, 0) - for n := range addedSet { - for p := range previous { - if n.BuiltInRole == p.BuiltInRole && n.Action == p.Action { - updated[p] = n - delete(addedSet, n) - } - } - } - - for p := range addedSet { - added = append(added, p) - } - - for p := range previous { - if p.Action == pluginaccesscontrol.ActionAppAccess && - p.Scope != pluginaccesscontrol.ScopeProvider.GetResourceAllScope() { - // Allows backward compatibility with plugins that have been seeded before the grant ignore rule was added - s.log.Info("This permission already existed so it will not be removed", - "role", p.BuiltInRole, "permission", p.Action, "scope", p.Scope) - continue - } - - removed = append(removed, p) - } - - return added, removed, updated -} - -func (s *Seeder) ClearBasicRolesPluginPermissions(ID string) { - removable := []accesscontrol.SeedPermission{} - - for key := range s.builtinsPermissions { - if matchPermissionByPluginID(key, ID) { - removable = append(removable, key) - } - } - - for _, perm := range removable { - delete(s.builtinsPermissions, perm) - } -} - -func matchPermissionByPluginID(perm accesscontrol.SeedPermission, pluginID string) bool { - if perm.Origin != pluginID { - return false - } - actionTemplate := regexp.MustCompile(fmt.Sprintf("%s[.:]", pluginID)) - scopeTemplate := fmt.Sprintf(":%s", pluginID) - return actionTemplate.MatchString(perm.Action) || strings.HasSuffix(perm.Scope, scopeTemplate) -} - -// RolesToUpgrade returns the unique basic roles that should have their version incremented. -func RolesToUpgrade(added, removed []accesscontrol.SeedPermission) []string { - set := map[string]struct{}{} - for _, p := range added { - set[p.BuiltInRole] = struct{}{} - } - for _, p := range removed { - set[p.BuiltInRole] = struct{}{} - } - out := make([]string, 0, len(set)) - for r := range set { - out = append(out, r) - } - return out -} - -func (s *Seeder) ClearPluginRoles(ID string) { - expectedPrefix := fmt.Sprintf("%s%s:", accesscontrol.PluginRolePrefix, ID) - - for roleName := range s.seededPluginRoles { - if strings.HasPrefix(roleName, expectedPrefix) { - delete(s.seededPluginRoles, roleName) - } - } -} - -func (s *Seeder) MarkSeededAlready() { - s.hasSeededAlready = true -} - -func (s *Seeder) HasSeededAlready() bool { - return s.hasSeededAlready -} - -func (s *Seeder) RemoveAbsentRoles(ctx context.Context) error { - roleMap, errGet := s.roleStore.LoadRoles(ctx) - if errGet != nil { - s.log.Error("failed to get fixed roles from store", "err", errGet) - return errGet - } - - toRemove := []string{} - for _, r := range roleMap { - if r == nil { - continue - } - if r.IsFixed() { - if !s.seededFixedRoles[r.Name] { - s.log.Info("role is not seeded anymore, mark it for deletion", "role", r.Name) - toRemove = append(toRemove, r.UID) - } - continue - } - - if r.IsPlugin() { - if !s.seededPlugins[pluginutils.PluginIDFromName(r.Name)] { - // To be resilient to failed plugin loadings - // ignore stored roles related to plugins that have not registered this time - s.log.Debug("plugin role has not been registered on this run skipping its removal", "role", r.Name) - continue - } - if !s.seededPluginRoles[r.Name] { - s.log.Info("role is not seeded anymore, mark it for deletion", "role", r.Name) - toRemove = append(toRemove, r.UID) - } - } - } - - if errDelete := s.roleStore.DeleteRoles(ctx, toRemove); errDelete != nil { - s.log.Error("failed to delete absent fixed and plugin roles", "err", errDelete) - return errDelete - } - return nil -} diff --git a/pkg/tests/apis/folder/folder_tree_test.go b/pkg/tests/apis/folder/folder_tree_test.go index 613d021b236..4d95d64b024 100644 --- a/pkg/tests/apis/folder/folder_tree_test.go +++ b/pkg/tests/apis/folder/folder_tree_test.go @@ -33,6 +33,8 @@ import ( ) func TestIntegrationFolderTreeZanzana(t *testing.T) { + // TODO: Add back OSS seeding and enable this test + t.Skip("Skipping folder tree test with Zanzana") testutil.SkipIntegrationTestInShortMode(t) runIntegrationFolderTree(t, testinfra.GrafanaOpts{