Alerting: Managed receiver resource permission in receiver_svc (#93556)

* Alerting: Managed receiver resource permission in receiver_svc
This commit is contained in:
Matthew Jacobson
2024-09-23 21:12:25 +03:00
committed by GitHub
parent ff37d477fd
commit 6652233493
16 changed files with 593 additions and 18 deletions
@@ -155,6 +155,8 @@ type ServiceAccountPermissionsService interface {
type ReceiverPermissionsService interface {
PermissionsService
SetDefaultPermissions(ctx context.Context, orgID int64, user identity.Requester, uid string)
CopyPermissions(ctx context.Context, orgID int64, user identity.Requester, oldUID, newUID string) (int, error)
}
type PermissionsService interface {
@@ -1,14 +1,22 @@
package ossaccesscontrol
import (
"context"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/ngalert"
alertingac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@@ -18,6 +26,14 @@ var ReceiversViewActions = []string{accesscontrol.ActionAlertingReceiversRead}
var ReceiversEditActions = append(ReceiversViewActions, []string{accesscontrol.ActionAlertingReceiversUpdate, accesscontrol.ActionAlertingReceiversDelete}...)
var ReceiversAdminActions = append(ReceiversEditActions, []string{accesscontrol.ActionAlertingReceiversReadSecrets, accesscontrol.ActionAlertingReceiversPermissionsRead, accesscontrol.ActionAlertingReceiversPermissionsWrite}...)
// defaultPermissions returns the default permissions for a newly created receiver.
func defaultPermissions() []accesscontrol.SetResourcePermissionCommand {
return []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: string(alertingac.ReceiverPermissionEdit)},
{BuiltinRole: string(org.RoleViewer), Permission: string(alertingac.ReceiverPermissionView)},
}
}
func ProvideReceiverPermissionsService(
cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl,
license licensing.Licensing, service accesscontrol.Service,
@@ -50,11 +66,131 @@ func ProvideReceiverPermissionsService(
if err != nil {
return nil, err
}
return &ReceiverPermissionsService{Service: srv}, nil
return &ReceiverPermissionsService{srv, service, log.New("resourcepermissions.receivers")}, nil
}
var _ accesscontrol.ReceiverPermissionsService = new(ReceiverPermissionsService)
type ReceiverPermissionsService struct {
*resourcepermissions.Service
ac accesscontrol.Service
log log.Logger
}
// SetDefaultPermissions sets the default permissions for a newly created receiver.
func (r ReceiverPermissionsService) SetDefaultPermissions(ctx context.Context, orgID int64, user identity.Requester, uid string) {
permissions := defaultPermissions()
clearCache := false
if user != nil && user.IsIdentityType(claims.TypeUser) {
userID, err := user.GetInternalID()
if err != nil {
r.log.Error("Could not make user admin", "receiver_uid", uid, "id", user.GetID(), "error", err)
} else {
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
UserID: userID, Permission: string(alertingac.ReceiverPermissionAdmin),
})
clearCache = true
}
}
if _, err := r.SetPermissions(ctx, orgID, uid, permissions...); err != nil {
r.log.Error("Could not set default permissions", "receiver_uid", uid, "error", err)
}
if clearCache {
// Clear permission cache for the user who created the receiver, so that new permissions are fetched for their next call
// Required for cases when caller wants to immediately interact with the newly created object
r.ac.ClearUserPermissionCache(user)
}
}
// copyPermissionUser returns a user with permissions to copy permissions from one receiver to another. This must include
// permissions to read and write permissions for the receiver, as well as read permissions for users, service accounts, and teams.
func copyPermissionUser(orgID int64) identity.Requester {
return accesscontrol.BackgroundUser("receiver_access_service", orgID, org.RoleAdmin, accesscontrol.ConcatPermissions(
accesscontrol.PermissionsForActions(ReceiversAdminActions, alertingac.ScopeReceiversAll),
[]accesscontrol.Permission{ // Permissions needed for GetPermissions to return user, service account, and team permissions.
{Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll},
{Action: serviceaccounts.ActionRead, Scope: serviceaccounts.ScopeAll},
{Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll},
},
),
)
}
// CopyPermissions copies the resource permissions from one receiver uid to a new uid. This is a temporary
// method to be used during receiver renaming that is necessitated by receiver uids being generated from the receiver
// name.
func (r ReceiverPermissionsService) CopyPermissions(ctx context.Context, orgID int64, user identity.Requester, oldUID, newUID string) (int, error) {
currentPermissions, err := r.GetPermissions(ctx, copyPermissionUser(orgID), oldUID)
if err != nil {
return 0, err
}
setPermissionCommands := r.toSetResourcePermissionCommands(currentPermissions)
if _, err := r.SetPermissions(ctx, orgID, newUID, setPermissionCommands...); err != nil {
return 0, err
}
// Clear permission cache for the user who updated the receiver, so that new permissions are fetched for their next call
// Required for cases when caller wants to immediately interact with the newly updated object
if user != nil && user.IsIdentityType(claims.TypeUser) {
r.ac.ClearUserPermissionCache(user)
}
return countCustomPermissions(setPermissionCommands), nil
}
// toSetResourcePermissionCommands converts a list of resource permissions to a list of set resource permission commands.
// Only includes managed permissions.
func (r ReceiverPermissionsService) toSetResourcePermissionCommands(permissions []accesscontrol.ResourcePermission) []accesscontrol.SetResourcePermissionCommand {
cmds := make([]accesscontrol.SetResourcePermissionCommand, 0, len(permissions))
for _, p := range permissions {
if !p.IsManaged {
continue
}
permission := r.MapActions(p)
if permission == "" {
continue
}
cmds = append(cmds, accesscontrol.SetResourcePermissionCommand{
Permission: permission,
BuiltinRole: p.BuiltInRole,
TeamID: p.TeamId,
UserID: p.UserId,
})
}
return cmds
}
// countCustomPermissions counts the number of custom permissions in a list of set resource permission commands. A
// custom permission is a permission that is not a default permission for a receiver.
func countCustomPermissions(permissions []accesscontrol.SetResourcePermissionCommand) int {
cacheKey := func(p accesscontrol.SetResourcePermissionCommand) accesscontrol.SetResourcePermissionCommand {
return accesscontrol.SetResourcePermissionCommand{
Permission: "",
BuiltinRole: p.BuiltinRole,
TeamID: p.TeamID,
UserID: p.UserID,
}
}
missingDefaults := make(map[accesscontrol.SetResourcePermissionCommand]string, 2)
for _, p := range defaultPermissions() {
missingDefaults[cacheKey(p)] = p.Permission
}
diff := 0
for _, p := range permissions {
key := cacheKey(p)
perm, ok := missingDefaults[key]
if perm != p.Permission {
diff++
}
if ok {
delete(missingDefaults, key)
}
}
// missing + new
return len(missingDefaults) + diff
}
@@ -374,6 +374,7 @@ func createNotificationSrvSutFromEnv(t *testing.T, env *testEnvironment) Notific
env.secrets,
env.xact,
env.log,
fakes.NewFakeReceiverPermissionsService(),
)
return NotificationSrv{
logger: env.log,
@@ -43,6 +43,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store"
ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/secrets"
secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
@@ -1901,6 +1902,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi
env.secrets,
env.xact,
env.log,
ngalertfakes.NewFakeReceiverPermissionsService(),
)
return ProvisioningSrv{
log: env.log,
+8 -3
View File
@@ -77,6 +77,7 @@ func ProvideService(
tracer tracing.Tracer,
ruleStore *store.DBstore,
httpClientProvider httpclient.Provider,
resourcePermissions accesscontrol.ReceiverPermissionsService,
) (*AlertNG, error) {
ng := &AlertNG{
Cfg: cfg,
@@ -98,12 +99,13 @@ func ProvideService(
dashboardService: dashboardService,
renderService: renderService,
bus: bus,
accesscontrolService: accesscontrolService,
AccesscontrolService: accesscontrolService,
annotationsRepo: annotationsRepo,
pluginsStore: pluginsStore,
tracer: tracer,
store: ruleStore,
httpClientProvider: httpClientProvider,
ResourcePermissions: resourcePermissions,
}
if ng.IsDisabled() {
@@ -147,7 +149,8 @@ type AlertNG struct {
MultiOrgAlertmanager *notifier.MultiOrgAlertmanager
AlertsRouter *sender.AlertsRouter
accesscontrol accesscontrol.AccessControl
accesscontrolService accesscontrol.Service
AccesscontrolService accesscontrol.Service
ResourcePermissions accesscontrol.ReceiverPermissionsService
annotationsRepo annotations.Repository
store *store.DBstore
@@ -423,6 +426,7 @@ func (ng *AlertNG) init() error {
ng.SecretsService,
ng.store,
ng.Log,
ng.ResourcePermissions,
)
provisioningReceiverService := notifier.NewReceiverService(
ac.NewReceiverAccess[*models.Receiver](ng.accesscontrol, true),
@@ -432,6 +436,7 @@ func (ng *AlertNG) init() error {
ng.SecretsService,
ng.store,
ng.Log,
ng.ResourcePermissions,
)
// Provisioning
@@ -489,7 +494,7 @@ func (ng *AlertNG) init() error {
return key.LogContext(), true
})
return DeclareFixedRoles(ng.accesscontrolService, ng.FeatureToggles)
return DeclareFixedRoles(ng.AccesscontrolService, ng.FeatureToggles)
}
func subscribeToFolderChanges(logger log.Logger, bus bus.Bus, dbStore api.RuleStore) {
@@ -19,7 +19,7 @@ func (rev *ConfigRevision) DeleteReceiver(uid string) {
func (rev *ConfigRevision) CreateReceiver(receiver *models.Receiver) (*definitions.PostableApiReceiver, error) {
// Check if the receiver already exists.
_, err := rev.GetReceiver(NameToUid(receiver.Name)) // get UID from name because the new receiver does not have UID yet.
_, err := rev.GetReceiver(receiver.GetUID())
if err == nil {
return nil, ErrReceiverExists.Errorf("")
}
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
@@ -45,6 +46,7 @@ type ReceiverService struct {
xact transactionManager
log log.Logger
provenanceValidator validation.ProvenanceStatusTransitionValidator
resourcePermissions ac.ReceiverPermissionsService
}
type alertRuleNotificationSettingsStore interface {
@@ -96,6 +98,7 @@ func NewReceiverService(
encryptionService secretService,
xact transactionManager,
log log.Logger,
resourcePermissions ac.ReceiverPermissionsService,
) *ReceiverService {
return &ReceiverService{
authz: authz,
@@ -106,6 +109,7 @@ func NewReceiverService(
xact: xact,
log: log,
provenanceValidator: validation.ValidateProvenanceRelaxed,
resourcePermissions: resourcePermissions,
}
}
@@ -312,6 +316,12 @@ func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, calle
if err != nil {
return err
}
if rs.resourcePermissions != nil {
err = rs.resourcePermissions.DeleteResourcePermissions(ctx, orgID, uid)
if err != nil {
rs.log.Error("Could not delete receiver permissions", "receiver", existing.Name, "error", err)
}
}
return rs.deleteProvenances(ctx, orgID, existing.Integrations)
})
}
@@ -336,6 +346,9 @@ func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receive
return nil, legacy_storage.MakeErrReceiverInvalid(err)
}
// Generate UID from name.
createdReceiver.UID = legacy_storage.NameToUid(createdReceiver.Name)
created, err := revision.CreateReceiver(&createdReceiver)
if err != nil {
return nil, err
@@ -346,6 +359,9 @@ func (rs *ReceiverService) CreateReceiver(ctx context.Context, r *models.Receive
if err != nil {
return err
}
if rs.resourcePermissions != nil {
rs.resourcePermissions.SetDefaultPermissions(ctx, orgID, user, createdReceiver.GetUID())
}
return rs.setReceiverProvenance(ctx, orgID, &createdReceiver)
})
if err != nil {
@@ -422,6 +438,19 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r *models.Receive
if err != nil {
return err
}
// Update receiver permissions
if rs.resourcePermissions != nil {
permissionsUpdated, err := rs.resourcePermissions.CopyPermissions(ctx, orgID, user, legacy_storage.NameToUid(existing.Name), legacy_storage.NameToUid(r.Name))
if err != nil {
return err
}
if permissionsUpdated > 0 {
rs.log.FromContext(ctx).Debug("Moved custom receiver permissions", "oldName", existing.Name, "newName", r.Name, "count", permissionsUpdated)
}
if err := rs.resourcePermissions.DeleteResourcePermissions(ctx, orgID, legacy_storage.NameToUid(existing.Name)); err != nil {
return err
}
}
}
err = rs.cfgStore.Save(ctx, revision, orgID)
if err != nil {
@@ -1483,6 +1483,7 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secretService) *ReceiverS
encryptSvc,
xact,
log.NewNopLogger(),
fakes.NewFakeReceiverPermissionsService(),
)
}
@@ -493,6 +493,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec
secretService,
xact,
log.NewNopLogger(),
fakes.NewFakeReceiverPermissionsService(),
)
return &ContactPointService{
@@ -0,0 +1,28 @@
package fakes
import (
"context"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
)
type FakeReceiverPermissionsService struct {
*actest.FakePermissionsService
}
func NewFakeReceiverPermissionsService() *FakeReceiverPermissionsService {
return &FakeReceiverPermissionsService{
FakePermissionsService: &actest.FakePermissionsService{},
}
}
func (f FakeReceiverPermissionsService) SetDefaultPermissions(ctx context.Context, orgID int64, user identity.Requester, uid string) {
}
func (f FakeReceiverPermissionsService) CopyPermissions(ctx context.Context, orgID int64, user identity.Requester, oldUID, newUID string) (int, error) {
return 0, nil
}
var _ accesscontrol.ReceiverPermissionsService = new(FakeReceiverPermissionsService)
+2 -1
View File
@@ -29,6 +29,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/ngalert/testutil"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
@@ -70,7 +71,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG,
ng, err := ngalert.ProvideService(
cfg, features, nil, nil, routing.NewRouteRegister(), sqlStore, kvstore.NewFakeKVStore(), nil, nil, quotatest.New(false, nil),
secretsService, nil, m, folderService, ac, &dashboards.FakeDashboardService{}, nil, bus, ac,
annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(),
annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(),
)
require.NoError(tb, err)
return ng, &store.DBstore{
@@ -53,6 +53,7 @@ func ProvideService(
quotaService quota.Service,
secrectService secrets.Service,
orgService org.Service,
resourcePermissions accesscontrol.ReceiverPermissionsService,
) (*ProvisioningServiceImpl, error) {
s := &ProvisioningServiceImpl{
Cfg: cfg,
@@ -76,6 +77,7 @@ func ProvideService(
log: log.New("provisioning"),
orgService: orgService,
folderService: folderService,
resourcePermissions: resourcePermissions,
}
if err := s.setDashboardProvisioner(); err != nil {
@@ -154,6 +156,7 @@ type ProvisioningServiceImpl struct {
quotaService quota.Service
secretService secrets.Service
folderService folder.Service
resourcePermissions accesscontrol.ReceiverPermissionsService
}
func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error {
@@ -287,6 +290,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
ps.secretService,
ps.SQLStore,
ps.log,
ps.resourcePermissions,
)
contactPointService := provisioning.NewContactPointService(configStore, ps.secretService,
st, ps.SQLStore, receiverSvc, ps.log, &st)
+1 -1
View File
@@ -502,7 +502,7 @@ func setupEnv(t *testing.T, replStore db.ReplDB, cfg *setting.Cfg, b bus.Bus, qu
_, err = ngalert.ProvideService(
cfg, featuremgmt.WithFeatures(), nil, nil, routing.NewRouteRegister(), sqlStore, ngalertfakes.NewFakeKVStore(t), nil, nil, quotaService,
secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{},
annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(),
annotationstest.NewFakeAnnotationsRepo(), &pluginstore.FakePluginStore{}, tracer, ruleStore, httpclient.NewProvider(), ngalertfakes.NewFakeReceiverPermissionsService(),
)
require.NoError(t, err)
_, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), cfg, quotaService, storesrv.ProvideSystemUsersService())