From 1ede1e32b8d4c3bb5adf028bc79ea1ae5cdbda8c Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Fri, 20 Sep 2024 18:31:42 -0400 Subject: [PATCH] Alerting: Receiver resource permissions service (#93552) --- .../notifications/receiver/conversions.go | 6 +- pkg/server/wire.go | 2 + pkg/services/accesscontrol/accesscontrol.go | 5 + pkg/services/accesscontrol/models.go | 14 +-- .../ossaccesscontrol/receivers.go | 60 ++++++++++++ pkg/services/ngalert/accesscontrol.go | 95 ++++++++++++++----- .../ngalert/accesscontrol/receivers.go | 67 ++++++++++--- .../ngalert/accesscontrol/receivers_test.go | 59 ++++++++++++ pkg/services/ngalert/models/permissions.go | 8 +- pkg/services/ngalert/ngalert.go | 2 +- .../notifications/receivers/receiver_test.go | 34 ++++++- 11 files changed, 295 insertions(+), 57 deletions(-) create mode 100644 pkg/services/accesscontrol/ossaccesscontrol/receivers.go diff --git a/pkg/registry/apis/alerting/notifications/receiver/conversions.go b/pkg/registry/apis/alerting/notifications/receiver/conversions.go index d6c564a0f03..44ce1bb5d40 100644 --- a/pkg/registry/apis/alerting/notifications/receiver/conversions.go +++ b/pkg/registry/apis/alerting/notifications/receiver/conversions.go @@ -107,9 +107,9 @@ func convertToK8sResource( var permissionMapper = map[ngmodels.ReceiverPermission]string{ ngmodels.ReceiverPermissionReadSecret: "canReadSecrets", - //ngmodels.ReceiverPermissionAdmin: "canAdmin", // TODO: Add when resource permissions are implemented. - ngmodels.ReceiverPermissionWrite: "canWrite", - ngmodels.ReceiverPermissionDelete: "canDelete", + ngmodels.ReceiverPermissionAdmin: "canAdmin", + ngmodels.ReceiverPermissionWrite: "canWrite", + ngmodels.ReceiverPermissionDelete: "canDelete", } func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[string][]string, error) { diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 1c28257cace..98edf7282b9 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -315,6 +315,8 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), ossaccesscontrol.ProvideDashboardPermissions, wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), + ossaccesscontrol.ProvideReceiverPermissionsService, + wire.Bind(new(accesscontrol.ReceiverPermissionsService), new(*ossaccesscontrol.ReceiverPermissionsService)), starimpl.ProvideService, playlistimpl.ProvideService, apikeyimpl.ProvideService, diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index a3fea676c1c..d833654d46e 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -11,6 +11,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/authn" @@ -148,6 +149,10 @@ type ServiceAccountPermissionsService interface { PermissionsService } +type ReceiverPermissionsService interface { + PermissionsService +} + type PermissionsService interface { // GetPermissions returns all permissions for given resourceID GetPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]ResourcePermission, error) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 7bc0ae81aa6..4ab28eba0b3 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -444,12 +444,14 @@ const ( ActionAlertingNotificationsTimeIntervalsDelete = "alert.notifications.time-intervals:delete" // Alerting receiver actions - ActionAlertingReceiversList = "alert.notifications.receivers:list" - ActionAlertingReceiversRead = "alert.notifications.receivers:read" - ActionAlertingReceiversReadSecrets = "alert.notifications.receivers.secrets:read" - ActionAlertingReceiversCreate = "alert.notifications.receivers:create" - ActionAlertingReceiversUpdate = "alert.notifications.receivers:write" - ActionAlertingReceiversDelete = "alert.notifications.receivers:delete" + ActionAlertingReceiversList = "alert.notifications.receivers:list" + ActionAlertingReceiversRead = "alert.notifications.receivers:read" + ActionAlertingReceiversReadSecrets = "alert.notifications.receivers.secrets:read" + ActionAlertingReceiversCreate = "alert.notifications.receivers:create" + ActionAlertingReceiversUpdate = "alert.notifications.receivers:write" + ActionAlertingReceiversDelete = "alert.notifications.receivers:delete" + ActionAlertingReceiversPermissionsRead = "receivers.permissions:read" + ActionAlertingReceiversPermissionsWrite = "receivers.permissions:write" // External alerting rule actions. We can only narrow it down to writes or reads, as we don't control the atomicity in the external system. ActionAlertingRuleExternalWrite = "alert.rules.external:write" diff --git a/pkg/services/accesscontrol/ossaccesscontrol/receivers.go b/pkg/services/accesscontrol/ossaccesscontrol/receivers.go new file mode 100644 index 00000000000..8ef3bb837b1 --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/receivers.go @@ -0,0 +1,60 @@ +package ossaccesscontrol + +import ( + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/db" + "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/team" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" +) + +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}...) + +func ProvideReceiverPermissionsService( + cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl, + license licensing.Licensing, service accesscontrol.Service, + teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService, +) (*ReceiverPermissionsService, error) { + if !features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) { + return nil, nil + } + + options := resourcepermissions.Options{ + Resource: "receivers", + ResourceAttribute: "uid", + Assignments: resourcepermissions.Assignments{ + Users: true, + Teams: true, + BuiltInRoles: true, + ServiceAccounts: true, + }, + PermissionsToActions: map[string][]string{ + string(alertingac.ReceiverPermissionView): append([]string{}, ReceiversViewActions...), + string(alertingac.ReceiverPermissionEdit): append([]string{}, ReceiversEditActions...), + string(alertingac.ReceiverPermissionAdmin): append([]string{}, ReceiversAdminActions...), + }, + ReaderRoleName: "Alerting receiver permission reader", + WriterRoleName: "Alerting receiver permission writer", + RoleGroup: ngalert.AlertRolesGroup, + } + + srv, err := resourcepermissions.New(cfg, options, features, router, license, ac, service, sql, teamService, userService, actionSetService) + if err != nil { + return nil, err + } + return &ReceiverPermissionsService{Service: srv}, nil +} + +var _ accesscontrol.ReceiverPermissionsService = new(ReceiverPermissionsService) + +type ReceiverPermissionsService struct { + *resourcepermissions.Service +} diff --git a/pkg/services/ngalert/accesscontrol.go b/pkg/services/ngalert/accesscontrol.go index a14f41e564f..53c4ba79c5f 100644 --- a/pkg/services/ngalert/accesscontrol.go +++ b/pkg/services/ngalert/accesscontrol.go @@ -4,6 +4,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/org" ) @@ -115,13 +116,50 @@ var ( }, } + receiversReaderRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting.receivers:reader", + DisplayName: "Contact Point Reader", + Description: "Read all contact points in Grafana", + Group: AlertRolesGroup, + Permissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionAlertingReceiversRead, Scope: ac.ScopeReceiversAll}, + }, + }, + } + + receiversCreatorRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting.receivers:creator", + DisplayName: "Contact Point Creator", + Description: "Create new contact points in Grafana", + Group: AlertRolesGroup, + Permissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionAlertingReceiversCreate}, + }, + }, + } + + receiversWriterRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting.receivers:writer", + DisplayName: "Contact Point Writer", + Description: "Create, update, and delete all contact points in Grafana", + Group: AlertRolesGroup, + Permissions: accesscontrol.ConcatPermissions(receiversReaderRole.Role.Permissions, receiversCreatorRole.Role.Permissions, []accesscontrol.Permission{ + {Action: accesscontrol.ActionAlertingReceiversUpdate, Scope: ac.ScopeReceiversAll}, + {Action: accesscontrol.ActionAlertingReceiversDelete, Scope: ac.ScopeReceiversAll}, + }), + }, + } + notificationsReaderRole = accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ Name: accesscontrol.FixedRolePrefix + "alerting.notifications:reader", DisplayName: "Notifications Reader", Description: "Read notification policies and contact points in Grafana and external providers", Group: AlertRolesGroup, - Permissions: []accesscontrol.Permission{ + Permissions: accesscontrol.ConcatPermissions(receiversReaderRole.Role.Permissions, []accesscontrol.Permission{ { Action: accesscontrol.ActionAlertingNotificationsRead, }, @@ -132,11 +170,7 @@ var ( { Action: accesscontrol.ActionAlertingNotificationsTimeIntervalsRead, }, - { - Action: accesscontrol.ActionAlertingReceiversRead, - Scope: ac.ScopeReceiversAll, - }, - }, + }), }, } @@ -146,7 +180,7 @@ var ( DisplayName: "Notifications Writer", Description: "Add, update, and delete contact points and notification policies in Grafana and external providers", Group: AlertRolesGroup, - Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, []accesscontrol.Permission{ + Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, receiversWriterRole.Role.Permissions, []accesscontrol.Permission{ { Action: accesscontrol.ActionAlertingNotificationsWrite, }, @@ -154,18 +188,6 @@ var ( Action: accesscontrol.ActionAlertingNotificationsExternalWrite, Scope: datasources.ScopeAll, }, - { - Action: accesscontrol.ActionAlertingReceiversCreate, - Scope: ac.ScopeReceiversAll, - }, - { - Action: accesscontrol.ActionAlertingReceiversUpdate, - Scope: ac.ScopeReceiversAll, - }, - { - Action: accesscontrol.ActionAlertingReceiversDelete, - Scope: ac.ScopeReceiversAll, - }, }), }, } @@ -184,12 +206,27 @@ var ( alertingWriterRole = accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ Name: accesscontrol.FixedRolePrefix + "alerting:writer", - DisplayName: "Full access", - Description: "Add,update and delete alert rules, instances, silences, contact points, and notification policies in Grafana and all external providers", + DisplayName: "Full write access", + Description: "Add, update and delete alert rules, instances, silences, contact points, and notification policies in Grafana and all external providers", Group: AlertRolesGroup, Permissions: accesscontrol.ConcatPermissions(rulesWriterRole.Role.Permissions, instancesWriterRole.Role.Permissions, notificationsWriterRole.Role.Permissions), }, - Grants: []string{string(org.RoleEditor), string(org.RoleAdmin)}, + Grants: []string{string(org.RoleEditor)}, + } + + alertingAdminRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting:admin", + DisplayName: "Full admin access", + Description: "Full write access in Grafana and all external providers, including their permissions and secrets", + Group: AlertRolesGroup, + Permissions: accesscontrol.ConcatPermissions(alertingWriterRole.Role.Permissions, []accesscontrol.Permission{ + {Action: accesscontrol.ActionAlertingReceiversPermissionsRead, Scope: ac.ScopeReceiversAll}, + {Action: accesscontrol.ActionAlertingReceiversPermissionsWrite, Scope: ac.ScopeReceiversAll}, + {Action: accesscontrol.ActionAlertingReceiversReadSecrets, Scope: ac.ScopeReceiversAll}, + }), + }, + Grants: []string{string(org.RoleAdmin)}, } alertingProvisionerRole = accesscontrol.RoleRegistration{ @@ -266,11 +303,17 @@ var ( } ) -func DeclareFixedRoles(service accesscontrol.Service) error { - return service.DeclareFixedRoles( +func DeclareFixedRoles(service accesscontrol.Service, features featuremgmt.FeatureToggles) error { + fixedRoles := []accesscontrol.RoleRegistration{ rulesReaderRole, rulesWriterRole, instancesReaderRole, instancesWriterRole, notificationsReaderRole, notificationsWriterRole, - alertingReaderRole, alertingWriterRole, alertingProvisionerRole, alertingProvisioningReaderWithSecretsRole, alertingProvisioningStatus, - ) + alertingReaderRole, alertingWriterRole, alertingAdminRole, alertingProvisionerRole, alertingProvisioningReaderWithSecretsRole, alertingProvisioningStatus, + } + + if features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) { + fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole) + } + + return service.DeclareFixedRoles(fixedRoles...) } diff --git a/pkg/services/ngalert/accesscontrol/receivers.go b/pkg/services/ngalert/accesscontrol/receivers.go index 46d186705ee..47e101e7cd9 100644 --- a/pkg/services/ngalert/accesscontrol/receivers.go +++ b/pkg/services/ngalert/accesscontrol/receivers.go @@ -17,6 +17,15 @@ var ( ScopeReceiversAll = ScopeReceiversProvider.GetResourceAllScope() ) +// ReceiverPermission is a type for representing a receiver permission. +type ReceiverPermission string + +const ( + ReceiverPermissionView ReceiverPermission = "View" + ReceiverPermissionEdit ReceiverPermission = "Edit" + ReceiverPermissionAdmin ReceiverPermission = "Admin" +) + var ( // Asserts pre-conditions for read access to redacted receivers. If this evaluates to false, the user cannot read any redacted receivers. readRedactedReceiversPreConditionsEval = ac.EvalAny( @@ -125,6 +134,28 @@ var ( ac.EvalPermission(ac.ActionAlertingReceiversDelete, ScopeReceiversProvider.GetResourceScopeUID(uid)), ) } + + // Admin + + // Asserts pre-conditions for resource permissions access to receivers. If this evaluates to false, the user cannot modify permissions for any receivers. + permissionsReceiversPreConditionsEval = ac.EvalAll( + ac.EvalPermission(ac.ActionAlertingReceiversPermissionsRead), // Action for receivers. UID scope. + ac.EvalPermission(ac.ActionAlertingReceiversPermissionsWrite), // Action for receivers. UID scope. + ) + + // Asserts resource permissions access to all receivers. + permissionsAllReceiversEval = ac.EvalAll( + ac.EvalPermission(ac.ActionAlertingReceiversPermissionsRead, ScopeReceiversAll), + ac.EvalPermission(ac.ActionAlertingReceiversPermissionsWrite, ScopeReceiversAll), + ) + + // Asserts resource permissions access to a specific receiver. + permissionsReceiverEval = func(uid string) ac.Evaluator { + return ac.EvalAll( + ac.EvalPermission(ac.ActionAlertingReceiversPermissionsRead, ScopeReceiversProvider.GetResourceScopeUID(uid)), + ac.EvalPermission(ac.ActionAlertingReceiversPermissionsWrite, ScopeReceiversProvider.GetResourceScopeUID(uid)), + ) + } ) type ReceiverAccess[T models.Identified] struct { @@ -133,6 +164,7 @@ type ReceiverAccess[T models.Identified] struct { create actionAccess[T] update actionAccess[T] delete actionAccess[T] + permissions actionAccess[T] } // NewReceiverAccess creates a new ReceiverAccess service. If includeProvisioningActions is true, the service will include @@ -199,6 +231,18 @@ func NewReceiverAccess[T models.Identified](a ac.AccessControl, includeProvision }, authorizeAll: deleteAllReceiversEval, }, + permissions: actionAccess[T]{ + genericService: genericService{ + ac: a, + }, + resource: "receiver", + action: "admin", // Essentially read+write receiver resource permissions. + authorizeSome: permissionsReceiversPreConditionsEval, + authorizeOne: func(receiver models.Identified) ac.Evaluator { + return permissionsReceiverEval(receiver.GetUID()) + }, + authorizeAll: permissionsAllReceiversEval, + }, } // If this service is meant for the provisioning API, we include the provisioning actions as possible permissions. @@ -219,9 +263,10 @@ func NewReceiverAccess[T models.Identified](a ac.AccessControl, includeProvision }) } - // Write and delete permissions should require read permissions. + // Write, delete, and permissions management should require read permissions. extendAccessControl(&rcvAccess.update, ac.EvalAll, rcvAccess.read) extendAccessControl(&rcvAccess.delete, ac.EvalAll, rcvAccess.read) + extendAccessControl(&rcvAccess.permissions, ac.EvalAll, rcvAccess.read) return rcvAccess } @@ -335,12 +380,11 @@ func (s ReceiverAccess[T]) Access(ctx context.Context, user identity.Requester, basePerms.Set(models.ReceiverPermissionReadSecret, true) // Has access to all receivers. } - // TODO: Add when resource permissions are implemented. - //if err := s.permissions.AuthorizePreConditions(ctx, user); err != nil { - // basePerms.Set(models.ReceiverPermissionAdmin, false) // Doesn't match the preconditions. - //} else if err := s.permissions.AuthorizeAll(ctx, user); err == nil { - // basePerms.Set(models.ReceiverPermissionAdmin, true) // Has access to all receivers. - //} + if err := s.permissions.AuthorizePreConditions(ctx, user); err != nil { + basePerms.Set(models.ReceiverPermissionAdmin, false) // Doesn't match the preconditions. + } else if err := s.permissions.AuthorizeAll(ctx, user); err == nil { + basePerms.Set(models.ReceiverPermissionAdmin, true) // Has access to all receivers. + } if err := s.update.AuthorizePreConditions(ctx, user); err != nil { basePerms.Set(models.ReceiverPermissionWrite, false) // Doesn't match the preconditions. @@ -371,11 +415,10 @@ func (s ReceiverAccess[T]) Access(ctx context.Context, user identity.Requester, permSet.Set(models.ReceiverPermissionReadSecret, err == nil) } - // TODO: Add when resource permissions are implemented. - //if _, ok := permSet.Has(models.ReceiverPermissionAdmin); !ok { - // err := s.permissions.authorize(ctx, user, rcv) - // permSet.Set(models.ReceiverPermissionAdmin, err == nil) - //} + if _, ok := permSet.Has(models.ReceiverPermissionAdmin); !ok { + err := s.permissions.authorize(ctx, user, rcv) + permSet.Set(models.ReceiverPermissionAdmin, err == nil) + } if _, ok := permSet.Has(models.ReceiverPermissionWrite); !ok { err := s.update.authorize(ctx, user, rcv) diff --git a/pkg/services/ngalert/accesscontrol/receivers_test.go b/pkg/services/ngalert/accesscontrol/receivers_test.go index 23b0c635042..6ad2e1db463 100644 --- a/pkg/services/ngalert/accesscontrol/receivers_test.go +++ b/pkg/services/ngalert/accesscontrol/receivers_test.go @@ -238,6 +238,65 @@ func TestReceiverAccess(t *testing.T) { recv3.UID: permissions(), }, }, + // Receiver admin. + { + name: "receiver read permissions alone can't admin", + user: newViewUser(ac.Permission{Action: ac.ActionAlertingReceiversPermissionsRead, Scope: ScopeReceiversAll}), + expected: map[string]models.ReceiverPermissionSet{ + recv1.UID: permissions(), + recv2.UID: permissions(), + recv3.UID: permissions(), + }, + }, + { + name: "receiver write permissions alone can't admin", + user: newViewUser(ac.Permission{Action: ac.ActionAlertingReceiversPermissionsWrite, Scope: ScopeReceiversAll}), + expected: map[string]models.ReceiverPermissionSet{ + recv1.UID: permissions(), + recv2.UID: permissions(), + recv3.UID: permissions(), + }, + }, + { + name: "global receiver read + write permissions can admin", + user: newViewUser( + ac.Permission{Action: ac.ActionAlertingReceiversPermissionsRead, Scope: ScopeReceiversAll}, + ac.Permission{Action: ac.ActionAlertingReceiversPermissionsWrite, Scope: ScopeReceiversAll}, + ), + expected: map[string]models.ReceiverPermissionSet{ + recv1.UID: permissions(models.ReceiverPermissionAdmin), + recv2.UID: permissions(models.ReceiverPermissionAdmin), + recv3.UID: permissions(models.ReceiverPermissionAdmin), + }, + }, + { + name: "per-receiver read + write permissions should have per-receiver admin", + user: newViewUser( + ac.Permission{Action: ac.ActionAlertingReceiversPermissionsRead, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, + ac.Permission{Action: ac.ActionAlertingReceiversPermissionsWrite, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, + ac.Permission{Action: ac.ActionAlertingReceiversPermissionsRead, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, + ac.Permission{Action: ac.ActionAlertingReceiversPermissionsWrite, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, + ), + expected: map[string]models.ReceiverPermissionSet{ + recv1.UID: permissions(models.ReceiverPermissionAdmin), + recv2.UID: permissions(), + recv3.UID: permissions(models.ReceiverPermissionAdmin), + }, + }, + { + name: "per-receiver admin should require read", + user: newEmptyUser( + ac.Permission{Action: ac.ActionAlertingReceiversPermissionsRead, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, + ac.Permission{Action: ac.ActionAlertingReceiversPermissionsWrite, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, + ac.Permission{Action: ac.ActionAlertingReceiversPermissionsRead, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, + ac.Permission{Action: ac.ActionAlertingReceiversPermissionsWrite, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, + ), + expected: map[string]models.ReceiverPermissionSet{ + recv1.UID: permissions(), + recv2.UID: permissions(), + recv3.UID: permissions(), + }, + }, // Mixed permissions. { name: "legacy provisioning secret read, receiver write", diff --git a/pkg/services/ngalert/models/permissions.go b/pkg/services/ngalert/models/permissions.go index 81e07c5a3d5..a34e184fc99 100644 --- a/pkg/services/ngalert/models/permissions.go +++ b/pkg/services/ngalert/models/permissions.go @@ -10,16 +10,16 @@ type ReceiverPermission string const ( ReceiverPermissionReadSecret ReceiverPermission = "secrets" - //ReceiverPermissionAdmin ReceiverPermission = "admin" // TODO: Add when resource permissions are implemented. - ReceiverPermissionWrite ReceiverPermission = "write" - ReceiverPermissionDelete ReceiverPermission = "delete" + ReceiverPermissionAdmin ReceiverPermission = "admin" + ReceiverPermissionWrite ReceiverPermission = "write" + ReceiverPermissionDelete ReceiverPermission = "delete" ) // ReceiverPermissions returns all possible silence permissions. func ReceiverPermissions() []ReceiverPermission { return []ReceiverPermission{ ReceiverPermissionReadSecret, - //ReceiverPermissionAdmin, // TODO: Add when resource permissions are implemented. + ReceiverPermissionAdmin, ReceiverPermissionWrite, ReceiverPermissionDelete, } diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 6bf152a96a0..ed4ad7df2a2 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -489,7 +489,7 @@ func (ng *AlertNG) init() error { return key.LogContext(), true }) - return DeclareFixedRoles(ng.accesscontrolService) + return DeclareFixedRoles(ng.accesscontrolService, ng.FeatureToggles) } func subscribeToFolderChanges(logger log.Logger, bus bus.Bus, dbStore api.RuleStore) { diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index 175e83c5d6f..4017519158f 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -27,6 +27,7 @@ import ( notificationsv0alpha1 "github.com/grafana/grafana/pkg/generated/clientset/versioned/typed/alerting_notifications/v0alpha1" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards" @@ -142,6 +143,7 @@ func TestIntegrationAccessControl(t *testing.T) { canCreate bool canDelete bool canReadSecrets bool + canAdmin bool } // region users unauthorized := helper.CreateUser("unauthorized", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{}) @@ -185,6 +187,12 @@ func TestIntegrationAccessControl(t *testing.T) { }, }, }) + adminLikeUser := helper.CreateUser("adminLikeUser", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{ + createWildcardPermission(append( + []string{accesscontrol.ActionAlertingReceiversCreate}, + ossaccesscontrol.ReceiversAdminActions..., + )...), + }) // endregion @@ -197,11 +205,13 @@ func TestIntegrationAccessControl(t *testing.T) { canDelete: false, }, { - user: org1.Admin, - canRead: true, - canUpdate: true, - canCreate: true, - canDelete: true, + user: org1.Admin, + canRead: true, + canCreate: true, + canUpdate: true, + canDelete: true, + canAdmin: true, + canReadSecrets: true, }, { user: org1.Editor, @@ -249,6 +259,15 @@ func TestIntegrationAccessControl(t *testing.T) { canUpdate: true, canDelete: true, }, + { + user: adminLikeUser, + canRead: true, + canCreate: true, + canUpdate: true, + canDelete: true, + canAdmin: true, + canReadSecrets: true, + }, } admin := org1.Admin @@ -315,6 +334,9 @@ func TestIntegrationAccessControl(t *testing.T) { if tc.canReadSecrets { expectedWithMetadata.SetAccessControl("canReadSecrets") } + if tc.canAdmin { + expectedWithMetadata.SetAccessControl("canAdmin") + } t.Run("should be able to list receivers", func(t *testing.T) { list, err := client.List(ctx, v1.ListOptions{}) require.NoError(t, err) @@ -1027,6 +1049,8 @@ func TestIntegrationCRUD(t *testing.T) { // Set expected metadata receiver.SetAccessControl("canWrite") receiver.SetAccessControl("canDelete") + receiver.SetAccessControl("canReadSecrets") + receiver.SetAccessControl("canAdmin") receiver.SetInUse(0, nil) // Use export endpoint because it's the only way to get decrypted secrets fast.