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 }