diff --git a/pkg/services/sqlstore/migrations/accesscontrol/seed_assignment.go b/pkg/services/sqlstore/migrations/accesscontrol/seed_assignment.go index efbee777aa4..c6307ce9f75 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/seed_assignment.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/seed_assignment.go @@ -6,6 +6,8 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) +const PreventSeedingOnCallAccessID = "prevent seeding OnCall access" + const migSQLITERoleNameNullable = `ALTER TABLE seed_assignment ADD COLUMN tmp_role_name VARCHAR(190) DEFAULT NULL; UPDATE seed_assignment SET tmp_role_name = role_name; ALTER TABLE seed_assignment DROP COLUMN role_name; @@ -47,6 +49,7 @@ func AddSeedAssignmentMigrations(mg *migrator.Migrator) { &migrator.Column{Name: "origin", Type: migrator.DB_Varchar, Length: 190, Nullable: true})) mg.AddMigration("add origin to plugin seed_assignment", &seedAssignmentOnCallMigrator{}) + mg.AddMigration(PreventSeedingOnCallAccessID, &SeedAssignmentOnCallAccessMigrator{}) } type seedAssignmentPrimaryKeyMigrator struct { @@ -143,3 +146,59 @@ func (m *seedAssignmentOnCallMigrator) Exec(sess *xorm.Session, mig *migrator.Mi ) return err } + +type SeedAssignmentOnCallAccessMigrator struct { + migrator.MigrationBase +} + +func (m *SeedAssignmentOnCallAccessMigrator) SQL(dialect migrator.Dialect) string { + return CodeMigrationSQL +} + +func (m *SeedAssignmentOnCallAccessMigrator) Exec(sess *xorm.Session, mig *migrator.Migrator) error { + // Check if the migration is necessary + hasEntry := 0 + if _, err := sess.SQL(`SELECT 1 FROM seed_assignment LIMIT 1`).Get(&hasEntry); err != nil { + return err + } + if hasEntry == 0 { + // Skip migration the seed assignment table has not been populated + // Hence the oncall access permission can be granted without any risk + return nil + } + + // Check if the permission has not already been seeded + // This is the case for instances that activated the accessControlOnCall feature already. + type SeedAssignment struct { + BuiltinRole, Action, Scope, Origin string + } + assigns := []SeedAssignment{} + err := sess.SQL(`SELECT builtin_role, action, scope, origin FROM seed_assignment WHERE action = ? AND scope = ?`, + "plugins.app:access", "plugins:id:grafana-oncall-app"). + Find(&assigns) + if err != nil { + return err + } + + basicRoles := map[string]bool{"Viewer": true, "Editor": true, "Admin": true, "Grafana Admin": true} + for i := range assigns { + delete(basicRoles, assigns[i].BuiltinRole) + } + if len(basicRoles) == 0 { + return nil + } + + // By default, basic roles have access to all app plugins; no need for extra permission. + // Mark OnCall Access permission as already seeded to prevent it from being added to basic roles. + toSeed := []SeedAssignment{} + for br := range basicRoles { + toSeed = append(toSeed, SeedAssignment{ + BuiltinRole: br, + Action: "plugins.app:access", + Scope: "plugins:id:grafana-oncall-app", + Origin: "grafana-oncall-app", + }) + } + _, err = sess.Table("seed_assignment").InsertMulti(&toSeed) + return err +} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/seed_assign_mig_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/seed_assign_mig_test.go new file mode 100644 index 00000000000..e95c5990854 --- /dev/null +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/seed_assign_mig_test.go @@ -0,0 +1,79 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + acmig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/setting" +) + +func TestPreventOnCallAccessSeed(t *testing.T) { + // Run initial migration to have a working DB + x := setupTestDB(t) + + type SeedAssignment struct { + BuiltinRole, Action, Scope, Origin string + } + + want := []SeedAssignment{ + {BuiltinRole: "Admin", Action: "plugins.app:access", Scope: "plugins:id:grafana-oncall-app", Origin: "grafana-oncall-app"}, + {BuiltinRole: "Editor", Action: "plugins.app:access", Scope: "plugins:id:grafana-oncall-app", Origin: "grafana-oncall-app"}, + {BuiltinRole: "Viewer", Action: "plugins.app:access", Scope: "plugins:id:grafana-oncall-app", Origin: "grafana-oncall-app"}, + {BuiltinRole: "Grafana Admin", Action: "plugins.app:access", Scope: "plugins:id:grafana-oncall-app", Origin: "grafana-oncall-app"}, + } + + type testCase struct { + desc string + init []SeedAssignment + want []SeedAssignment + } + tt := []testCase{ + { + desc: "fresh table skip migration", + want: []SeedAssignment{}, + }, + { + desc: "seeded with an OnCall access already", + init: []SeedAssignment{ + {BuiltinRole: "Admin", Action: "plugins.app:access", Scope: "plugins:id:grafana-oncall-app", Origin: "grafana-oncall-app"}, + }, + want: want, + }, + { + desc: "seeded without any OnCall access", + init: []SeedAssignment{{BuiltinRole: "Admin", Action: "plugins.app:access", Scope: "plugins:id:*"}}, + want: append(want, SeedAssignment{BuiltinRole: "Admin", Action: "plugins.app:access", Scope: "plugins:id:*"}), + }, + } + + for _, tc := range tt { + t.Run(tc.desc, func(t *testing.T) { + // Remove migration + _, errDeleteMig := x.Exec(`DELETE FROM migration_log WHERE migration_id LIKE ?`, acmig.PreventSeedingOnCallAccessID+"%") + require.NoError(t, errDeleteMig) + _, errDeleteAssigns := x.Exec(`DELETE FROM seed_assignment`) + require.NoError(t, errDeleteAssigns) + + if len(tc.init) > 0 { + _, errInsertAssign := x.Table("seed_assignment").InsertMulti(tc.init) + require.NoError(t, errInsertAssign) + } + + // Run accesscontrol migration + acmigrator := migrator.NewMigrator(x, &setting.Cfg{Logger: log.New("acmigration.test")}) + acmigrator.AddMigration(acmig.PreventSeedingOnCallAccessID, &acmig.SeedAssignmentOnCallAccessMigrator{}) + + errRunningMig := acmigrator.Start(false, 0) + require.NoError(t, errRunningMig) + + got := []SeedAssignment{} + errFind := x.Table("seed_assignment").Find(&got) + require.NoError(t, errFind) + require.ElementsMatch(t, tc.want, got) + }) + } +}