Files
grafana/pkg/services/ngalert/store/namespace.go
T
Alexander Akhmetov b4ff398865 Alerting: Fix folder permissions for Editor role in Prometheus import (#109977)
Alerting: Fix folder permisisons for Editor role in Prometheus import
2025-08-22 13:15:53 +02:00

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
}