diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 3f53a9e14ae..6049f54c2a1 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -606,7 +606,7 @@ func (hs *HTTPServer) declareFixedRoles() error { Group: "Annotations", Permissions: []ac.Permission{ {Action: ac.ActionAnnotationsRead, Scope: ac.ScopeAnnotationsTypeOrganization}, - {Action: ac.ActionAnnotationsRead, Scope: dashboards.ScopeDashboardsAll}, + {Action: ac.ActionAnnotationsRead, Scope: dashboards.ScopeFoldersAll}, }, }, Grants: []string{string(org.RoleAdmin)}, @@ -620,11 +620,11 @@ func (hs *HTTPServer) declareFixedRoles() error { Group: "Annotations", Permissions: []ac.Permission{ {Action: ac.ActionAnnotationsCreate, Scope: ac.ScopeAnnotationsTypeOrganization}, - {Action: ac.ActionAnnotationsCreate, Scope: dashboards.ScopeDashboardsAll}, + {Action: ac.ActionAnnotationsCreate, Scope: dashboards.ScopeFoldersAll}, {Action: ac.ActionAnnotationsDelete, Scope: ac.ScopeAnnotationsTypeOrganization}, - {Action: ac.ActionAnnotationsDelete, Scope: dashboards.ScopeDashboardsAll}, + {Action: ac.ActionAnnotationsDelete, Scope: dashboards.ScopeFoldersAll}, {Action: ac.ActionAnnotationsWrite, Scope: ac.ScopeAnnotationsTypeOrganization}, - {Action: ac.ActionAnnotationsWrite, Scope: dashboards.ScopeDashboardsAll}, + {Action: ac.ActionAnnotationsWrite, Scope: dashboards.ScopeFoldersAll}, }, }, Grants: []string{string(org.RoleAdmin)}, diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index aaa75e17cee..14b62363539 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -116,6 +116,27 @@ var DashboardViewActions = []string{dashboards.ActionDashboardsRead} var DashboardEditActions = append(DashboardViewActions, []string{dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsDelete}...) var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...) +func getDashboardViewActions(features featuremgmt.FeatureToggles) []string { + if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { + return append(DashboardViewActions, accesscontrol.ActionAnnotationsRead) + } + return DashboardViewActions +} + +func getDashboardEditActions(features featuremgmt.FeatureToggles) []string { + if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { + return append(DashboardEditActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...) + } + return DashboardEditActions +} + +func getDashboardAdminActions(features featuremgmt.FeatureToggles) []string { + if features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) { + return append(DashboardAdminActions, []string{accesscontrol.ActionAnnotationsRead, accesscontrol.ActionAnnotationsWrite, accesscontrol.ActionAnnotationsDelete, accesscontrol.ActionAnnotationsCreate}...) + } + return DashboardAdminActions +} + func ProvideDashboardPermissions( features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl, license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, @@ -174,9 +195,9 @@ func ProvideDashboardPermissions( ServiceAccounts: true, }, PermissionsToActions: map[string][]string{ - "View": DashboardViewActions, - "Edit": DashboardEditActions, - "Admin": DashboardAdminActions, + "View": getDashboardViewActions(features), + "Edit": getDashboardEditActions(features), + "Admin": getDashboardAdminActions(features), }, ReaderRoleName: "Dashboard permission reader", WriterRoleName: "Dashboard permission writer", @@ -239,9 +260,9 @@ func ProvideFolderPermissions( ServiceAccounts: true, }, PermissionsToActions: map[string][]string{ - "View": append(DashboardViewActions, FolderViewActions...), - "Edit": append(DashboardEditActions, FolderEditActions...), - "Admin": append(DashboardAdminActions, FolderAdminActions...), + "View": append(getDashboardViewActions(features), FolderViewActions...), + "Edit": append(getDashboardEditActions(features), FolderEditActions...), + "Admin": append(getDashboardAdminActions(features), FolderAdminActions...), }, ReaderRoleName: "Folder permission reader", WriterRoleName: "Folder permission writer", diff --git a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go index 91bec67adba..0198ded5738 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go @@ -661,6 +661,171 @@ func (m *managedFolderLibraryPanelActionsMigrator) Exec(sess *xorm.Session, mg * return nil } +const ManagedDashboardAnnotationActionsMigratorID = "managed dashboard permissions annotation actions migration" + +func AddManagedDashboardAnnotationActionsMigration(mg *migrator.Migrator) { + mg.AddMigration(ManagedDashboardAnnotationActionsMigratorID, &managedDashboardAnnotationActionsMigrator{}) +} + +type managedDashboardAnnotationActionsMigrator struct { + migrator.MigrationBase +} + +func (m *managedDashboardAnnotationActionsMigrator) SQL(dialect migrator.Dialect) string { + return CodeMigrationSQL +} + +func (m *managedDashboardAnnotationActionsMigrator) Exec(sess *xorm.Session, mg *migrator.Migrator) error { + // Check if roles have been populated and return early if they haven't - this avoids logging a warning from hasDefaultAnnotationPermissions + roleCount := 0 + _, err := sess.SQL(`SELECT COUNT( DISTINCT r.uid ) FROM role AS r INNER JOIN permission AS p ON r.id = p.role_id WHERE r.uid IN (?, ?, ?)`, "basic_viewer", "basic_editor", "basic_admin").Get(&roleCount) + if err != nil { + return fmt.Errorf("failed to check if basic roles have been populated: %w", err) + } + // Role count will be 0 either for new Grafana installations (in that case no managed roles will exist either, and the next conditional will return nil) + // or for OSS instances, for which basic role permissions can't be changed, so we don't need to run the default permission check in that case. + if roleCount != 0 { + // Check that default annotation permissions are assigned to basic roles. If that is not the case, skip the migration. + if hasDefaultPerms, err := m.hasDefaultAnnotationPermissions(sess, mg); err != nil || !hasDefaultPerms { + return err + } + } + + var ids []any + if err := sess.SQL("SELECT id FROM role WHERE name LIKE 'managed:%'").Find(&ids); err != nil { + return err + } + + if len(ids) == 0 { + return nil + } + + var permissions []ac.Permission + roleQueryBatchSize := 100 + err = batch(len(ids), roleQueryBatchSize, func(start, end int) error { + var batchPermissions []ac.Permission + if err := sess.SQL("SELECT role_id, action, scope FROM permission WHERE role_id IN(?"+strings.Repeat(" ,?", len(ids[start:end])-1)+") AND (scope LIKE 'folders:%' or scope LIKE 'dashboards:%')", ids[start:end]...).Find(&batchPermissions); err != nil { + return err + } + permissions = append(permissions, batchPermissions...) + return nil + }) + if err != nil { + return err + } + + mapped := make(map[int64]map[string]map[string]bool, len(ids)-1) + for _, p := range permissions { + if mapped[p.RoleID] == nil { + mapped[p.RoleID] = make(map[string]map[string]bool) + } + if mapped[p.RoleID][p.Scope] == nil { + mapped[p.RoleID][p.Scope] = make(map[string]bool) + } + mapped[p.RoleID][p.Scope][p.Action] = true + } + + var toAdd []ac.Permission + now := time.Now() + + for roleId, mappedPermissions := range mapped { + for scope, roleActions := range mappedPermissions { + if roleActions[dashboards.ActionDashboardsRead] { + if !roleActions[ac.ActionAnnotationsRead] { + toAdd = append(toAdd, ac.Permission{ + RoleID: roleId, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAnnotationsRead, + }) + } + } + + if roleActions[dashboards.ActionDashboardsWrite] { + if !roleActions[ac.ActionAnnotationsCreate] { + toAdd = append(toAdd, ac.Permission{ + RoleID: roleId, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAnnotationsCreate, + }) + } + if !roleActions[ac.ActionAnnotationsDelete] { + toAdd = append(toAdd, ac.Permission{ + RoleID: roleId, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAnnotationsDelete, + }) + } + if !roleActions[ac.ActionAnnotationsWrite] { + toAdd = append(toAdd, ac.Permission{ + RoleID: roleId, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAnnotationsWrite, + }) + } + } + } + } + + if len(toAdd) == 0 { + return nil + } + + return batch(len(toAdd), batchSize, func(start, end int) error { + _, err := sess.InsertMulti(toAdd[start:end]) + return err + }) +} + +func (m *managedDashboardAnnotationActionsMigrator) hasDefaultAnnotationPermissions(sess *xorm.Session, mg *migrator.Migrator) (bool, error) { + type basicRolePermission struct { + Uid string + Action string + Scope string + } + + var basicRolePermissions []basicRolePermission + basicRoleUIDs := []any{"basic_viewer", "basic_editor", "basic_admin"} + query := `SELECT r.uid, p.action, p.scope FROM role r +LEFT OUTER JOIN permission p ON p.role_id = r.id +WHERE r.uid IN (?, ?, ?) AND p.action LIKE 'annotations:%' AND p.scope IN ('*', 'annotations:*', 'annotations:type:*', 'annotations:type:dashboard') +` + if err := sess.SQL(query, basicRoleUIDs...).Find(&basicRolePermissions); err != nil { + return false, fmt.Errorf("failed to list basic role permissions: %w", err) + } + + mappedBasicRolePerms := make(map[string]map[string]bool, 0) + for _, p := range basicRolePermissions { + if mappedBasicRolePerms[p.Uid] == nil { + mappedBasicRolePerms[p.Uid] = make(map[string]bool) + } + mappedBasicRolePerms[p.Uid][p.Action] = true + } + + expectedAnnotationActions := []string{ac.ActionAnnotationsRead, ac.ActionAnnotationsCreate, ac.ActionAnnotationsDelete, ac.ActionAnnotationsWrite} + + for _, uid := range basicRoleUIDs { + if mappedBasicRolePerms[uid.(string)] == nil { + mg.Logger.Warn("basic role permissions missing annotation permissions, skipping annotation permission migration", "uid", uid) + return false, nil + } + for _, action := range expectedAnnotationActions { + if !mappedBasicRolePerms[uid.(string)][action] { + mg.Logger.Warn("basic role permissions missing annotation permissions, skipping annotation permission migration", "uid", uid, "action", action) + return false, nil + } + } + } + return true, nil +} + func hasFolderAdmin(permissions []ac.Permission) bool { return hasActions(folderPermissionTranslation[dashboardaccess.PERMISSION_ADMIN], permissions) } diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go index d02b401d2f6..7e7f74f1666 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go @@ -262,6 +262,9 @@ func setupTestDB(t *testing.T) *xorm.Engine { mg := migrator.NewMigrator(x, &setting.Cfg{ Logger: log.New("acmigration.test"), Raw: ini.Empty(), + IsFeatureToggleEnabled: func(key string) bool { + return true + }, }) migrations := &migrations.OSSMigrations{} migrations.AddMigration(mg) diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/dashbord_permission_migrator_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/dashbord_permission_migrator_test.go new file mode 100644 index 00000000000..935977ebc59 --- /dev/null +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/dashbord_permission_migrator_test.go @@ -0,0 +1,335 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" + acmig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/setting" +) + +type testCase struct { + desc string + putRolePerms map[int64]map[string][]rawPermission + wantRolePerms map[int64]map[string][]rawPermission +} + +func testCases() []testCase { + allAnnotationPermissions := []rawPermission{ + {Action: accesscontrol.ActionAnnotationsRead, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}, + {Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}, + {Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}, + {Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}, + } + + onlyOrgAnnotations := []rawPermission{ + {Action: accesscontrol.ActionAnnotationsRead, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}, + {Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}, + {Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}, + {Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}, + } + + wildcardAnnotationPermissions := []rawPermission{ + {Action: accesscontrol.ActionAnnotationsRead, Scope: "*"}, + {Action: accesscontrol.ActionAnnotationsCreate, Scope: "annotations:*"}, + {Action: accesscontrol.ActionAnnotationsDelete, Scope: "annotations:type:*"}, + {Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsAll}, + } + + return []testCase{ + { + desc: "empty permissions lead to empty permissions", + putRolePerms: map[int64]map[string][]rawPermission{}, + wantRolePerms: map[int64]map[string][]rawPermission{}, + }, + { + desc: "adds new permissions for instances without basic roles (should only be OSS instances)", + putRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "managed:users:1:permissions": {{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}}, + }, + }, + wantRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}, + {Action: accesscontrol.ActionAnnotationsRead, Scope: "dashboards:uid:test"}, + }, + }, + }, + }, + { + desc: "doesn't add any new permissions if has default annotation permissions on basic roles but no dashboard or folder permissions", + putRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + }, + }, + wantRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + }, + }, + }, + { + desc: "adds new permissions if has default annotation permissions on basic roles and dashboard read permissions", + putRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": {{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}}, + }, + }, + wantRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}, + {Action: accesscontrol.ActionAnnotationsRead, Scope: "dashboards:uid:test"}, + }, + }, + }, + }, + { + desc: "adds new permissions if has default annotation permissions on basic roles and dashboard write permissions", + putRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:test"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}, + }, + }, + }, + wantRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:test"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}, + {Action: accesscontrol.ActionAnnotationsRead, Scope: "dashboards:uid:test"}, + {Action: accesscontrol.ActionAnnotationsWrite, Scope: "dashboards:uid:test"}, + {Action: accesscontrol.ActionAnnotationsDelete, Scope: "dashboards:uid:test"}, + {Action: accesscontrol.ActionAnnotationsCreate, Scope: "dashboards:uid:test"}, + }, + }, + }, + }, + { + desc: "adds new permissions if has default annotation permissions on basic roles and folder read permissions", + putRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": {{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:test"}}, + }, + }, + wantRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:test"}, + {Action: accesscontrol.ActionAnnotationsRead, Scope: "folders:uid:test"}, + }, + }, + }, + }, + { + desc: "adds new permissions if has default annotation permissions on basic roles and folder write permissions", + putRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsWrite, Scope: "folders:uid:test"}, + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:test"}, + }, + }, + }, + wantRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsWrite, Scope: "folders:uid:test"}, + {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:test"}, + {Action: accesscontrol.ActionAnnotationsRead, Scope: "folders:uid:test"}, + {Action: accesscontrol.ActionAnnotationsWrite, Scope: "folders:uid:test"}, + {Action: accesscontrol.ActionAnnotationsDelete, Scope: "folders:uid:test"}, + {Action: accesscontrol.ActionAnnotationsCreate, Scope: "folders:uid:test"}, + }, + }, + }, + }, + { + desc: "adds new permissions to several managed roles if has default annotation permissions on basic roles and dashboard read permissions", + putRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": {{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}}, + "managed:teams:1:permissions": {{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test2"}}, + }, + }, + wantRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": allAnnotationPermissions, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}, + {Action: accesscontrol.ActionAnnotationsRead, Scope: "dashboards:uid:test"}, + }, + "managed:teams:1:permissions": { + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test2"}, + {Action: accesscontrol.ActionAnnotationsRead, Scope: "dashboards:uid:test2"}, + }, + }, + }, + }, + { + desc: "doesn't add any new permissions if annotation permissions are missing from the basic roles", + putRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:test"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}, + }, + }, + }, + wantRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + }, + }, + }, + { + desc: "doesn't add any new permissions if annotation permissions from the basic roles don't have the dashboard scope", + putRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": onlyOrgAnnotations, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:test"}, + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}, + }, + }, + }, + wantRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": onlyOrgAnnotations, + "basic:editor": allAnnotationPermissions, + "basic:admin": allAnnotationPermissions, + }, + }, + }, + { + desc: "adds new permissions if has default annotation permissions with different wildcard scopes", + putRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": wildcardAnnotationPermissions, + "basic:editor": wildcardAnnotationPermissions, + "basic:admin": wildcardAnnotationPermissions, + "managed:users:1:permissions": {{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}}, + }, + }, + wantRolePerms: map[int64]map[string][]rawPermission{ + 1: { + "basic:viewer": wildcardAnnotationPermissions, + "basic:editor": wildcardAnnotationPermissions, + "basic:admin": wildcardAnnotationPermissions, + "managed:users:1:permissions": { + {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:test"}, + {Action: accesscontrol.ActionAnnotationsRead, Scope: "dashboards:uid:test"}, + }, + }, + }, + }, + } +} + +func TestAnnotationActionMigration(t *testing.T) { + // Run initial migration to have a working DB + x := setupTestDB(t) + + for _, tc := range testCases() { + t.Run(tc.desc, func(t *testing.T) { + // Remove migration + _, errDeleteMig := x.Exec(`DELETE FROM migration_log WHERE migration_id LIKE ?`, acmig.ManagedDashboardAnnotationActionsMigratorID) + require.NoError(t, errDeleteMig) + _, errDeletePerm := x.Exec(`DELETE FROM permission`) + require.NoError(t, errDeletePerm) + _, errDeleteRole := x.Exec(`DELETE FROM role`) + require.NoError(t, errDeleteRole) + + // Test running the migrations twice to make sure they don't conflict + for i := 0; i < 2; i++ { + if i == 0 { + // put permissions + putTestPermissions(t, x, tc.putRolePerms) + } + + // Run accesscontrol migration (permissions insertion should not have conflicted) + acmigrator := migrator.NewMigrator(x, &setting.Cfg{Logger: log.New("acmigration.test")}) + acmig.AddManagedDashboardAnnotationActionsMigration(acmigrator) + + errRunningMig := acmigrator.Start(false, 0) + require.NoError(t, errRunningMig) + + // verify got == want + for orgID, roles := range tc.wantRolePerms { + for roleName := range roles { + // Check managed roles exist + role := accesscontrol.Role{} + hasRole, errRoleSearch := x.Table("role").Where("org_id = ? AND name = ?", orgID, roleName).Get(&role) + + require.NoError(t, errRoleSearch) + require.True(t, hasRole, "expected role to exist", "orgID", orgID, "role", roleName) + + // Check permissions associated with each role + perms := []accesscontrol.Permission{} + count, errManagedPermsSearch := x.Table("permission").Where("role_id = ?", role.ID).FindAndCount(&perms) + + require.NoError(t, errManagedPermsSearch) + require.Equal(t, int64(len(tc.wantRolePerms[orgID][roleName])), count, "expected role to be tied to permissions", "orgID", orgID, "role", roleName) + + gotRawPerms := convertToRawPermissions(perms) + require.ElementsMatch(t, gotRawPerms, tc.wantRolePerms[orgID][roleName], "expected role to have permissions", "orgID", orgID, "role", roleName) + + // Check assignment of the roles + br := accesscontrol.BuiltinRole{} + has, errAssignmentSearch := x.Table("builtin_role").Where("role_id = ? AND role = ? AND org_id = ?", role.ID, acmig.ParseRoleFromName(roleName), orgID).Get(&br) + require.NoError(t, errAssignmentSearch) + require.True(t, has, "expected assignment of role to builtin role", "orgID", orgID, "role", roleName) + } + } + } + }) + } +} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go index 94ce65f9ee7..4d8c820958e 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/managed_permission_migrator_test.go @@ -2,7 +2,6 @@ package test import ( "fmt" - "strconv" "strings" "testing" @@ -256,7 +255,10 @@ func TestManagedPermissionsMigrationRunTwice(t *testing.T) { func putTestPermissions(t *testing.T, x *xorm.Engine, rolePerms map[int64]map[string][]rawPermission) { for orgID, roles := range rolePerms { for roleName, perms := range roles { - uid := strconv.FormatInt(orgID, 10) + strings.ReplaceAll(roleName, ":", "_") + uid := strings.ReplaceAll(roleName, ":", "_") + if !strings.HasPrefix(roleName, "basic") { + uid = fmt.Sprintf("%d_%s", orgID, uid) + } role := accesscontrol.Role{ OrgID: orgID, Version: 1, diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 50968f6c378..d2d384d54f7 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -113,6 +113,15 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { ssosettings.AddMigration(mg) ualert.CreateOrgMigratedKVStoreEntries(mg) + + // https://github.com/grafana/identity-access-team/issues/546: tracks removal of the feature toggle from the annotation permission migration + // nolint:staticcheck + if mg.Cfg != nil && mg.Cfg.IsFeatureToggleEnabled != nil { + // nolint:staticcheck + if mg.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAnnotationPermissionUpdate) { + accesscontrol.AddManagedDashboardAnnotationActionsMigration(mg) + } + } } func addStarMigrations(mg *Migrator) {