b4ff398865
Alerting: Fix folder permisisons for Editor role in Prometheus import
178 lines
5.3 KiB
Go
178 lines
5.3 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
)
|
|
|
|
// GetUserVisibleNamespaces returns the folders that are visible to the user
|
|
func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user identity.Requester) (map[string]*folder.Folder, error) {
|
|
folders, err := st.FolderService.GetFolders(ctx, folder.GetFoldersQuery{
|
|
OrgID: orgID,
|
|
WithFullpath: true,
|
|
SignedInUser: user,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
namespaceMap := make(map[string]*folder.Folder)
|
|
for _, f := range folders {
|
|
namespaceMap[f.UID] = f
|
|
}
|
|
return namespaceMap, nil
|
|
}
|
|
|
|
// GetNamespaceByUID is a handler for retrieving a namespace by its UID. Alerting rules follow a Grafana folder-like structure which we call namespaces.
|
|
func (st DBstore) GetNamespaceByUID(ctx context.Context, uid string, orgID int64, user identity.Requester) (*folder.Folder, error) {
|
|
f, err := st.FolderService.GetFolders(ctx, folder.GetFoldersQuery{OrgID: orgID, UIDs: []string{uid}, WithFullpath: true, SignedInUser: user})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(f) == 0 {
|
|
return nil, dashboards.ErrFolderAccessDenied
|
|
}
|
|
return f[0], nil
|
|
}
|
|
|
|
// GetNamespaceChildren gets namespace (folder) children (first level) by its UID.
|
|
func (st DBstore) GetNamespaceChildren(ctx context.Context, uid string, orgID int64, user identity.Requester) ([]*folder.FolderReference, error) {
|
|
q := &folder.GetChildrenQuery{
|
|
UID: uid,
|
|
OrgID: orgID,
|
|
SignedInUser: user,
|
|
}
|
|
folders, err := st.FolderService.GetChildren(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
found := make([]*folder.FolderReference, 0, len(folders))
|
|
for _, f := range folders {
|
|
if f.ParentUID == uid {
|
|
found = append(found, f)
|
|
}
|
|
}
|
|
|
|
return found, nil
|
|
}
|
|
|
|
// GetNamespaceByTitle gets namespace by its title in the specified folder.
|
|
func (st DBstore) GetNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, error) {
|
|
folders, err := st.GetNamespaceChildren(ctx, parentUID, orgID, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
foundByTitle := []*folder.FolderReference{}
|
|
for _, f := range folders {
|
|
if f.Title == title {
|
|
foundByTitle = append(foundByTitle, f)
|
|
}
|
|
}
|
|
|
|
if len(foundByTitle) == 0 {
|
|
return nil, dashboards.ErrFolderNotFound
|
|
}
|
|
|
|
// Sort by UID to return the first folder in case of multiple folders with the same title
|
|
sort.Slice(foundByTitle, func(i, j int) bool {
|
|
return foundByTitle[i].UID < foundByTitle[j].UID
|
|
})
|
|
|
|
return foundByTitle[0], nil
|
|
}
|
|
|
|
// GetOrCreateNamespaceByTitle gets or creates a namespace by title in the specified folder.
|
|
//
|
|
// To avoid race conditions when two concurrent requests try to create the same folder,
|
|
// we create folders with a deterministic UID based on the parent UID, title, and organization ID.
|
|
func (st DBstore) GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, bool, error) {
|
|
if len(title) == 0 {
|
|
return nil, false, fmt.Errorf("title is empty")
|
|
}
|
|
|
|
var f *folder.FolderReference
|
|
var err error
|
|
var created bool
|
|
|
|
f, err = st.GetNamespaceByTitle(ctx, title, orgID, user, parentUID)
|
|
if err != nil && !errors.Is(err, dashboards.ErrFolderNotFound) {
|
|
return nil, false, err
|
|
}
|
|
|
|
if f == nil {
|
|
// Generate a deterministic UID with an alerting prefix
|
|
uid, err := generateAlertingFolderUID(title, parentUID, orgID)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("error creating a new folder: %w", err)
|
|
}
|
|
|
|
cmd := &folder.CreateFolderCommand{
|
|
UID: uid,
|
|
OrgID: orgID,
|
|
Title: title,
|
|
SignedInUser: user,
|
|
ParentUID: parentUID,
|
|
}
|
|
var newFolder *folder.Folder
|
|
newFolder, err = st.FolderService.Create(ctx, cmd)
|
|
if err != nil {
|
|
// Handle potential race condition where another request might have created
|
|
// the folder between our check and creation attempt
|
|
existingFolder, lookupErr := st.GetNamespaceByTitle(ctx, title, orgID, user, parentUID)
|
|
if lookupErr == nil {
|
|
return existingFolder, false, nil
|
|
}
|
|
|
|
// If we couldn't find it, return errors
|
|
return nil, false, fmt.Errorf("failed to get or create folder: %w", errors.Join(
|
|
fmt.Errorf("create folder: %w", err),
|
|
fmt.Errorf("lookup folder: %w", lookupErr),
|
|
))
|
|
}
|
|
|
|
f = newFolder.ToFolderReference()
|
|
created = true
|
|
}
|
|
|
|
return f, created, nil
|
|
}
|
|
|
|
// generateAlertingFolderUID creates a deterministic UID for folders
|
|
// based on the title and parent UID to avoid race conditions when multiple
|
|
// identical folders are created concurrently
|
|
func generateAlertingFolderUID(title string, parentUID string, orgID int64) (string, error) {
|
|
h := fnv.New64a()
|
|
|
|
hashData := [][]byte{
|
|
[]byte(parentUID),
|
|
{0}, // separator
|
|
[]byte(title),
|
|
{0},
|
|
[]byte(strconv.FormatInt(orgID, 10)),
|
|
}
|
|
|
|
// Add hashData strings to the hash with a separator between them
|
|
for _, data := range hashData {
|
|
_, err := h.Write(data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// Create a deterministic string with alerting prefix
|
|
base36 := strconv.FormatUint(h.Sum64(), 36)
|
|
uid := fmt.Sprintf("alerting-%s", base36)
|
|
|
|
return uid, nil
|
|
}
|