forked from grafana/grafana
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathsilences.go
383 lines (332 loc) · 15.2 KB
/
silences.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
package accesscontrol
import (
"context"
"fmt"
"golang.org/x/exp/maps"
"github.com/grafana/grafana/pkg/apimachinery/identity"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
const (
instancesRead = ac.ActionAlertingInstanceRead
instancesCreate = ac.ActionAlertingInstanceCreate
instancesWrite = ac.ActionAlertingInstanceUpdate
silenceRead = ac.ActionAlertingSilencesRead
silenceCreate = ac.ActionAlertingSilencesCreate
silenceWrite = ac.ActionAlertingSilencesWrite
)
var (
// asserts full read-only access to silences
readAllSilencesEvaluator = ac.EvalAny(ac.EvalPermission(instancesRead), ac.EvalPermission(silenceRead, dashboards.ScopeFoldersProvider.GetResourceAllScope()))
// shortcut assertion that to check if user can read silences
readSomeSilenceEvaluator = ac.EvalAny(ac.EvalPermission(instancesRead), ac.EvalPermission(silenceRead))
// asserts whether user has read access to rules in a specific folder
readRuleSilenceEvaluator = func(folderUID string) ac.Evaluator {
return ac.EvalAny(
ac.EvalPermission(instancesRead),
ac.EvalPermission(silenceRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)),
)
}
// shortcut assertion to check if user can create any silence
createAnySilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesCreate), readAllSilencesEvaluator)
// asserts that user has access to create general silences, the ones that can match alerts created by one or many rules
createGeneralSilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesCreate), readSomeSilenceEvaluator)
// shortcut assertion to check if user can create silences at all
createSomeRuleSilenceEvaluator = ac.EvalAll(
readSomeSilenceEvaluator,
ac.EvalAny(
ac.EvalPermission(instancesCreate),
ac.EvalPermission(silenceCreate)),
)
// asserts that user has access to create silences in a specific folder
createRuleSilenceEvaluator = func(uid string) ac.Evaluator {
return ac.EvalAll(
ac.EvalAny(
ac.EvalPermission(instancesCreate),
ac.EvalPermission(silenceCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)),
),
readRuleSilenceEvaluator(uid),
)
}
// shortcut assertion to check if user can update any silence
updateAnySilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesWrite), readAllSilencesEvaluator)
// asserts that user has access to update general silences
updateGeneralSilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesWrite), readSomeSilenceEvaluator)
// asserts that user has access to update silences at all
updateSomeRuleSilenceEvaluator = ac.EvalAll(
readSomeSilenceEvaluator,
ac.EvalAny(
ac.EvalPermission(instancesWrite),
ac.EvalPermission(silenceWrite)),
)
// asserts that user has access to create silences in a specific folder
updateRuleSilenceEvaluator = func(uid string) ac.Evaluator {
return ac.EvalAll(
ac.EvalAny(
ac.EvalPermission(instancesWrite),
ac.EvalPermission(silenceWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)),
),
readRuleSilenceEvaluator(uid),
)
}
)
type RuleUIDToNamespaceStore interface {
GetNamespacesByRuleUID(ctx context.Context, orgID int64, uids ...string) (map[string]string, error)
}
type SilenceService struct {
genericService
store RuleUIDToNamespaceStore
}
func NewSilenceService(ac ac.AccessControl, store RuleUIDToNamespaceStore) *SilenceService {
return &SilenceService{
genericService: genericService{
ac: ac,
},
store: store,
}
}
// silenceWithFolder is a helper struct that holds a silence and its associated rule and folder UIDs.
type silenceWithFolder struct {
*models.Silence
ruleUID *string
folderUID string
}
// FilterByAccess filters the given list of silences based on the access control permissions of the user.
// Global silence (one that is not attached to a particular rule) is considered available to all users.
// For silences that are not attached to a rule, are checked against authorization.
// This method is more preferred when many silences need to be checked.
func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Requester, silences ...*models.Silence) ([]*models.Silence, error) {
canAll, err := s.authorizeReadSilencePreConditions(ctx, user)
if err != nil {
return nil, err
}
if canAll {
return silences, nil
}
silencesWithFolders, err := s.withFolders(ctx, user.GetOrgID(), silences...)
if err != nil {
return nil, err
}
result := make([]*models.Silence, 0, len(silences))
accessCacheByFolder := make(map[string]bool)
for _, silWithFolder := range silencesWithFolders {
hasAccess, ok := accessCacheByFolder[silWithFolder.folderUID]
if !ok {
hasAccess = s.authorizeReadSilence(ctx, user, silWithFolder) == nil
// Cache non-empty namespaces to avoid repeated checks for the same folder.
if silWithFolder.folderUID != "" {
accessCacheByFolder[silWithFolder.folderUID] = hasAccess
}
}
if hasAccess {
result = append(result, silWithFolder.Silence)
}
}
return result, nil
}
// AuthorizeReadSilence checks if user has access to read a silence.
func (s SilenceService) AuthorizeReadSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error {
canAll, err := s.authorizeReadSilencePreConditions(ctx, user)
if canAll || err != nil { // return early if user can either read all silences or there is error
return err
}
silWithFolder, err := s.withFolders(ctx, user.GetOrgID(), silence)
if err != nil || len(silWithFolder) != 1 {
return fmt.Errorf("resolve rule UID to folder UID: %w", err)
}
return s.authorizeReadSilence(ctx, user, silWithFolder[0])
}
// authorizeReadSilencePreConditions checks necessary preconditions for reading silences. Returns true if user can
// read all silences. Returns error if user does not have access to read any silences.
func (s SilenceService) authorizeReadSilencePreConditions(ctx context.Context, user identity.Requester) (bool, error) {
canAll, err := s.HasAccess(ctx, user, readAllSilencesEvaluator)
if canAll || err != nil { // return early if user can either read all silences or there is error
return canAll, err
}
can, err := s.HasAccess(ctx, user, readSomeSilenceEvaluator)
if err != nil {
return false, err
}
if !can { // User does not have silence permissions at all.
return false, NewAuthorizationErrorWithPermissions("read any silences", readSomeSilenceEvaluator)
}
return false, nil
}
// authorizeReadSilence checks if user has access to read a silence given precondition checks have passed.
func (s SilenceService) authorizeReadSilence(ctx context.Context, user identity.Requester, silence *silenceWithFolder) error {
if silence.ruleUID == nil {
return nil // No rule metadata means that this is a general silence and at this point the user can read them
}
if silence.folderUID == "" { // if we did not find folder by rule UID then it does not exist.
return NewAuthorizationErrorGeneric(fmt.Sprintf("read silence for rule %s", *silence.ruleUID))
}
return s.HasAccessOrError(ctx, user, readRuleSilenceEvaluator(silence.folderUID), func() string {
return "read silence"
})
}
// AuthorizeCreateSilence checks if user has access to create a silence. Returns ErrAuthorizationBase if user is not authorized
func (s SilenceService) AuthorizeCreateSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error {
canAny, err := s.authorizeCreateSilencePreConditions(ctx, user)
if canAny || err != nil { // return early if user can either create any silence or there is an error
return err
}
silWithFolder, err := s.withFolders(ctx, user.GetOrgID(), silence)
if err != nil || len(silWithFolder) != 1 {
return fmt.Errorf("resolve rule UID to folder UID: %w", err)
}
return s.authorizeCreateSilence(ctx, user, silWithFolder[0])
}
// authorizeCreateSilencePreConditions checks necessary preconditions for creating silences. Returns true if user can
// create any silence. Returns error if user does not have access to create any silences at all.
func (s SilenceService) authorizeCreateSilencePreConditions(ctx context.Context, user identity.Requester) (bool, error) {
canAny, err := s.HasAccess(ctx, user, createAnySilenceEvaluator)
if err != nil || canAny {
// return early if user can either create any silence or there is an error
return canAny, err
}
// pre-check whether a user has at least some basic permissions before hit the store
if err := s.HasAccessOrError(ctx, user, createSomeRuleSilenceEvaluator, func() string { return "create any silences" }); err != nil {
return false, err
}
return false, nil
}
// authorizeCreateSilence checks if user has access to create a silence given precondition checks have passed.
func (s SilenceService) authorizeCreateSilence(ctx context.Context, user identity.Requester, silence *silenceWithFolder) error {
if silence.ruleUID == nil {
return s.HasAccessOrError(ctx, user, createGeneralSilenceEvaluator, func() string {
return "create a general silence"
})
}
if silence.folderUID == "" { // if we did not find folder by rule UID then it does not exist.
return NewAuthorizationErrorGeneric(fmt.Sprintf("create silence for rule %s", *silence.ruleUID))
}
return s.HasAccessOrError(ctx, user, createRuleSilenceEvaluator(silence.folderUID), func() string {
return fmt.Sprintf("create silence for rule %s", *silence.ruleUID)
})
}
// AuthorizeUpdateSilence checks if user has access to update\expire a silence. Returns ErrAuthorizationBase if user is not authorized
func (s SilenceService) AuthorizeUpdateSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error {
canAny, err := s.authorizeUpdateSilencePreConditions(ctx, user)
if canAny || err != nil { // return early if user can either update any silence or there is an error
return err
}
silWithFolder, err := s.withFolders(ctx, user.GetOrgID(), silence)
if err != nil || len(silWithFolder) != 1 {
return fmt.Errorf("resolve rule UID to folder UID: %w", err)
}
return s.authorizeUpdateSilence(ctx, user, silWithFolder[0])
}
// authorizeUpdateSilencePreConditions checks necessary preconditions for updating silences. Returns true if user can
// update any silence. Returns error if user does not have access to update any silences at all.
func (s SilenceService) authorizeUpdateSilencePreConditions(ctx context.Context, user identity.Requester) (bool, error) {
canAny, err := s.HasAccess(ctx, user, updateAnySilenceEvaluator)
if err != nil || canAny {
// return early if user can either update any silence or there is an error
return canAny, err
}
// pre-check whether a user has at least some basic permissions before hit the store
if err := s.HasAccessOrError(ctx, user, updateSomeRuleSilenceEvaluator, func() string { return "update some silences" }); err != nil {
return false, err
}
return false, nil
}
// authorizeUpdateSilence checks if user has access to update a silence given precondition checks have passed.
func (s SilenceService) authorizeUpdateSilence(ctx context.Context, user identity.Requester, silence *silenceWithFolder) error {
if silence.ruleUID == nil {
return s.HasAccessOrError(ctx, user, updateGeneralSilenceEvaluator, func() string {
return "update a general silence"
})
}
if silence.folderUID == "" { // if we did not find folder by rule UID then it does not exist.
return NewAuthorizationErrorGeneric(fmt.Sprintf("create update for rule %s", *silence.ruleUID))
}
return s.HasAccessOrError(ctx, user, updateRuleSilenceEvaluator(silence.folderUID), func() string {
return fmt.Sprintf("update silence for rule %s", *silence.ruleUID)
})
}
// SilenceAccess returns the permission sets for a slice of silences. The permission set includes read, write, and
// create which corresponds the given user being able to read, write, and create each given silence, respectively.
func (s SilenceService) SilenceAccess(ctx context.Context, user identity.Requester, silences []*models.Silence) (map[*models.Silence]models.SilencePermissionSet, error) {
basePerms := make(models.SilencePermissionSet, 3)
canReadAll, err := s.authorizeReadSilencePreConditions(ctx, user)
if err != nil || canReadAll {
basePerms[models.SilencePermissionRead] = err == nil
}
canUpdateAny, err := s.authorizeUpdateSilencePreConditions(ctx, user)
if err != nil || canUpdateAny {
basePerms[models.SilencePermissionWrite] = err == nil
}
canCreateAny, err := s.authorizeCreateSilencePreConditions(ctx, user)
if err != nil || canCreateAny {
basePerms[models.SilencePermissionCreate] = err == nil
}
if basePerms.AllSet() {
// Shortcut for the case when all permissions are known based on preconditions. We don't need to hit the database to find folder UIDs.
return withPermissionSet(silences, basePerms), nil
}
silencesWithFolders, err := s.withFolders(ctx, user.GetOrgID(), silences...)
if err != nil {
return nil, err
}
result := make(map[*models.Silence]models.SilencePermissionSet, len(silences))
accessCacheByFolder := make(map[string]models.SilencePermissionSet)
for _, silWithFolder := range silencesWithFolders {
if perms, ok := accessCacheByFolder[silWithFolder.folderUID]; ok {
result[silWithFolder.Silence] = perms.Clone()
continue
}
permSet := basePerms.Clone()
if _, ok := permSet[models.SilencePermissionRead]; !ok {
err := s.authorizeReadSilence(ctx, user, silWithFolder)
permSet[models.SilencePermissionRead] = err == nil
}
if _, ok := permSet[models.SilencePermissionWrite]; !ok {
err := s.authorizeUpdateSilence(ctx, user, silWithFolder)
permSet[models.SilencePermissionWrite] = err == nil
}
if _, ok := permSet[models.SilencePermissionCreate]; !ok {
err := s.authorizeCreateSilence(ctx, user, silWithFolder)
permSet[models.SilencePermissionCreate] = err == nil
}
result[silWithFolder.Silence] = permSet
// Cache non-empty namespaces to avoid repeated checks for the same folder.
if silWithFolder.folderUID != "" {
accessCacheByFolder[silWithFolder.folderUID] = permSet
}
}
return result, nil
}
// withFolders resolves rule UIDs to folder UIDs for rule-specific silences and returns a list of silenceWithFolder
// that includes rule information, if available.
func (s SilenceService) withFolders(ctx context.Context, orgID int64, silences ...*models.Silence) ([]*silenceWithFolder, error) {
result := make([]*silenceWithFolder, 0, len(silences))
ruleUIDs := make(map[string]struct{})
for _, silence := range silences {
silWithFolder := silenceWithFolder{Silence: silence, ruleUID: silence.GetRuleUID()}
if silWithFolder.ruleUID != nil {
ruleUIDs[*silWithFolder.ruleUID] = struct{}{}
}
result = append(result, &silWithFolder)
}
if len(ruleUIDs) == 0 {
return result, nil
}
namespaceByRuleUID, err := s.store.GetNamespacesByRuleUID(ctx, orgID, maps.Keys(ruleUIDs)...)
if err != nil {
return nil, err
}
for _, silWithFolder := range result {
if silWithFolder.ruleUID != nil {
silWithFolder.folderUID = namespaceByRuleUID[*silWithFolder.ruleUID]
}
}
return result, nil
}
func withPermissionSet(silences []*models.Silence, perms models.SilencePermissionSet) map[*models.Silence]models.SilencePermissionSet {
result := make(map[*models.Silence]models.SilencePermissionSet, len(silences))
for _, silence := range silences {
result[silence] = perms.Clone()
}
return result
}