Alerting: Protected fields for Contact points (#115442)

* Alerting: Protect sensitive fields of contact points from
 unauthorized modification

- Introduce a new permission alert.notifications.receivers.protected:write. The permission is granted to contact point administrators.
- Introduce field Protected to NotifierOption
- Introduce DiffReport for models.Integrations with focus on Settings. The diff report is extended with methods that return all keys that are different between two settings.
- Add new annotation 'grafana.com/access/CanModifyProtected' to Receiver model
- Update receiver service to enforce the permission and return status 403 if unauthorized user modifies protected field
- Update receiver testing API to enforce permission and return status 403 if unauthorized user modifies protected field.
- Update UI to disable protected fields if user cannot modify them
This commit is contained in:
Yuri Tseretyan
2025-12-16 15:56:02 -05:00
committed by GitHub
parent 30fb1c032a
commit f2c30cbbd1
37 changed files with 1482 additions and 114 deletions
@@ -95,6 +95,26 @@ var (
)
}
// Asserts pre-conditions for access to modify protected fields of receivers. If this evaluates to false, the user cannot modify protected fields of any receivers.
updateReceiversProtectedPreConditionsEval = ac.EvalAll(
updateReceiversPreConditionsEval,
ac.EvalPermission(ac.ActionAlertingReceiversUpdateProtected), // Action for receivers. UID scope.
)
// Asserts access to modify protected fields of a specific receiver.
updateReceiverProtectedEval = func(uid string) ac.Evaluator {
return ac.EvalAll(
updateReceiverEval(uid),
ac.EvalPermission(ac.ActionAlertingReceiversUpdateProtected, models.ScopeReceiversProvider.GetResourceScopeUID(uid)),
)
}
// Asserts access to modify protected fields of all receivers.
updateAllReceiverProtectedEval = ac.EvalAll(
updateAllReceiversEval,
ac.EvalPermission(ac.ActionAlertingReceiversUpdateProtected, models.ScopeReceiversAll),
)
// Delete
// Asserts pre-conditions for delete access to receivers. If this evaluates to false, the user cannot delete any receivers.
@@ -141,12 +161,13 @@ var (
)
type ReceiverAccess[T models.Identified] struct {
read actionAccess[T]
readDecrypted actionAccess[T]
create actionAccess[T]
update actionAccess[T]
delete actionAccess[T]
permissions actionAccess[T]
read actionAccess[T]
readDecrypted actionAccess[T]
create actionAccess[T]
update actionAccess[T]
updateProtected actionAccess[T]
delete actionAccess[T]
permissions actionAccess[T]
}
// NewReceiverAccess creates a new ReceiverAccess service. If includeProvisioningActions is true, the service will include
@@ -201,6 +222,18 @@ func NewReceiverAccess[T models.Identified](a ac.AccessControl, includeProvision
},
authorizeAll: updateAllReceiversEval,
},
updateProtected: actionAccess[T]{
genericService: genericService{
ac: a,
},
resource: "receiver",
action: "update protected fields of", // this produces message "user is not authorized to update protected fields of X receiver"
authorizeSome: updateReceiversProtectedPreConditionsEval,
authorizeOne: func(receiver models.Identified) ac.Evaluator {
return updateReceiverProtectedEval(receiver.GetUID())
},
authorizeAll: updateAllReceiverProtectedEval,
},
delete: actionAccess[T]{
genericService: genericService{
ac: a,
@@ -311,6 +344,14 @@ func (s ReceiverAccess[T]) AuthorizeUpdate(ctx context.Context, user identity.Re
return s.update.Authorize(ctx, user, receiver)
}
func (s ReceiverAccess[T]) HasUpdateProtected(ctx context.Context, user identity.Requester, receiver T) (bool, error) {
return s.updateProtected.Has(ctx, user, receiver)
}
func (s ReceiverAccess[T]) AuthorizeUpdateProtected(ctx context.Context, user identity.Requester, receiver T) error {
return s.updateProtected.Authorize(ctx, user, receiver)
}
// Global
// AuthorizeCreate checks if user has access to create receivers. Returns an error if user does not have access.
@@ -380,6 +421,12 @@ func (s ReceiverAccess[T]) Access(ctx context.Context, user identity.Requester,
basePerms.Set(models.ReceiverPermissionDelete, true) // Has access to all receivers.
}
if err := s.updateProtected.AuthorizePreConditions(ctx, user); err != nil {
basePerms.Set(models.ReceiverPermissionModifyProtected, false)
} else if err := s.updateProtected.AuthorizeAll(ctx, user); err == nil {
basePerms.Set(models.ReceiverPermissionModifyProtected, true)
}
if basePerms.AllSet() {
// Shortcut for the case when all permissions are known based on preconditions.
result := make(map[string]models.ReceiverPermissionSet, len(receivers))
@@ -412,6 +459,11 @@ func (s ReceiverAccess[T]) Access(ctx context.Context, user identity.Requester,
permSet.Set(models.ReceiverPermissionDelete, err == nil)
}
if _, ok := permSet.Has(models.ReceiverPermissionModifyProtected); !ok {
err := s.updateProtected.authorize(ctx, user, rcv)
permSet.Set(models.ReceiverPermissionModifyProtected, err == nil)
}
result[rcv.GetUID()] = permSet
}
return result, nil
@@ -204,6 +204,33 @@ func TestReceiverAccess(t *testing.T) {
recv3.UID: permissions(),
},
},
{
name: "update protected cannot update receivers",
user: newEmptyUser(
ac.Permission{Action: ac.ActionAlertingReceiversRead, Scope: models.ScopeReceiversAll},
ac.Permission{Action: ac.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversAll},
),
expected: map[string]models.ReceiverPermissionSet{
recv1.UID: permissions(),
recv2.UID: permissions(),
recv3.UID: permissions(),
},
},
{
name: "update protected receivers",
user: newEmptyUser(
ac.Permission{Action: ac.ActionAlertingReceiversRead, Scope: models.ScopeReceiversAll},
ac.Permission{Action: ac.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)},
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)},
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv2.UID)},
ac.Permission{Action: ac.ActionAlertingReceiversUpdateProtected, Scope: models.ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)},
),
expected: map[string]models.ReceiverPermissionSet{
recv1.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionModifyProtected),
recv2.UID: permissions(models.ReceiverPermissionWrite),
recv3.UID: permissions(),
},
},
// Receiver delete.
{
name: "global receiver delete should have delete but no write",