Alerting: Receiver API complete core implementation (#91738)
* Replace global authz abstraction with one compatible with uid scope * Replace GettableApiReceiver with models.Receiver in receiver_svc * GrafanaIntegrationConfig -> models.Integration * Implement Create/Update methods * Add optimistic concurrency to receiver API * Add scope to ReceiversRead & ReceiversReadSecrets migrates existing permissions to include implicit global scope * Add receiver create, update, delete actions * Check if receiver is used by rules before delete * On receiver name change update in routes and notification settings * Improve errors * Linting * Include read permissions are requirements for create/update/delete * Alias ngalert/models to ngmodels to differentiate from v0alpha1 model * Ensure integration UIDs are valid, unique, and generated if empty * Validate integration settings on create/update * Leverage UidToName to GetReceiver instead of GetReceivers * Remove some unnecessary uses of simplejson * alerting.notifications.receiver -> alerting.notifications.receivers * validator -> provenanceValidator * Only validate the modified receiver stops existing invalid receivers from preventing modification of a valid receiver. * Improve error in Integration.Encrypt * Remove scope from alert.notifications.receivers:create * Add todos for receiver renaming * Use receiverAC precondition checks in k8s api * Linting * Optional optimistic concurrency for delete * make update-workspace * More specific auth checks in k8s authorize.go * Add debug log when delete optimistic concurrency is skipped * Improve error message on authorizer.DecisionDeny * Keep error for non-forbidden errutil errors
This commit is contained in:
@@ -127,6 +127,8 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) {
|
||||
ualert.AddStateResolvedAtColumns(mg)
|
||||
|
||||
enableTraceQLStreaming(mg, oss.features != nil && oss.features.IsEnabledGlobally(featuremgmt.FlagTraceQLStreaming))
|
||||
|
||||
ualert.AddReceiverActionScopesMigration(mg)
|
||||
}
|
||||
|
||||
func addStarMigrations(mg *Migrator) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package ualert
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
)
|
||||
|
||||
const (
|
||||
AlertingAddReceiverActionScopes = "Add scope to alert.notifications.receivers:read and alert.notifications.receivers.secrets:read"
|
||||
)
|
||||
|
||||
// AddReceiverActionScopesMigration is a migration that will add scopes to alert.notifications.receivers:read and
|
||||
// alert.notifications.receivers.secrets:read actions.
|
||||
// Originally, they were created without any scope, but treated as if all actions were globally scoped.
|
||||
// With the introduction of receiver FGAC, we need to scope these actions to UID so any existing roles should be updated
|
||||
// to explicitly have the global scope.
|
||||
func AddReceiverActionScopesMigration(mg *migrator.Migrator) {
|
||||
mg.AddMigration(AlertingAddReceiverActionScopes, &addReceiverActionScopesMigrator{})
|
||||
}
|
||||
|
||||
var _ migrator.CodeMigration = (*addReceiverActionScopesMigrator)(nil)
|
||||
|
||||
type addReceiverActionScopesMigrator struct {
|
||||
migrator.MigrationBase
|
||||
}
|
||||
|
||||
func (p addReceiverActionScopesMigrator) SQL(migrator.Dialect) string {
|
||||
return codeMigration
|
||||
}
|
||||
|
||||
func (p addReceiverActionScopesMigrator) Exec(sess *xorm.Session, migrator *migrator.Migrator) error {
|
||||
// Vendored.
|
||||
actionAlertingReceiversRead := "alert.notifications.receivers:read"
|
||||
actionAlertingReceiversReadSecrets := "alert.notifications.receivers.secrets:read"
|
||||
|
||||
_, err := sess.Exec("UPDATE permission SET `scope` = 'receivers:*', `kind` = 'receivers', `attribute` = '*', `identifier` = '*' WHERE action = ?", actionAlertingReceiversRead)
|
||||
if err != nil {
|
||||
migrator.Logger.Error("Failed to update permissions for action", "action", actionAlertingReceiversRead, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sess.Exec("UPDATE permission SET `scope` = 'receivers:*', `kind` = 'receivers', `attribute` = '*', `identifier` = '*' WHERE action = ?", actionAlertingReceiversReadSecrets)
|
||||
if err != nil {
|
||||
migrator.Logger.Error("Failed to update permissions for action", "action", actionAlertingReceiversReadSecrets, "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"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/sqlstore/migrations/ualert"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestScopeMigration(t *testing.T) {
|
||||
x := setupTestDB(t)
|
||||
now := time.Now()
|
||||
|
||||
// Vendored.
|
||||
actionAlertingReceiversRead := "alert.notifications.receivers:read"
|
||||
actionAlertingReceiversReadSecrets := "alert.notifications.receivers.secrets:read"
|
||||
|
||||
type migrationTestCase struct {
|
||||
desc string
|
||||
permissionSeed []*accesscontrol.Permission
|
||||
wantPermissions []*accesscontrol.Permission
|
||||
}
|
||||
testCases := []migrationTestCase{
|
||||
{
|
||||
desc: "convert existing alert.notifications.receivers:read regardless of scope",
|
||||
permissionSeed: []*accesscontrol.Permission{
|
||||
{
|
||||
RoleID: 1,
|
||||
Action: actionAlertingReceiversRead,
|
||||
Scope: "",
|
||||
Kind: "",
|
||||
Attribute: "",
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
RoleID: 2,
|
||||
Action: actionAlertingReceiversRead,
|
||||
Scope: "Scope",
|
||||
Kind: "Kind",
|
||||
Attribute: "Attribute",
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
},
|
||||
wantPermissions: []*accesscontrol.Permission{
|
||||
{
|
||||
RoleID: 1,
|
||||
Action: actionAlertingReceiversRead,
|
||||
Scope: "receivers:*",
|
||||
Kind: "receivers",
|
||||
Attribute: "*",
|
||||
Identifier: "*",
|
||||
},
|
||||
{
|
||||
RoleID: 2,
|
||||
Action: actionAlertingReceiversRead,
|
||||
Scope: "receivers:*",
|
||||
Kind: "receivers",
|
||||
Attribute: "*",
|
||||
Identifier: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "convert existing alert.notifications.receivers:read regardless of scope",
|
||||
permissionSeed: []*accesscontrol.Permission{
|
||||
{
|
||||
RoleID: 1,
|
||||
Action: actionAlertingReceiversReadSecrets,
|
||||
Scope: "",
|
||||
Kind: "",
|
||||
Attribute: "",
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
RoleID: 2,
|
||||
Action: actionAlertingReceiversReadSecrets,
|
||||
Scope: "Scope",
|
||||
Kind: "Kind",
|
||||
Attribute: "Attribute",
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
},
|
||||
wantPermissions: []*accesscontrol.Permission{
|
||||
{
|
||||
RoleID: 1,
|
||||
Action: actionAlertingReceiversReadSecrets,
|
||||
Scope: "receivers:*",
|
||||
Kind: "receivers",
|
||||
Attribute: "*",
|
||||
Identifier: "*",
|
||||
},
|
||||
{
|
||||
RoleID: 2,
|
||||
Action: actionAlertingReceiversReadSecrets,
|
||||
Scope: "receivers:*",
|
||||
Kind: "receivers",
|
||||
Attribute: "*",
|
||||
Identifier: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "empty perms",
|
||||
permissionSeed: []*accesscontrol.Permission{},
|
||||
wantPermissions: []*accesscontrol.Permission{},
|
||||
},
|
||||
{
|
||||
desc: "unrelated perms",
|
||||
permissionSeed: []*accesscontrol.Permission{
|
||||
{
|
||||
RoleID: 1,
|
||||
Action: "some.other.resource:read",
|
||||
Scope: "Scope",
|
||||
Kind: "Kind",
|
||||
Attribute: "Attribute",
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
},
|
||||
wantPermissions: []*accesscontrol.Permission{
|
||||
{
|
||||
RoleID: 1,
|
||||
Action: "some.other.resource:read",
|
||||
Scope: "Scope",
|
||||
Kind: "Kind",
|
||||
Attribute: "Attribute",
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
// Remove migration and permissions
|
||||
_, errDeleteMig := x.Exec(`DELETE FROM migration_log WHERE migration_id = ?`, ualert.AlertingAddReceiverActionScopes)
|
||||
require.NoError(t, errDeleteMig)
|
||||
_, errDeletePerms := x.Exec(`DELETE FROM permission`)
|
||||
require.NoError(t, errDeletePerms)
|
||||
|
||||
// seed DB with permissions
|
||||
if len(tc.permissionSeed) != 0 {
|
||||
permissionsCount, err := x.Insert(tc.permissionSeed)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(len(tc.permissionSeed)), permissionsCount)
|
||||
}
|
||||
|
||||
// Run RBAC action name migration
|
||||
acmigrator := migrator.NewMigrator(x, &setting.Cfg{Logger: log.New("acmigration.test")})
|
||||
ualert.AddReceiverActionScopesMigration(acmigrator)
|
||||
|
||||
errRunningMig := acmigrator.Start(false, 0)
|
||||
require.NoError(t, errRunningMig)
|
||||
|
||||
// Check permissions
|
||||
resultingPermissions := []*accesscontrol.Permission{}
|
||||
err := x.Table("permission").Find(&resultingPermissions)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify got == want
|
||||
cOpt := []cmp.Option{
|
||||
cmpopts.SortSlices(func(a, b accesscontrol.Permission) bool { return a.RoleID < b.RoleID }),
|
||||
cmpopts.IgnoreFields(accesscontrol.Permission{}, "ID", "Created", "Updated"),
|
||||
}
|
||||
if !cmp.Equal(tc.wantPermissions, resultingPermissions, cOpt...) {
|
||||
t.Errorf("Unexpected permissions: %v", cmp.Diff(tc.wantPermissions, resultingPermissions, cOpt...))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) *xorm.Engine {
|
||||
t.Helper()
|
||||
dbType := sqlutil.GetTestDBType()
|
||||
testDB, err := sqlutil.GetTestDB(dbType)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(testDB.Cleanup)
|
||||
|
||||
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
if err := x.Close(); err != nil {
|
||||
fmt.Printf("failed to close xorm engine: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
err = migrator.NewDialect(x.DriverName()).CleanDB(x)
|
||||
require.NoError(t, err)
|
||||
|
||||
mg := migrator.NewMigrator(x, &setting.Cfg{
|
||||
Logger: log.New("acmigration.test"),
|
||||
Raw: ini.Empty(),
|
||||
})
|
||||
migrations := &migrations.OSSMigrations{}
|
||||
migrations.AddMigration(mg)
|
||||
|
||||
err = mg.Start(false, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
return x
|
||||
}
|
||||
Reference in New Issue
Block a user