Alerting: Fix folder permissions for Editor role in Prometheus import (#109977)

Alerting: Fix folder permisisons for Editor role in Prometheus import
This commit is contained in:
Alexander Akhmetov
2025-08-22 13:15:53 +02:00
committed by GitHub
parent 33ca5f166f
commit b4ff398865
6 changed files with 87 additions and 13 deletions
@@ -461,7 +461,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context
func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger, workingFolderUID string) (*folder.FolderReference, response.Response) {
logger.Debug("Getting or creating a new folder")
ns, err := srv.ruleStore.GetOrCreateNamespaceByTitle(
ns, created, err := srv.ruleStore.GetOrCreateNamespaceByTitle(
c.Req.Context(),
title,
c.GetOrgID(),
@@ -473,6 +473,26 @@ func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext
return nil, namespaceErrorResponse(err)
}
// Not all users have global-scoped permissions, even if they can create folders.
// For example, Editor users can create folders, but they have UID-scoped folder permissions.
// Permissions are populated in a middleware before this handler, and the folder we just created
// is not included in the permissions yet. We add it manually.
if created {
orgID := c.GetOrgID()
if c.Permissions == nil {
c.Permissions = make(map[int64]map[string][]string)
}
if c.Permissions[orgID] == nil {
c.Permissions[orgID] = make(map[string][]string)
}
folderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(ns.UID)
if c.Permissions[orgID][dashboards.ActionFoldersRead] == nil {
c.Permissions[orgID][dashboards.ActionFoldersRead] = []string{}
}
c.Permissions[orgID][dashboards.ActionFoldersRead] = append(c.Permissions[orgID][dashboards.ActionFoldersRead], folderScope)
}
logger.Debug("Using folder for the converted rules", "folder_uid", ns.UID)
return ns, nil
+1 -1
View File
@@ -16,7 +16,7 @@ type RuleStore interface {
GetUserVisibleNamespaces(context.Context, int64, identity.Requester) (map[string]*folder.Folder, error)
GetNamespaceByUID(ctx context.Context, uid string, orgID int64, user identity.Requester) (*folder.Folder, error)
GetNamespaceByTitle(ctx context.Context, fullpath string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, error)
GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, error)
GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, bool, error)
// GetNamespaceChildren returns all children (first level) of the namespace with the given id.
GetNamespaceChildren(ctx context.Context, uid string, orgID int64, user identity.Requester) ([]*folder.FolderReference, error)
+9 -7
View File
@@ -95,24 +95,25 @@ func (st DBstore) GetNamespaceByTitle(ctx context.Context, title string, orgID i
//
// 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, error) {
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, fmt.Errorf("title is empty")
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, err
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, fmt.Errorf("error creating a new folder: %w", err)
return nil, false, fmt.Errorf("error creating a new folder: %w", err)
}
cmd := &folder.CreateFolderCommand{
@@ -129,20 +130,21 @@ func (st DBstore) GetOrCreateNamespaceByTitle(ctx context.Context, title string,
// the folder between our check and creation attempt
existingFolder, lookupErr := st.GetNamespaceByTitle(ctx, title, orgID, user, parentUID)
if lookupErr == nil {
return existingFolder, nil
return existingFolder, false, nil
}
// If we couldn't find it, return errors
return nil, fmt.Errorf("failed to get or create folder: %w", errors.Join(
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, nil
return f, created, nil
}
// generateAlertingFolderUID creates a deterministic UID for folders
+2 -1
View File
@@ -78,7 +78,8 @@ func TestGetNamespaceByTitle(t *testing.T) {
func TestGetOrCreateNamespaceByTitle(t *testing.T) {
store := DBstore{}
_, err := store.GetOrCreateNamespaceByTitle(context.Background(), "", 1, nil, folder.RootFolderUID)
_, created, err := store.GetOrCreateNamespaceByTitle(context.Background(), "", 1, nil, folder.RootFolderUID)
require.False(t, created)
require.Error(t, err)
require.Contains(t, err.Error(), "title is empty")
+3 -3
View File
@@ -363,13 +363,13 @@ func (f *RuleStore) GetNamespaceByUID(_ context.Context, uid string, orgID int64
return nil, dashboards.ErrFolderNotFound
}
func (f *RuleStore) GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, error) {
func (f *RuleStore) GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, bool, error) {
f.mtx.Lock()
defer f.mtx.Unlock()
for _, folder := range f.Folders[orgID] {
if folder.Title == title && folder.ParentUID == parentUID {
return folder.ToFolderReference(), nil
return folder.ToFolderReference(), false, nil
}
}
@@ -382,7 +382,7 @@ func (f *RuleStore) GetOrCreateNamespaceByTitle(ctx context.Context, title strin
}
f.Folders[orgID] = append(f.Folders[orgID], newFolder)
return newFolder.ToFolderReference(), nil
return newFolder.ToFolderReference(), true, nil
}
func (f *RuleStore) GetNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.FolderReference, error) {
@@ -1167,6 +1167,57 @@ func TestIntegrationConvertPrometheusEndpoints_Delete(t *testing.T) {
})
}
func TestIntegrationConvertPrometheusEndpoints_Editor(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
testinfra.SQLiteIntegrationTest(t)
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableRecordingRules: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "editor",
})
editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "password")
ds := adminClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
testGroup := apimodels.PrometheusRuleGroup{
Name: "test-group-permission-cache",
Interval: prommodel.Duration(60 * time.Second),
Rules: []apimodels.PrometheusRule{
{
Alert: "test-editor-permissions",
Expr: "vector(0)",
For: util.Pointer(prommodel.Duration(1 * time.Minute)),
},
},
}
ns := util.GenerateShortUID()
t.Run("editor can import rules that create new folder", func(t *testing.T) {
editorClient.ConvertPrometheusPostRuleGroup(t, ns, ds.Body.Datasource.UID, testGroup, nil)
group := editorClient.ConvertPrometheusGetRuleGroupRules(t, ns, testGroup.Name, nil)
require.Equal(t, testGroup.Name, group.Name)
require.Len(t, group.Rules, 1)
require.Equal(t, testGroup.Rules[0].Alert, group.Rules[0].Alert)
})
}
func TestIntegrationConvertPrometheusEndpoints_GroupLabels(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")