Alerting: Fix per-receiver RBAC for receivers with long names (#95084)

* Implement uidToResourceID

* add middleware

* Move uidToResourceID to alerting package

* Only hash uid if it's too long

* Use hashed uid in access control

* Move ReceiverUidToResourceId to ScopeProvider

* resolve uid in middleware only if param exists

* Tests

* Linting

---------

Co-authored-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
This commit is contained in:
Matthew Jacobson 2024-10-22 10:04:13 -04:00 committed by GitHub
parent 25e85f8947
commit 4aad44e848
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 74 additions and 11 deletions

View File

@ -76,12 +76,13 @@ type ReceiverPermissionsService struct {
// SetDefaultPermissions sets the default permissions for a newly created receiver.
func (r ReceiverPermissionsService) SetDefaultPermissions(ctx context.Context, orgID int64, user identity.Requester, uid string) {
r.log.Debug("Setting default permissions for receiver", "receiver_uid", uid)
resourceId := alertingac.ScopeReceiversProvider.GetResourceIDFromUID(uid)
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)
r.log.Error("Could not make user admin", "receiver_uid", uid, "resource_id", resourceId, "id", user.GetID(), "error", err)
} else {
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
UserID: userID, Permission: string(alertingac.ReceiverPermissionAdmin),
@ -90,8 +91,8 @@ func (r ReceiverPermissionsService) SetDefaultPermissions(ctx context.Context, o
}
}
if _, err := r.SetPermissions(ctx, orgID, uid, permissions...); err != nil {
r.log.Error("Could not set default permissions", "receiver_uid", uid, "error", err)
if _, err := r.SetPermissions(ctx, orgID, resourceId, permissions...); err != nil {
r.log.Error("Could not set default permissions", "receiver_uid", uid, "resource_id", resourceId, "id", "error", err)
}
if clearCache {
@ -120,13 +121,15 @@ func copyPermissionUser(orgID int64) identity.Requester {
// name.
func (r ReceiverPermissionsService) CopyPermissions(ctx context.Context, orgID int64, user identity.Requester, oldUID, newUID string) (int, error) {
r.log.Debug("Copying permissions from receiver", "old_uid", oldUID, "new_uid", newUID)
currentPermissions, err := r.GetPermissions(ctx, copyPermissionUser(orgID), oldUID)
oldResourceId := alertingac.ScopeReceiversProvider.GetResourceIDFromUID(oldUID)
newResourceId := alertingac.ScopeReceiversProvider.GetResourceIDFromUID(newUID)
currentPermissions, err := r.GetPermissions(ctx, copyPermissionUser(orgID), oldResourceId)
if err != nil {
return 0, err
}
setPermissionCommands := r.toSetResourcePermissionCommands(currentPermissions)
if _, err := r.SetPermissions(ctx, orgID, newUID, setPermissionCommands...); err != nil {
if _, err := r.SetPermissions(ctx, orgID, newResourceId, setPermissionCommands...); err != nil {
return 0, err
}
@ -146,6 +149,10 @@ func (r ReceiverPermissionsService) CopyPermissions(ctx context.Context, orgID i
return countCustomPermissions(setPermissionCommands), nil
}
func (r ReceiverPermissionsService) DeleteResourcePermissions(ctx context.Context, orgID int64, uid string) error {
return r.Service.DeleteResourcePermissions(ctx, orgID, alertingac.ScopeReceiversProvider.GetResourceIDFromUID(uid))
}
// 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 {

View File

@ -5,16 +5,18 @@ import (
"net/http"
"strconv"
"go.opentelemetry.io/otel"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
alertingac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"go.opentelemetry.io/otel"
)
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/accesscontrol/resourcepermissions")
@ -48,6 +50,9 @@ func (a *api) registerEndpoints() {
if a.service.options.Resource == "teams" {
teamUIDResolverResource = team.MiddlewareTeamUIDResolver(a.service.teamService, ":resourceID")
}
if a.service.options.Resource == "receivers" {
teamUIDResolverResource = MiddlewareReceiverUIDResolver(":resourceID")
}
a.router.Group(fmt.Sprintf("/api/access-control/%s", a.service.options.Resource), func(r routing.RouteRegister) {
actionRead := fmt.Sprintf("%s.permissions:read", a.service.options.Resource)
@ -430,3 +435,13 @@ func permissionSetResponse(cmd setPermissionCommand) response.Response {
}
return response.Success(message)
}
func MiddlewareReceiverUIDResolver(paramName string) web.Handler {
return func(c *contextmodel.ReqContext) {
gotParams := web.Params(c.Req)
if uid, ok := gotParams[paramName]; ok {
gotParams[paramName] = alertingac.ScopeReceiversProvider.GetResourceIDFromUID(uid)
web.SetURLParams(c.Req, gotParams)
}
}
}

View File

@ -2,10 +2,14 @@ package accesscontrol
import (
"context"
// #nosec G505 Used only for shortening the uid, not for security purposes.
"crypto/sha1"
"encoding/hex"
"github.com/grafana/grafana/pkg/apimachinery/identity"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
)
const (
@ -13,10 +17,30 @@ const (
)
var (
ScopeReceiversProvider = ac.NewScopeProvider(ScopeReceiversRoot)
ScopeReceiversProvider = ReceiverScopeProvider{ac.NewScopeProvider(ScopeReceiversRoot)}
ScopeReceiversAll = ScopeReceiversProvider.GetResourceAllScope()
)
type ReceiverScopeProvider struct {
ac.ScopeProvider
}
func (p ReceiverScopeProvider) GetResourceScopeUID(uid string) string {
return ScopeReceiversProvider.ScopeProvider.GetResourceScopeUID(p.GetResourceIDFromUID(uid))
}
// GetResourceIDFromUID converts a receiver uid to a resource id. This is necessary as resource ids are limited to 40 characters.
// If the uid is already less than or equal to 40 characters, it is returned as is.
func (p ReceiverScopeProvider) GetResourceIDFromUID(uid string) string {
if len(uid) <= util.MaxUIDLength {
return uid
}
// #nosec G505 Used only for shortening the uid, not for security purposes.
h := sha1.New()
h.Write([]byte(uid))
return hex.EncodeToString(h.Sum(nil))
}
// ReceiverPermission is a type for representing a receiver permission.
type ReceiverPermission string

View File

@ -851,7 +851,7 @@ func TestReceiverServiceAC_Read(t *testing.T) {
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))
recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver3"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver with a really long name that surpasses 40 characters"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
allReceivers := func() []models.Receiver {
return []models.Receiver{recv1, recv2, recv3}
}
@ -990,7 +990,7 @@ func TestReceiverServiceAC_Create(t *testing.T) {
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))
recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver3"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver with a really long name that surpasses 40 characters"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
allReceivers := func() []models.Receiver {
return []models.Receiver{recv1, recv2, recv3}
}
@ -1078,7 +1078,7 @@ func TestReceiverServiceAC_Update(t *testing.T) {
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))
recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver3"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver with a really long name that surpasses 40 characters"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
allReceivers := func() []models.Receiver {
return []models.Receiver{recv1, recv2, recv3}
}
@ -1230,7 +1230,7 @@ func TestReceiverServiceAC_Delete(t *testing.T) {
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))
recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver1"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver2"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver3"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("receiver with a really long name that surpasses 40 characters"), models.ReceiverMuts.WithIntegrations(slackIntegration(), emailIntegration()))()
allReceivers := func() []models.Receiver {
return []models.Receiver{recv1, recv2, recv3}
}

View File

@ -481,6 +481,14 @@ func TestIntegrationAccessControl(t *testing.T) {
)...),
})
// Test receivers with uids longer than 40 characters. User name is used in receiver name.
adminLikeUserLongName := helper.CreateUser("adminLikeUserCreatingAReallyLongReceiverName", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(append(
[]string{accesscontrol.ActionAlertingReceiversCreate},
ossaccesscontrol.ReceiversAdminActions...,
)...),
})
// endregion
testCases := []testCase{
@ -555,6 +563,15 @@ func TestIntegrationAccessControl(t *testing.T) {
canAdmin: true,
canReadSecrets: true,
},
{
user: adminLikeUserLongName,
canRead: true,
canCreate: true,
canUpdate: true,
canDelete: true,
canAdmin: true,
canReadSecrets: true,
},
}
admin := org1.Admin