Folders: Add better integration tests (#111241)

This commit is contained in:
Ryan McKinley
2025-09-17 20:19:50 +03:00
committed by GitHub
parent b7fa49765d
commit 14b6e60f31
8 changed files with 621 additions and 77 deletions
+6 -6
View File
@@ -19,7 +19,7 @@ import (
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
claims "github.com/grafana/authlib/types"
authlib "github.com/grafana/authlib/types"
internal "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard"
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
@@ -80,7 +80,7 @@ type DashboardsAPIBuilder struct {
authorizer authorizer.Authorizer
accessControl accesscontrol.AccessControl
accessClient claims.AccessClient
accessClient authlib.AccessClient
legacy *DashboardStorage
unified resource.ResourceClient
dashboardProvisioningService dashboards.DashboardProvisioningService
@@ -114,7 +114,7 @@ func RegisterAPIService(
dashboardPermissions dashboards.PermissionsRegistrationService,
dashboardPermissionsSvc accesscontrol.DashboardPermissionsService,
accessControl accesscontrol.AccessControl,
accessClient claims.AccessClient,
accessClient authlib.AccessClient,
provisioning provisioning.ProvisioningService,
dashStore dashboards.Store,
reg prometheus.Registerer,
@@ -168,7 +168,7 @@ func RegisterAPIService(
return builder
}
func NewAPIService(ac claims.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceInfoProvider, pluginStore *pluginstore.Service) *DashboardsAPIBuilder {
func NewAPIService(ac authlib.AccessClient, features featuremgmt.FeatureToggles, folderClientProvider client.K8sHandlerProvider, datasourceProvider schemaversion.DataSourceInfoProvider, pluginStore *pluginstore.Service) *DashboardsAPIBuilder {
// TODO: Plugin store will soon be removed,
// as the cases for plugin fetching is not needed. Keeping it now to not break implementation
if pluginStore == nil {
@@ -275,7 +275,7 @@ func (b *DashboardsAPIBuilder) validateDelete(ctx context.Context, a admission.A
return nil
}
nsInfo, err := claims.ParseNamespace(a.GetNamespace())
nsInfo, err := authlib.ParseNamespace(a.GetNamespace())
if err != nil {
return fmt.Errorf("%v: %w", "failed to parse namespace", err)
}
@@ -386,7 +386,7 @@ func (b *DashboardsAPIBuilder) validateUpdate(ctx context.Context, a admission.A
}
// Parse namespace for old dashboard
nsInfo, err := claims.ParseNamespace(oldAccessor.GetNamespace())
nsInfo, err := authlib.ParseNamespace(oldAccessor.GetNamespace())
if err != nil {
return fmt.Errorf("failed to parse namespace: %w", err)
}
+3 -3
View File
@@ -2,11 +2,11 @@ package rbac
import claims "github.com/grafana/authlib/types"
type CheckRequest struct {
type checkRequest struct {
Namespace claims.NamespaceInfo
IdentityType claims.IdentityType
UserUID string
Action string
Action string // Verb has been mapped into an action
Group string
Resource string
Verb string
@@ -14,7 +14,7 @@ type CheckRequest struct {
ParentFolder string
}
type ListRequest struct {
type listRequest struct {
Namespace claims.NamespaceInfo
IdentityType claims.IdentityType
UserUID string
+11 -10
View File
@@ -240,7 +240,7 @@ func (s *Service) List(ctx context.Context, req *authzv1.ListRequest) (*authzv1.
return resp, err
}
func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRequest) (*CheckRequest, error) {
func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRequest) (*checkRequest, error) {
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.validateCheckRequest")
defer span.End()
@@ -259,7 +259,7 @@ func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRe
return nil, err
}
checkReq := &CheckRequest{
checkReq := &checkRequest{
Namespace: ns,
UserUID: userUID,
IdentityType: idType,
@@ -273,7 +273,7 @@ func (s *Service) validateCheckRequest(ctx context.Context, req *authzv1.CheckRe
return checkReq, nil
}
func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequest) (*ListRequest, error) {
func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequest) (*listRequest, error) {
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.validateListRequest")
defer span.End()
@@ -300,7 +300,7 @@ func (s *Service) validateListRequest(ctx context.Context, req *authzv1.ListRequ
SkipCache: authzOptions.Skipcache,
}
listReq := &ListRequest{
listReq := &listRequest{
Namespace: ns,
UserUID: userUID,
IdentityType: idType,
@@ -352,18 +352,19 @@ func (s *Service) validateSubject(ctx context.Context, subject string) (string,
return userUID, identityType, nil
}
// Find the action for a selected verb
func (s *Service) validateAction(ctx context.Context, group, resource, verb string) (string, error) {
ctxLogger := s.logger.FromContext(ctx)
t, ok := s.mapper.Get(group, resource)
if !ok {
ctxLogger.Error("unsupport resource", "group", group, "resource", resource)
ctxLogger.Error("unsupported resource", "group", group, "resource", resource)
return "", status.Error(codes.NotFound, "unsupported resource")
}
action, ok := t.Action(verb)
if !ok {
ctxLogger.Error("unsupport verb", "group", group, "resource", resource, "verb", verb)
ctxLogger.Error("unsupported verb", "group", group, "resource", resource, "verb", verb)
return "", status.Error(codes.NotFound, "unsupported verb")
}
@@ -591,7 +592,7 @@ func (s *Service) getUserBasicRole(ctx context.Context, ns types.NamespaceInfo,
return *basicRole, nil
}
func (s *Service) checkPermission(ctx context.Context, scopeMap map[string]bool, req *CheckRequest) (bool, error) {
func (s *Service) checkPermission(ctx context.Context, scopeMap map[string]bool, req *checkRequest) (bool, error) {
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.checkPermission", trace.WithAttributes(
attribute.Int("scope_count", len(scopeMap))))
defer span.End()
@@ -648,7 +649,7 @@ func getScopeMap(permissions []accesscontrol.Permission) map[string]bool {
return permMap
}
func (s *Service) checkInheritedPermissions(ctx context.Context, scopeMap map[string]bool, req *CheckRequest) (bool, error) {
func (s *Service) checkInheritedPermissions(ctx context.Context, scopeMap map[string]bool, req *checkRequest) (bool, error) {
if req.ParentFolder == "" {
return false, nil
}
@@ -725,7 +726,7 @@ func (s *Service) buildFolderTree(ctx context.Context, ns types.NamespaceInfo) (
return res.(folderTree), nil
}
func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, req *ListRequest) (*authzv1.ListResponse, error) {
func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool, req *listRequest) (*authzv1.ListResponse, error) {
if scopeMap["*"] {
return &authzv1.ListResponse{
All: true,
@@ -739,7 +740,7 @@ func (s *Service) listPermission(ctx context.Context, scopeMap map[string]bool,
t, ok := s.mapper.Get(req.Group, req.Resource)
if !ok {
ctxLogger.Error("unsupport resource", "group", req.Group, "resource", req.Resource)
ctxLogger.Error("unsupported resource", "group", req.Group, "resource", req.Resource)
return nil, status.Error(codes.NotFound, "unsupported resource")
}
+96 -27
View File
@@ -16,7 +16,7 @@ import (
authzv1 "github.com/grafana/authlib/authz/proto/v1"
"github.com/grafana/authlib/cache"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
@@ -30,7 +30,7 @@ func TestService_checkPermission(t *testing.T) {
type testCase struct {
name string
permissions []accesscontrol.Permission
check CheckRequest
check checkRequest
folders []store.Folder
expected bool
}
@@ -47,7 +47,7 @@ func TestService_checkPermission(t *testing.T) {
Identifier: "some_dashboard",
},
},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -66,7 +66,7 @@ func TestService_checkPermission(t *testing.T) {
Identifier: "another_dashboard",
},
},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -85,7 +85,7 @@ func TestService_checkPermission(t *testing.T) {
Identifier: "*",
},
},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -103,7 +103,7 @@ func TestService_checkPermission(t *testing.T) {
Attribute: "*",
},
},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -120,7 +120,7 @@ func TestService_checkPermission(t *testing.T) {
Kind: "*",
},
},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -139,7 +139,7 @@ func TestService_checkPermission(t *testing.T) {
Identifier: "general",
},
},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:create",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -154,7 +154,7 @@ func TestService_checkPermission(t *testing.T) {
Action: "dashboards:create",
},
},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:create",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -165,7 +165,7 @@ func TestService_checkPermission(t *testing.T) {
{
name: "should return false if user has no permissions on resource",
permissions: []accesscontrol.Permission{},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -187,7 +187,7 @@ func TestService_checkPermission(t *testing.T) {
{UID: "parent"},
{UID: "child", ParentUID: strPtr("parent")},
},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -208,7 +208,7 @@ func TestService_checkPermission(t *testing.T) {
},
},
folders: []store.Folder{{UID: "parent"}},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:create",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -230,7 +230,7 @@ func TestService_checkPermission(t *testing.T) {
},
},
folders: []store.Folder{{UID: "parent"}, {UID: "other_parent"}},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:create",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -252,7 +252,7 @@ func TestService_checkPermission(t *testing.T) {
},
},
folders: []store.Folder{{UID: "parent"}},
check: CheckRequest{
check: checkRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -273,7 +273,7 @@ func TestService_checkPermission(t *testing.T) {
Identifier: "ds1",
},
},
check: CheckRequest{
check: checkRequest{
Action: "datasources:query",
Group: "query.grafana.app",
Resource: "query",
@@ -293,7 +293,7 @@ func TestService_checkPermission(t *testing.T) {
Identifier: "ds2",
},
},
check: CheckRequest{
check: checkRequest{
Action: "datasources:query",
Group: "query.grafana.app",
Resource: "query",
@@ -307,7 +307,7 @@ func TestService_checkPermission(t *testing.T) {
permissions: []accesscontrol.Permission{
{Action: "teams:create"},
},
check: CheckRequest{
check: checkRequest{
Action: "teams:create",
Group: "iam.grafana.app",
Resource: "teams",
@@ -330,6 +330,75 @@ func TestService_checkPermission(t *testing.T) {
}
}
func TestService_mapping(t *testing.T) {
type testCase struct {
name string
input *authzv1.CheckRequest
output *checkRequest
err string
}
ns := "default"
testUserA := &identity.StaticRequester{
Type: types.TypeUser,
Login: "test",
UserID: 123,
UserUID: "u123",
OrgRole: identity.RoleAdmin,
IsGrafanaAdmin: true, // can do anything
Namespace: ns,
OrgID: 1,
}
ctx := types.WithAuthInfo(request.WithNamespace(context.Background(), ns), testUserA)
testCases := []testCase{
{
name: "should return true if user has permission",
input: &authzv1.CheckRequest{
Group: "folder.grafana.app",
Resource: "folders",
Name: "aaa",
Verb: utils.VerbCreate,
Folder: "folder",
},
output: &checkRequest{
Action: "folders:create",
Group: "folder.grafana.app",
Resource: "folders",
Name: "aaa",
Verb: "create",
ParentFolder: "folder",
Namespace: types.NamespaceInfo{
Value: ns,
OrgID: 1,
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := setupService()
tc.input.Namespace = ns
tc.input.Subject = testUserA.GetUID() // the subject string
got, err := s.validateCheckRequest(ctx, tc.input)
if tc.err != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
tc.output.IdentityType = types.TypeUser
tc.output.UserUID = testUserA.GetIdentifier()
require.Equal(t, tc.output, got)
})
}
}
func TestService_checkPermission_folderCacheMissRecovery(t *testing.T) {
s := setupService()
ctx := context.Background()
@@ -350,7 +419,7 @@ func TestService_checkPermission_folderCacheMissRecovery(t *testing.T) {
s.folderCache.Set(ctx, folderCacheKey("default"), newFolderTree([]store.Folder{{UID: "root"}}))
// Perform check on sub folder
check := CheckRequest{
check := checkRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -387,7 +456,7 @@ func TestService_listPermission_skipCache(t *testing.T) {
s.folderCache.Set(ctx, folderCacheKey("default"), newFolderTree([]store.Folder{{UID: "root"}}))
// Perform list
listReq := ListRequest{
listReq := listRequest{
Action: "folders:read",
Group: "folder.grafana.app",
Resource: "folders",
@@ -629,7 +698,7 @@ func TestService_listPermission(t *testing.T) {
name string
permissions []accesscontrol.Permission
folders []store.Folder
list ListRequest
list listRequest
expectedItems []string
expectedFolders []string
expectedAll bool
@@ -645,7 +714,7 @@ func TestService_listPermission(t *testing.T) {
Kind: "*",
},
},
list: ListRequest{
list: listRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -682,7 +751,7 @@ func TestService_listPermission(t *testing.T) {
{UID: "some_folder_1"},
{UID: "some_folder_2"},
},
list: ListRequest{
list: listRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -710,7 +779,7 @@ func TestService_listPermission(t *testing.T) {
{UID: "some_folder_subsubchild", ParentUID: strPtr("some_folder_subchild2")},
{UID: "some_folder_1", ParentUID: strPtr("some_other_folder")},
},
list: ListRequest{
list: listRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -740,7 +809,7 @@ func TestService_listPermission(t *testing.T) {
{UID: "some_folder_parent"},
{UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")},
},
list: ListRequest{
list: listRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -773,7 +842,7 @@ func TestService_listPermission(t *testing.T) {
{UID: "some_folder_subchild", ParentUID: strPtr("some_folder_child")},
{UID: "some_folder_child2", ParentUID: strPtr("some_folder_parent")},
},
list: ListRequest{
list: listRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -788,7 +857,7 @@ func TestService_listPermission(t *testing.T) {
folders: []store.Folder{
{UID: "some_folder_1"},
},
list: ListRequest{
list: listRequest{
Action: "dashboards:read",
Group: "dashboard.grafana.app",
Resource: "dashboards",
@@ -810,7 +879,7 @@ func TestService_listPermission(t *testing.T) {
{UID: "some_folder_parent"},
{UID: "some_folder_child", ParentUID: strPtr("some_folder_parent")},
},
list: ListRequest{
list: listRequest{
Action: "folders:read",
Group: "folder.grafana.app",
Resource: "folders",
@@ -13,7 +13,6 @@ import (
"strings"
"testing"
"github.com/grafana/alerting/notify"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
@@ -21,15 +20,12 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"github.com/grafana/alerting/notify"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/routingtree"
test_common "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/routingtree"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
@@ -45,6 +41,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis"
test_common "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
@@ -131,8 +128,7 @@ func TestIntegrationResourcePermissions(t *testing.T) {
helper := getTestHelper(t)
org1 := helper.Org1
noneUser := helper.CreateUser("none", apis.Org1, org.RoleNone, nil)
noneUser := org1.None
creator := helper.CreateUser("creator", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(
+450
View File
@@ -0,0 +1,450 @@
package folder
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/xlab/treeprint"
apierrors "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
foldersV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/util/testutil"
)
func TestIntegrationFolderTree(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
if !db.IsTestDbSQLite() {
t.Skip("test only on sqlite for now")
}
modes := []grafanarest.DualWriterMode{
// grafanarest.Mode1, (nothing new tested)
grafanarest.Mode2, // write both, read legacy
// grafanarest.Mode3, // write both, read unified
// grafanarest.Mode4,
// grafanarest.Mode5,
}
for _, mode := range modes {
t.Run(fmt.Sprintf("mode %d", mode), func(t *testing.T) {
flags := []string{}
if mode >= grafanarest.Mode3 { // make sure modes 0-3 work without it
flags = append(flags, featuremgmt.FlagUnifiedStorageSearch)
}
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: true,
DisableAnonymous: true,
APIServerStorageType: "unified",
EnableFeatureToggles: flags,
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
foldersV1.RESOURCEGROUP: {
DualWriterMode: mode,
},
},
// We set it to 1 here, so we always get forced pagination based on the response size.
UnifiedStorageMaxPageSizeBytes: 1,
})
defer helper.Shutdown()
tests := []struct {
Name string
Definition FolderDefinition
Expected []ExpectedTree
}{
{
Name: "admin-only-tree",
Definition: FolderDefinition{
Children: []FolderDefinition{
{Name: "top",
Creator: helper.Org1.Admin,
Children: []FolderDefinition{
{Name: "middle",
Creator: helper.Org1.Admin,
Children: []FolderDefinition{
{Name: "child",
Creator: helper.Org1.Admin,
},
},
},
},
},
},
},
Expected: []ExpectedTree{
{Users: []apis.User{
helper.Org1.Admin,
helper.Org1.Viewer, // By default, viewer can view all dashboards
}, Listing: `
└── top
....└── middle
........└── child`},
{Users: []apis.User{helper.Org1.None}, Listing: ``},
},
},
}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
tt.Definition.RequireUniqueName(t, make(map[string]bool))
tt.Definition.CreateWithLegacyAPI(t, helper, "")
// CreateWithLegacyAPI
for _, expect := range tt.Expected {
for _, user := range expect.Users {
t.Run(fmt.Sprintf("query as %s", user.Identity.GetLogin()), func(t *testing.T) {
legacy := getFoldersFromLegacyAPISearch(t, user)
legacy.requireEqual(t, expect.Listing, "legacy")
listed := getFoldersFromAPIServerList(t, user)
listed.requireEqual(t, expect.Listing, "listed")
search := getFoldersFromDashboardV0Search(t, user)
search.requireEqual(t, expect.Listing, "search")
// ensure sure GET also works on each folder we can list
requireGettable(t, user, listed)
})
}
}
})
}
})
}
}
type ExpectedTree struct {
Users []apis.User
Listing string
}
type FolderDefinition struct {
Name string
Creator apis.User // The user who will create the folder
Permissions []FolderPermission
Children []FolderDefinition
}
type FolderPermission struct {
User apis.User
Team team.Team
Role identity.RoleType
Access dashboardaccess.PermissionType
}
func (f *FolderDefinition) CreateWithLegacyAPI(t *testing.T, h *apis.K8sTestHelper, parent string) {
if f.Name == "" {
require.Empty(t, parent, "only the root should be empty")
} else {
cfg := dynamic.ConfigFor(f.Creator.NewRestConfig())
cfg.GroupVersion = &schema.GroupVersion{Group: "folder.grafana.app", Version: "v1beta1"} // group does not matter
client, err := rest.RESTClientFor(cfg)
require.NoError(t, err)
body, err := json.Marshal(map[string]any{
"uid": f.Name,
"title": f.Name,
"parentUid": parent,
})
require.NoError(t, err)
var statusCode int
result := client.Post().AbsPath("api", "folders").
Body(body).
SetHeader("Content-type", "application/json").
Do(context.Background()).
StatusCode(&statusCode)
require.NoError(t, result.Error(), f.Name)
require.Equal(t, int(http.StatusOK), statusCode, f.Name)
parent = f.Name
if len(f.Permissions) > 0 {
cmd := dtos.UpdateDashboardACLCommand{}
for _, def := range f.Permissions {
p := dtos.DashboardACLUpdateItem{
TeamID: def.Team.ID, // likely zero
Role: &def.Role,
Permission: def.Access,
}
if def.User.Identity != nil {
p.UserID, err = def.User.Identity.GetInternalID()
require.NoError(t, err)
}
cmd.Items = append(cmd.Items, p)
}
body, err := json.Marshal(cmd)
require.NoError(t, err)
var statusCode int // folders/{folder_uid}/permissions
result = client.Post().AbsPath("api", "folders", parent, "permissions").
Body(body).
SetHeader("Content-type", "application/json").
Do(context.Background()).
StatusCode(&statusCode)
require.NoError(t, result.Error(), f.Name)
require.Equal(t, int(http.StatusOK), statusCode, f.Name)
}
}
for _, child := range f.Children {
child.CreateWithLegacyAPI(t, h, parent)
}
}
func (f *FolderDefinition) CreateWithAPIServer(t *testing.T, h *apis.K8sTestHelper, parent string) {
if f.Name == "" {
require.Empty(t, parent, "only the root should be empty")
} else {
gvr := schema.GroupVersionResource{Group: "folder.grafana.app", Version: "v1beta1", Resource: "folders"}
ns := f.Creator.Identity.GetNamespace()
cfg := dynamic.ConfigFor(f.Creator.NewRestConfig())
dyn, err := dynamic.NewForConfig(cfg)
require.NoError(t, err)
client := dyn.Resource(gvr).Namespace(ns)
obj, err := client.Create(context.Background(), &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": f.Name,
"namespace": ns,
"annotations": map[string]string{
utils.AnnoKeyFolder: parent,
},
},
"spec": map[string]interface{}{
"title": f.Name,
},
},
}, v1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, f.Name, obj.GetName())
}
for _, child := range f.Children {
child.CreateWithAPIServer(t, h, parent)
}
}
func (f *FolderDefinition) RequireUniqueName(t *testing.T, names map[string]bool) {
if f.Name != "" && names[f.Name] {
t.Fatalf("duplicate name: %s", f.Name)
}
names[f.Name] = true
for _, child := range f.Children {
child.RequireUniqueName(t, names)
}
}
type FolderView struct {
Name string
Parent string
Title string
Children []*FolderView
}
func (n *FolderView) forEach(cb func(*FolderView)) {
for _, child := range n.Children {
cb(child)
}
}
func (n *FolderView) requireEqual(t *testing.T, expect string, msg string) {
input := strings.Split(expect, "\n")
output := make([]string, 0, len(input))
for _, v := range input {
v = strings.TrimSpace(v)
if len(v) > 0 {
output = append(output, v)
}
}
expect = strings.Join(output, "\n")
found := dotify(n.build(treeprint.New()))
require.Equal(t, expect, found, fmt.Sprintf("%s // EXPECT:\n%s\n\nFOUND:\n%s", msg, expect, found))
}
func (n *FolderView) build(tree treeprint.Tree) treeprint.Tree {
for _, child := range n.Children {
child.build(tree.AddBranch(child.Name))
}
return tree
}
func getFoldersFromLegacyAPISearch(t *testing.T, who apis.User) *FolderView {
cfg := dynamic.ConfigFor(who.NewRestConfig())
cfg.GroupVersion = &schema.GroupVersion{Group: "folder.grafana.app", Version: "v1beta1"} // group does not matter
client, err := rest.RESTClientFor(cfg)
require.NoError(t, err)
var statusCode int
result := client.Get().AbsPath("api", "search").
Param("type", "dash-folder").
Param("limit", "1000").
Do(context.Background()).
StatusCode(&statusCode)
require.NoError(t, result.Error(), "getting folders with /api/search")
require.Equal(t, int(http.StatusOK), statusCode)
body, err := result.Raw()
require.NoError(t, err)
hits := model.HitList{}
err = json.Unmarshal(body, &hits)
require.NoError(t, err)
lookup := make(map[string]*FolderView, len(hits))
for _, hit := range hits {
lookup[hit.UID] = &FolderView{
Name: hit.UID,
Title: hit.Title,
Parent: hit.FolderUID,
}
}
return makeRoot(t, lookup, "/api/search")
}
func makeRoot(t *testing.T, lookup map[string]*FolderView, name string) *FolderView {
root := &FolderView{}
for _, v := range lookup {
if v.Parent == "" {
root.Children = append(root.Children, v)
} else {
p, ok := lookup[v.Parent]
require.Truef(t, ok, "[%s] parent not found for: %s (parent:%s)", name, v.Name, v.Parent)
p.Children = append(p.Children, v)
}
}
return root
}
func getFoldersFromDashboardV0Search(t *testing.T, who apis.User) *FolderView {
cfg := dynamic.ConfigFor(who.NewRestConfig())
cfg.GroupVersion = &schema.GroupVersion{Group: "dashboard.grafana.app", Version: "v0alpha1"} // group does not matter
client, err := rest.RESTClientFor(cfg)
require.NoError(t, err)
var statusCode int
result := client.Get().AbsPath("apis", "dashboard.grafana.app", "v0alpha1", "namespaces", who.Identity.GetNamespace(), "search").
Param("limit", "1000").
Do(context.Background()).
StatusCode(&statusCode)
err = result.Error()
if err != nil {
if apierrors.IsForbidden(err) {
return &FolderView{} // empty list
}
require.NoError(t, err, "getting folders with /apis/dashboard.grafana.app/v0alpha1/.../search")
}
require.Equal(t, int(http.StatusOK), statusCode)
body, err := result.Raw()
require.NoError(t, err)
results := &dashboardV0.SearchResults{}
err = json.Unmarshal(body, &results)
require.NoError(t, err)
lookup := make(map[string]*FolderView, len(results.Hits))
for _, hit := range results.Hits {
lookup[hit.Name] = &FolderView{
Name: hit.Name,
Title: hit.Title,
Parent: hit.Folder,
}
}
return makeRoot(t, lookup, "dashboards/search")
}
func getFoldersFromAPIServerList(t *testing.T, who apis.User) *FolderView {
gvr := schema.GroupVersionResource{Group: "folder.grafana.app", Version: "v1beta1", Resource: "folders"}
ns := who.Identity.GetNamespace()
cfg := dynamic.ConfigFor(who.NewRestConfig())
dyn, err := dynamic.NewForConfig(cfg)
require.NoError(t, err)
client := dyn.Resource(gvr).Namespace(ns)
result, err := client.List(context.Background(), v1.ListOptions{Limit: 1000})
if apierrors.IsForbidden(err) {
return &FolderView{} // empty list
}
require.NoError(t, err)
lookup := make(map[string]*FolderView, len(result.Items))
for _, hit := range result.Items {
obj, err := utils.MetaAccessor(&hit)
require.NoError(t, err)
title, _, err := unstructured.NestedString(hit.Object, "spec", "title")
require.NoError(t, err)
lookup[hit.GetName()] = &FolderView{
Name: hit.GetName(),
Title: title,
Parent: obj.GetFolder(),
}
}
return makeRoot(t, lookup, "folders/list")
}
func requireGettable(t *testing.T, who apis.User, root *FolderView) {
gvr := schema.GroupVersionResource{Group: "folder.grafana.app", Version: "v1beta1", Resource: "folders"}
ns := who.Identity.GetNamespace()
cfg := dynamic.ConfigFor(who.NewRestConfig())
dyn, err := dynamic.NewForConfig(cfg)
require.NoError(t, err)
client := dyn.Resource(gvr).Namespace(ns)
root.forEach(func(fv *FolderView) {
found, err := client.Get(context.Background(), fv.Name, v1.GetOptions{})
require.NoErrorf(t, err, "getting folder: %s", fv.Name)
require.Equal(t, found.GetName(), fv.Name)
})
}
func dotify(t treeprint.Tree) string {
buff := bytes.Buffer{}
for _, line := range strings.Split(t.String(), "\n") {
if line == "." || line == " " || len(line) == 0 {
continue
}
runes := []rune(line)
for j, r := range runes {
if r == rune(' ') {
runes[j] = '.'
continue
}
break
}
if buff.Len() > 0 {
buff.WriteRune('\n')
}
buff.WriteString(string(runes))
}
return buff.String()
}
+11 -1
View File
@@ -337,6 +337,7 @@ type OrgUsers struct {
Admin User
Editor User
Viewer User
None User
OrgID int64
@@ -566,6 +567,7 @@ func (c *K8sTestHelper) createTestUsers(orgName string) OrgUsers {
Admin: c.CreateUser("admin2", orgName, org.RoleAdmin, nil),
Editor: c.CreateUser("editor", orgName, org.RoleEditor, nil),
Viewer: c.CreateUser("viewer", orgName, org.RoleViewer, nil),
None: c.CreateUser("none", orgName, org.RoleNone, nil),
}
users.OrgID = users.Admin.Identity.GetOrgID()
@@ -621,13 +623,20 @@ func (c *K8sTestHelper) CreateUser(name string, orgName string, basicRole org.Ro
// make org1 admins grafana admins
isGrafanaAdmin := basicRole == identity.RoleAdmin && orgId == 1
login := name
if isGrafanaAdmin {
login = "grafana-admin"
} else if orgId > 1 {
login = fmt.Sprintf("%s-%s", login, c.Namespacer(orgId))
}
u, err := c.userSvc.Create(context.Background(), &user.CreateUserCommand{
DefaultOrgRole: string(basicRole),
Password: user.Password(name),
Login: fmt.Sprintf("%s-%d", name, orgId),
Login: login,
OrgID: orgId,
IsAdmin: isGrafanaAdmin,
Name: name,
})
// for tests to work we need to add grafana admins to every org
@@ -662,6 +671,7 @@ func (c *K8sTestHelper) CreateUser(name string, orgName string, basicRole org.Ro
require.NoError(c.t, err)
s.IDToken = idToken
s.IDTokenClaims = idClaims
s.Namespace = c.Namespacer(orgId)
usr := User{
Identity: s,
+40 -22
View File
@@ -100,29 +100,38 @@ func TestIntegrationIdentity(t *testing.T) {
},
{
"disabled": false,
"email": "admin2-1",
"email": "grafana-admin",
"emailVerified": false,
"grafanaAdmin": true,
"login": "admin2-1",
"name": "",
"login": "grafana-admin",
"name": "admin2",
"provisioned": false
},
{
"disabled": false,
"email": "editor-1",
"email": "editor",
"emailVerified": false,
"grafanaAdmin": false,
"login": "editor-1",
"name": "",
"login": "editor",
"name": "editor",
"provisioned": false
},
{
"disabled": false,
"email": "viewer-1",
"email": "viewer",
"emailVerified": false,
"grafanaAdmin": false,
"login": "viewer-1",
"name": "",
"login": "viewer",
"name": "viewer",
"provisioned": false
},
{
"disabled": false,
"email": "none",
"emailVerified": false,
"grafanaAdmin": false,
"login": "none",
"name": "none",
"provisioned": false
}
]`, found)
@@ -141,41 +150,50 @@ func TestIntegrationIdentity(t *testing.T) {
require.JSONEq(t, `[
{
"disabled": false,
"email": "admin2-1",
"email": "grafana-admin",
"emailVerified": false,
"grafanaAdmin": true,
"login": "admin2-1",
"name": "",
"login": "grafana-admin",
"name": "admin2",
"provisioned": false
},
{
"disabled": false,
"email": "admin2-2",
"email": "admin2-org-2",
"emailVerified": false,
"grafanaAdmin": false,
"login": "admin2-2",
"name": "",
"login": "admin2-org-2",
"name": "admin2",
"provisioned": false
},
{
"disabled": false,
"email": "editor-2",
"email": "editor-org-2",
"emailVerified": false,
"grafanaAdmin": false,
"login": "editor-2",
"name": "",
"login": "editor-org-2",
"name": "editor",
"provisioned": false
},
{
"disabled": false,
"email": "viewer-2",
"email": "viewer-org-2",
"emailVerified": false,
"grafanaAdmin": false,
"login": "viewer-2",
"name": "",
"login": "viewer-org-2",
"name": "viewer",
"provisioned": false
},
{
"disabled": false,
"email": "none-org-2",
"emailVerified": false,
"grafanaAdmin": false,
"login": "none-org-2",
"name": "none",
"provisioned": false
}
]`, found)
] `, found)
})
}