mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
RBAC: prevent seeding oncall access (#80862)
* RBAC: prevent seeding oncall access * Add comments and an early exit * Test SeedAssignmentOnCallAccessMigrator * imports * Comment rework * Check error * Nit.
This commit is contained in:
parent
fd73b75ef7
commit
63679813b0
@ -6,6 +6,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
"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;
|
const migSQLITERoleNameNullable = `ALTER TABLE seed_assignment ADD COLUMN tmp_role_name VARCHAR(190) DEFAULT NULL;
|
||||||
UPDATE seed_assignment SET tmp_role_name = role_name;
|
UPDATE seed_assignment SET tmp_role_name = role_name;
|
||||||
ALTER TABLE seed_assignment DROP COLUMN 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}))
|
&migrator.Column{Name: "origin", Type: migrator.DB_Varchar, Length: 190, Nullable: true}))
|
||||||
|
|
||||||
mg.AddMigration("add origin to plugin seed_assignment", &seedAssignmentOnCallMigrator{})
|
mg.AddMigration("add origin to plugin seed_assignment", &seedAssignmentOnCallMigrator{})
|
||||||
|
mg.AddMigration(PreventSeedingOnCallAccessID, &SeedAssignmentOnCallAccessMigrator{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type seedAssignmentPrimaryKeyMigrator struct {
|
type seedAssignmentPrimaryKeyMigrator struct {
|
||||||
@ -143,3 +146,59 @@ func (m *seedAssignmentOnCallMigrator) Exec(sess *xorm.Session, mig *migrator.Mi
|
|||||||
)
|
)
|
||||||
return err
|
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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user