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:
Matthew Jacobson
2024-08-26 10:47:53 -04:00
committed by GitHub
parent 22ad1cc16f
commit 32f06c6d9c
36 changed files with 3583 additions and 548 deletions
@@ -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
}