K8s/Permissions: Enable a grant-permissions annotation action to set default permissions (#102527)

* create permissions

* add key

* lint

* structure as a delayed callback

* legacy API hook

* merge main

* wired up

* and folders

* watch repos

* missing return statement

* Set the correct permissions

* add TestAfterCreatePermissionCreator

* do not add perms on folder create

* fix tests

* add annotation on create

* lint

* lint

* ensure we set permissions when the FT is disabled

* remove custom folder_storage

* fix lint

* change default

* lint

* lint

* fix: annotation

* ensure permissions are added on folder legacy

* remove folderstorage again

* fix tests

* add FT

* undo change to folder

* dashboard on create

* remove annotation for folder

* fix tests

* fix prepare after rebase

* fix tests

* fix tests

* fix tests

* lint

* address comments

* add test for prepareObjectForStorage

* add again skipIfMode as per comment

---------

Co-authored-by: Georges Chaudy <chaudyg@gmail.com>
This commit is contained in:
Ryan McKinley
2025-04-09 13:05:37 +02:00
committed by GitHub
co-authored by Georges Chaudy
parent ceed824378
commit af8a70bbab
18 changed files with 466 additions and 83 deletions
+6
View File
@@ -3,11 +3,14 @@ package dashboards
import (
"context"
authtypes "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
@@ -42,6 +45,9 @@ type DashboardService interface {
type PermissionsRegistrationService interface {
RegisterDashboardPermissions(service accesscontrol.DashboardPermissionsService)
// Used to apply default permissions in unified storage after create
SetDefaultPermissionsAfterCreate(ctx context.Context, key *resource.ResourceKey, id authtypes.AuthInfo, obj utils.GrafanaMetaAccessor) error
}
// PluginService is a service for operating on plugin dashboards.
@@ -23,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/selection"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard"
dashboardv0alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
@@ -41,6 +42,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
dashboardclient "github.com/grafana/grafana/pkg/services/dashboards/service/client"
@@ -1186,6 +1188,60 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que
return dr.dashboardStore.GetDashboardsByPluginID(ctx, query)
}
// (sometimes) called by the k8s storage engine after creating an object
func (dr *DashboardServiceImpl) SetDefaultPermissionsAfterCreate(ctx context.Context, key *resource.ResourceKey, id claims.AuthInfo, obj utils.GrafanaMetaAccessor) error {
ctx, span := tracer.Start(ctx, "dashboards.service.SetDefaultPermissionsAfterCreate")
defer span.End()
logger := logging.FromContext(ctx)
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return err
}
user, err := identity.GetRequester(ctx)
if err != nil {
return err
}
uid, err := user.GetInternalID()
if err != nil {
return err
}
var permissions []accesscontrol.SetResourcePermissionCommand
if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesDashboards) {
// legacy behavior
permissions = []accesscontrol.SetResourcePermissionCommand{}
if user.IsIdentityType(claims.TypeUser) {
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
UserID: uid, Permission: dashboardaccess.PERMISSION_ADMIN.String(),
})
}
isNested := obj.GetFolder() != ""
if !isNested || !dr.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
}...)
}
} else {
if obj.GetFolder() != "" {
return nil
}
permissions = []accesscontrol.SetResourcePermissionCommand{
{UserID: uid, Permission: dashboardaccess.PERMISSION_ADMIN.String()},
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_ADMIN.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
}
}
svc := dr.getPermissionsService(key.Resource == "folders")
if _, err := svc.SetPermissions(ctx, ns.OrgID, obj.GetName(), permissions...); err != nil {
logger.Error("Could not set default permissions", "error", err)
return err
}
return nil
}
func (dr *DashboardServiceImpl) SetDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *dashboards.Dashboard, provisioned bool) {
ctx, span := tracer.Start(ctx, "dashboards.service.setDefaultPermissions")
defer span.End()
@@ -1227,6 +1283,10 @@ func (dr *DashboardServiceImpl) SetDefaultPermissions(ctx context.Context, dto *
}
func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context, cmd *folder.CreateFolderCommand, f *folder.Folder, provisioned bool) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return
}
ctx, span := tracer.Start(ctx, "dashboards.service.setDefaultFolderPermissions")
defer span.End()
@@ -1792,7 +1852,6 @@ func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd
if err != nil {
return nil, err
}
dashboard.SetPluginIDMeta(obj, cmd.PluginID)
out, err := dr.k8sclient.Update(ctx, obj, orgID)
@@ -14,6 +14,7 @@ import (
"gopkg.in/ini.v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apiserver/pkg/endpoints/request"
dashboardv1alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
@@ -28,6 +29,7 @@ import (
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
@@ -2410,6 +2412,110 @@ func TestLegacySaveCommandToUnstructured(t *testing.T) {
})
}
func TestSetDefaultPermissionsAfterCreate(t *testing.T) {
t.Run("Should set correct default permissions", func(t *testing.T) {
testCases := []struct {
name string
rootFolder bool
featureKubernetesDashboards bool
expectedPermission []accesscontrol.SetResourcePermissionCommand
}{
{
name: "without kubernetesDashboards feature in root folder",
rootFolder: true,
featureKubernetesDashboards: false,
expectedPermission: []accesscontrol.SetResourcePermissionCommand{
{UserID: 1, Permission: dashboardaccess.PERMISSION_ADMIN.String()},
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
},
},
{
name: "with kubernetesDashboards feature in root folder",
rootFolder: true,
featureKubernetesDashboards: true,
expectedPermission: []accesscontrol.SetResourcePermissionCommand{
{UserID: 1, Permission: dashboardaccess.PERMISSION_ADMIN.String()},
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_ADMIN.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
},
},
{
name: "without kubernetesDashboards feature in subfolder",
rootFolder: false,
featureKubernetesDashboards: false,
expectedPermission: []accesscontrol.SetResourcePermissionCommand{
{UserID: 1, Permission: dashboardaccess.PERMISSION_ADMIN.String()},
},
},
{
name: "with kubernetesDashboards feature in subfolder",
rootFolder: false,
featureKubernetesDashboards: true,
expectedPermission: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
user := &user.SignedInUser{
OrgID: 1,
OrgRole: "Admin",
UserID: 1,
}
ctx := request.WithNamespace(context.Background(), "default")
ctx = identity.WithRequester(ctx, user)
// Setup mocks and service
dashboardStore := &dashboards.FakeDashboardStore{}
folderStore := foldertest.FakeFolderStore{}
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
if tc.featureKubernetesDashboards {
features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesDashboards, featuremgmt.FlagNestedFolders)
}
permService := acmock.NewMockedPermissionsService()
permService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
service := &DashboardServiceImpl{
cfg: setting.NewCfg(),
log: log.New("test-logger"),
dashboardStore: dashboardStore,
folderStore: &folderStore,
features: features,
dashboardPermissions: permService,
folderPermissions: permService,
dashboardPermissionsReady: make(chan struct{}),
}
service.RegisterDashboardPermissions(permService)
// Create test object
key := &resource.ResourceKey{Group: "dashboard.grafana.app", Resource: "dashboards", Name: "test", Namespace: "default"}
obj := &dashboardv1alpha1.Dashboard{
TypeMeta: metav1.TypeMeta{
APIVersion: "dashboard.grafana.app/v0alpha1",
},
}
meta, err := utils.MetaAccessor(obj)
require.NoError(t, err)
if !tc.rootFolder {
meta.SetFolder("subfolder")
}
// Call the method
err = service.SetDefaultPermissionsAfterCreate(ctx, key, user, meta)
require.NoError(t, err)
// Verify results
if tc.expectedPermission == nil {
permService.AssertNotCalled(t, "SetPermissions")
} else {
permService.AssertCalled(t, "SetPermissions", mock.Anything, mock.Anything, mock.Anything, tc.expectedPermission)
}
})
}
})
}
func TestCleanUpDashboard(t *testing.T) {
tests := []struct {
name string
@@ -50,6 +50,7 @@ func (ss *FolderUnifiedStoreImpl) Create(ctx context.Context, cmd folder.CreateF
if err != nil {
return nil, err
}
out, err := ss.k8sclient.Create(ctx, obj, cmd.OrgID)
if err != nil {
return nil, err