mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 04:22:13 +08:00
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:
@ -26,6 +26,13 @@ const LabelKeyGetTrash = "grafana.app/get-trash"
|
||||
// AnnoKeyKubectlLastAppliedConfig is the annotation kubectl writes with the entire previous config
|
||||
const AnnoKeyKubectlLastAppliedConfig = "kubectl.kubernetes.io/last-applied-configuration"
|
||||
|
||||
// AnnoKeyGrantPermissions allows users to explicitly grant themself permissions when creating
|
||||
// resoures in the "root" folder. This annotation is not saved and invalud for update.
|
||||
const AnnoKeyGrantPermissions = "grafana.app/grant-permissions"
|
||||
|
||||
// AnnoGrantPermissionsDefault is the value that should be sent with AnnoKeyGrantPermissions
|
||||
const AnnoGrantPermissionsDefault = "default"
|
||||
|
||||
// DeletedGeneration is set on Resources that have been (soft) deleted
|
||||
const DeletedGeneration = int64(-999)
|
||||
|
||||
@ -206,14 +213,10 @@ func (m *grafanaMetaAccessor) SetAnnotation(key string, val string) {
|
||||
|
||||
func (m *grafanaMetaAccessor) GetAnnotation(key string) string {
|
||||
anno := m.obj.GetAnnotations()
|
||||
if anno != nil {
|
||||
return anno[key]
|
||||
if anno == nil {
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) get(key string) string {
|
||||
return m.obj.GetAnnotations()[key]
|
||||
return anno[key]
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) GetUpdatedTimestamp() (*time.Time, error) {
|
||||
@ -247,7 +250,7 @@ func (m *grafanaMetaAccessor) SetUpdatedTimestamp(v *time.Time) {
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) GetCreatedBy() string {
|
||||
return m.get(AnnoKeyCreatedBy)
|
||||
return m.GetAnnotation(AnnoKeyCreatedBy)
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) SetCreatedBy(user string) {
|
||||
@ -255,7 +258,7 @@ func (m *grafanaMetaAccessor) SetCreatedBy(user string) {
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) GetUpdatedBy() string {
|
||||
return m.get(AnnoKeyUpdatedBy)
|
||||
return m.GetAnnotation(AnnoKeyUpdatedBy)
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) SetUpdatedBy(user string) {
|
||||
@ -263,7 +266,7 @@ func (m *grafanaMetaAccessor) SetUpdatedBy(user string) {
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) GetBlob() *BlobInfo {
|
||||
return ParseBlobInfo(m.get(AnnoKeyBlob))
|
||||
return ParseBlobInfo(m.GetAnnotation(AnnoKeyBlob))
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) SetBlob(info *BlobInfo) {
|
||||
@ -275,7 +278,7 @@ func (m *grafanaMetaAccessor) SetBlob(info *BlobInfo) {
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) GetFolder() string {
|
||||
return m.get(AnnoKeyFolder)
|
||||
return m.GetAnnotation(AnnoKeyFolder)
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) SetFolder(uid string) {
|
||||
@ -283,7 +286,7 @@ func (m *grafanaMetaAccessor) SetFolder(uid string) {
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) GetMessage() string {
|
||||
return m.get(AnnoKeyMessage)
|
||||
return m.GetAnnotation(AnnoKeyMessage)
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) SetMessage(uid string) {
|
||||
@ -329,7 +332,7 @@ func (m *grafanaMetaAccessor) SetDeprecatedInternalID(id int64) {
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) GetFullpath() string {
|
||||
return m.get(AnnoKeyFullpath)
|
||||
return m.GetAnnotation(AnnoKeyFullpath)
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) SetFullpath(path string) {
|
||||
@ -337,7 +340,7 @@ func (m *grafanaMetaAccessor) SetFullpath(path string) {
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) GetFullpathUIDs() string {
|
||||
return m.get(AnnoKeyFullpathUIDs)
|
||||
return m.GetAnnotation(AnnoKeyFullpathUIDs)
|
||||
}
|
||||
|
||||
func (m *grafanaMetaAccessor) SetFullpathUIDs(uids string) {
|
||||
|
@ -204,6 +204,8 @@ func TestMetaAccessor(t *testing.T) {
|
||||
"sloth": "🦥",
|
||||
},
|
||||
}
|
||||
require.Equal(t, "", meta.GetFolder())
|
||||
require.Equal(t, "", meta.GetAnnotation("missing annotation"))
|
||||
|
||||
meta.SetManagerProperties(repoInfo)
|
||||
meta.SetFolder("folderUID")
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
@ -19,7 +18,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
type DashboardStorage struct {
|
||||
@ -27,7 +25,7 @@ type DashboardStorage struct {
|
||||
DashboardService dashboards.DashboardService
|
||||
}
|
||||
|
||||
func (s *DashboardStorage) NewStore(dash utils.ResourceInfo, scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, reg prometheus.Registerer) (grafanarest.Storage, error) {
|
||||
func (s *DashboardStorage) NewStore(dash utils.ResourceInfo, scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, reg prometheus.Registerer, permissions dashboards.PermissionsRegistrationService) (grafanarest.Storage, error) {
|
||||
server, err := resource.NewResourceServer(resource.ResourceServerOptions{
|
||||
Backend: s.Access,
|
||||
Reg: reg,
|
||||
@ -47,6 +45,7 @@ func (s *DashboardStorage) NewStore(dash utils.ResourceInfo, scheme *runtime.Sch
|
||||
optsGetter.RegisterOptions(dash.GroupResource(), apistore.StorageOptions{
|
||||
EnableFolderSupport: true,
|
||||
RequireDeprecatedInternalID: true,
|
||||
Permissions: permissions.SetDefaultPermissionsAfterCreate,
|
||||
})
|
||||
|
||||
store, err := grafanaregistry.NewRegistryStore(scheme, dash, optsGetter)
|
||||
@ -65,15 +64,7 @@ type storeWrapper struct {
|
||||
func (s *storeWrapper) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
ctx = legacy.WithLegacyAccess(ctx)
|
||||
|
||||
meta, err := utils.MetaAccessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
managerProperties, managerPresent := meta.GetManagerProperties()
|
||||
isProvisioned := managerPresent && managerProperties.Kind != utils.ManagerKindUnknown
|
||||
|
||||
obj, err = s.Store.Create(ctx, obj, createValidation, options)
|
||||
obj, err := s.Store.Create(ctx, obj, createValidation, options)
|
||||
access := legacy.GetLegacyAccess(ctx)
|
||||
if access != nil && access.DashboardID > 0 {
|
||||
meta, _ := utils.MetaAccessor(obj)
|
||||
@ -82,10 +73,9 @@ func (s *storeWrapper) Create(ctx context.Context, obj runtime.Object, createVal
|
||||
meta.SetDeprecatedInternalID(access.DashboardID) //nolint:staticcheck
|
||||
}
|
||||
}
|
||||
|
||||
meta, metaErr := utils.MetaAccessor(obj)
|
||||
if metaErr == nil {
|
||||
// Reconstruc the same UID as done at the storage level
|
||||
// Reconstruct the same UID as done at the storage level
|
||||
// https://github.com/grafana/grafana/blob/a84e96fba29c3a1bb384fdbad1c9c658cc79ec8f/pkg/registry/apis/dashboard/legacy/sql_dashboards.go#L287
|
||||
// This is necessary because the UID generated during the creation via legacy storage is actually never stored in the database
|
||||
// and the one returned here is wrong.
|
||||
@ -100,31 +90,6 @@ func (s *storeWrapper) Create(ctx context.Context, obj runtime.Object, createVal
|
||||
return obj, metaErr
|
||||
}
|
||||
|
||||
unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
return obj, err
|
||||
}
|
||||
unstructuredObj := &unstructured.Unstructured{Object: unstructuredMap}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return obj, err
|
||||
}
|
||||
|
||||
legacyDashboard, err := s.DashboardService.UnstructuredToLegacyDashboard(ctx, unstructuredObj, user.GetOrgID())
|
||||
if err != nil {
|
||||
return obj, err
|
||||
}
|
||||
|
||||
// We only need these two parameters for SetDefaultPermissions
|
||||
dto := &dashboards.SaveDashboardDTO{
|
||||
User: user,
|
||||
OrgID: user.GetOrgID(),
|
||||
}
|
||||
|
||||
// Temporary approach to set default permissions until we have a proper method in place via k8s
|
||||
s.DashboardService.SetDefaultPermissions(ctx, dto, legacyDashboard, isProvisioned)
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,7 @@ type DashboardsAPIBuilder struct {
|
||||
legacy *DashboardStorage
|
||||
unified resource.ResourceClient
|
||||
dashboardProvisioningService dashboards.DashboardProvisioningService
|
||||
dashboardPermissions dashboards.PermissionsRegistrationService
|
||||
scheme *runtime.Scheme
|
||||
search *SearchHandler
|
||||
dashStore dashboards.Store
|
||||
@ -94,6 +95,7 @@ func RegisterAPIService(
|
||||
apiregistration builder.APIRegistrar,
|
||||
dashboardService dashboards.DashboardService,
|
||||
provisioningDashboardService dashboards.DashboardProvisioningService,
|
||||
dashboardPermissions dashboards.PermissionsRegistrationService,
|
||||
accessControl accesscontrol.AccessControl,
|
||||
provisioning provisioning.ProvisioningService,
|
||||
dashStore dashboards.Store,
|
||||
@ -116,6 +118,7 @@ func RegisterAPIService(
|
||||
log: log.New("grafana-apiserver.dashboards"),
|
||||
|
||||
dashboardService: dashboardService,
|
||||
dashboardPermissions: dashboardPermissions,
|
||||
features: features,
|
||||
accessControl: accessControl,
|
||||
unified: unified,
|
||||
@ -389,6 +392,9 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
|
||||
storageOpts := apistore.StorageOptions{
|
||||
EnableFolderSupport: true,
|
||||
RequireDeprecatedInternalID: true,
|
||||
|
||||
// Sets default root permissions
|
||||
Permissions: b.dashboardPermissions.SetDefaultPermissionsAfterCreate,
|
||||
}
|
||||
|
||||
// Split dashboards when they are large
|
||||
@ -468,7 +474,7 @@ func (b *DashboardsAPIBuilder) storageForVersion(
|
||||
storage := map[string]rest.Storage{}
|
||||
apiGroupInfo.VersionedResourcesStorageMap[dashboards.GroupVersion().Version] = storage
|
||||
|
||||
legacyStore, err := b.legacy.NewStore(dashboards, opts.Scheme, opts.OptsGetter, b.reg)
|
||||
legacyStore, err := b.legacy.NewStore(dashboards, opts.Scheme, opts.OptsGetter, b.reg, b.dashboardPermissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -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
|
||||
|
52
pkg/storage/unified/apistore/permissions.go
Normal file
52
pkg/storage/unified/apistore/permissions.go
Normal file
@ -0,0 +1,52 @@
|
||||
package apistore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
authtypes "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
type permissionCreatorFunc = func(ctx context.Context) error
|
||||
|
||||
func afterCreatePermissionCreator(ctx context.Context,
|
||||
key *resource.ResourceKey,
|
||||
grantPermisions string,
|
||||
obj runtime.Object,
|
||||
setter DefaultPermissionSetter,
|
||||
) (permissionCreatorFunc, error) {
|
||||
if grantPermisions == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if grantPermisions != utils.AnnoGrantPermissionsDefault {
|
||||
return nil, fmt.Errorf("invalid permissions value. only '%s' supported", utils.AnnoGrantPermissionsDefault)
|
||||
}
|
||||
if setter == nil {
|
||||
return nil, fmt.Errorf("missing default permission creator")
|
||||
}
|
||||
val, err := utils.MetaAccessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if val.GetAnnotation(utils.AnnoKeyManagerKind) != "" {
|
||||
return nil, fmt.Errorf("managed resource may not grant permissions")
|
||||
}
|
||||
auth, ok := authtypes.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("missing auth info")
|
||||
}
|
||||
|
||||
idtype := auth.GetIdentityType()
|
||||
if !(idtype == authtypes.TypeUser || idtype == authtypes.TypeServiceAccount) {
|
||||
return nil, fmt.Errorf("only users or service accounts may grant themselves permissions using an annotation")
|
||||
}
|
||||
|
||||
return func(ctx context.Context) error {
|
||||
return setter(ctx, key, auth, val)
|
||||
}, nil
|
||||
}
|
120
pkg/storage/unified/apistore/permissions_test.go
Normal file
120
pkg/storage/unified/apistore/permissions_test.go
Normal file
@ -0,0 +1,120 @@
|
||||
package apistore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
authtypes "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
|
||||
func TestAfterCreatePermissionCreator(t *testing.T) {
|
||||
mockSetter := func(ctx context.Context, key *resource.ResourceKey, auth authtypes.AuthInfo, val utils.GrafanaMetaAccessor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Run("should return nil when grantPermissions is empty", func(t *testing.T) {
|
||||
creator, err := afterCreatePermissionCreator(context.Background(), nil, "", nil, mockSetter)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, creator)
|
||||
})
|
||||
|
||||
t.Run("should error with invalid grantPermissions value", func(t *testing.T) {
|
||||
creator, err := afterCreatePermissionCreator(context.Background(), nil, "invalid", nil, mockSetter)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, creator)
|
||||
require.Contains(t, err.Error(), "invalid permissions value")
|
||||
})
|
||||
|
||||
t.Run("should error when setter is nil", func(t *testing.T) {
|
||||
creator, err := afterCreatePermissionCreator(context.Background(), nil, utils.AnnoGrantPermissionsDefault, nil, nil)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, creator)
|
||||
require.Contains(t, err.Error(), "missing default permission creator")
|
||||
})
|
||||
|
||||
t.Run("should error for managed resources", func(t *testing.T) {
|
||||
obj := &v0alpha1.Dashboard{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
utils.AnnoKeyManagerKind: "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
creator, err := afterCreatePermissionCreator(context.Background(), nil, utils.AnnoGrantPermissionsDefault, obj, mockSetter)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, creator)
|
||||
require.Contains(t, err.Error(), "managed resource may not grant permissions")
|
||||
})
|
||||
|
||||
t.Run("should error when auth info is missing", func(t *testing.T) {
|
||||
obj := &v0alpha1.Dashboard{}
|
||||
creator, err := afterCreatePermissionCreator(context.Background(), nil, utils.AnnoGrantPermissionsDefault, obj, mockSetter)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, creator)
|
||||
require.Contains(t, err.Error(), "missing auth info")
|
||||
})
|
||||
|
||||
t.Run("should succeed for user identity", func(t *testing.T) {
|
||||
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
OrgRole: "Admin",
|
||||
UserID: 1,
|
||||
})
|
||||
obj := &v0alpha1.Dashboard{}
|
||||
key := &resource.ResourceKey{
|
||||
Group: "test",
|
||||
Resource: "test",
|
||||
Namespace: "test",
|
||||
Name: "test",
|
||||
}
|
||||
|
||||
creator, err := afterCreatePermissionCreator(ctx, key, utils.AnnoGrantPermissionsDefault, obj, mockSetter)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, creator)
|
||||
|
||||
err = creator(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should succeed for service account identity", func(t *testing.T) {
|
||||
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
OrgRole: "Admin",
|
||||
UserID: 1,
|
||||
})
|
||||
obj := &v0alpha1.Dashboard{}
|
||||
key := &resource.ResourceKey{
|
||||
Group: "test",
|
||||
Resource: "test",
|
||||
Namespace: "test",
|
||||
Name: "test",
|
||||
}
|
||||
|
||||
creator, err := afterCreatePermissionCreator(ctx, key, utils.AnnoGrantPermissionsDefault, obj, mockSetter)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, creator)
|
||||
|
||||
err = creator(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should error for non-user/non-service-account identity", func(t *testing.T) {
|
||||
ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{
|
||||
Type: authtypes.TypeAnonymous,
|
||||
})
|
||||
obj := &v0alpha1.Dashboard{}
|
||||
|
||||
creator, err := afterCreatePermissionCreator(ctx, nil, utils.AnnoGrantPermissionsDefault, obj, mockSetter)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, creator)
|
||||
require.Contains(t, err.Error(), "only users or service accounts may grant themselves permissions")
|
||||
})
|
||||
}
|
@ -39,30 +39,35 @@ func formatBytes(numBytes int) string {
|
||||
}
|
||||
|
||||
// Called on create
|
||||
func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime.Object) ([]byte, error) {
|
||||
func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime.Object) ([]byte, string, error) {
|
||||
info, ok := authtypes.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("missing auth info")
|
||||
return nil, "", errors.New("missing auth info")
|
||||
}
|
||||
|
||||
obj, err := utils.MetaAccessor(newObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
if obj.GetName() == "" {
|
||||
return nil, storage.NewInvalidObjError("", "missing name")
|
||||
return nil, "", storage.NewInvalidObjError("", "missing name")
|
||||
}
|
||||
if obj.GetResourceVersion() != "" {
|
||||
return nil, storage.ErrResourceVersionSetOnCreate
|
||||
return nil, "", storage.ErrResourceVersionSetOnCreate
|
||||
}
|
||||
if obj.GetUID() == "" {
|
||||
obj.SetUID(types.UID(uuid.NewString()))
|
||||
}
|
||||
if obj.GetFolder() != "" && !s.opts.EnableFolderSupport {
|
||||
return nil, apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String()))
|
||||
return nil, "", apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String()))
|
||||
}
|
||||
|
||||
grantPermisions := obj.GetAnnotation(utils.AnnoKeyGrantPermissions)
|
||||
if grantPermisions != "" {
|
||||
obj.SetAnnotation(utils.AnnoKeyGrantPermissions, "") // remove the annotation
|
||||
}
|
||||
if err := checkManagerPropertiesOnCreate(info, obj); err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if s.opts.RequireDeprecatedInternalID {
|
||||
@ -88,9 +93,11 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err = s.codec.Encode(newObject, &buf); err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
return s.handleLargeResources(ctx, obj, buf)
|
||||
|
||||
val, err := s.handleLargeResources(ctx, obj, buf)
|
||||
return val, grantPermisions, err
|
||||
}
|
||||
|
||||
// Called on update
|
||||
@ -130,7 +137,8 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti
|
||||
|
||||
obj.SetCreatedBy(previous.GetCreatedBy())
|
||||
obj.SetCreationTimestamp(previous.GetCreationTimestamp())
|
||||
obj.SetResourceVersion("") // removed from saved JSON because the RV is not yet calculated
|
||||
obj.SetResourceVersion("") // removed from saved JSON because the RV is not yet calculated
|
||||
obj.SetAnnotation(utils.AnnoKeyGrantPermissions, "") // Grant is ignored for update requests
|
||||
|
||||
// for dashboards, a mutation hook will set it if it didn't exist on the previous obj
|
||||
// avoid setting it back to 0
|
||||
|
@ -42,14 +42,14 @@ func TestPrepareObjectForStorage(t *testing.T) {
|
||||
)
|
||||
|
||||
t.Run("Error getting auth info from context", func(t *testing.T) {
|
||||
_, err := s.prepareObjectForStorage(context.Background(), nil)
|
||||
_, _, err := s.prepareObjectForStorage(context.Background(), nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "missing auth info")
|
||||
})
|
||||
|
||||
t.Run("Error on missing name", func(t *testing.T) {
|
||||
dashboard := v1alpha1.Dashboard{}
|
||||
_, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
_, _, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "missing name")
|
||||
})
|
||||
@ -58,7 +58,7 @@ func TestPrepareObjectForStorage(t *testing.T) {
|
||||
dashboard := v1alpha1.Dashboard{}
|
||||
dashboard.Name = "test-name"
|
||||
dashboard.ResourceVersion = "123"
|
||||
_, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
_, _, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, storage.ErrResourceVersionSetOnCreate, err)
|
||||
})
|
||||
@ -67,7 +67,7 @@ func TestPrepareObjectForStorage(t *testing.T) {
|
||||
dashboard := v1alpha1.Dashboard{}
|
||||
dashboard.Name = "test-name"
|
||||
|
||||
encodedData, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
encodedData, _, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
require.NoError(t, err)
|
||||
|
||||
newObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{})
|
||||
@ -106,7 +106,7 @@ func TestPrepareObjectForStorage(t *testing.T) {
|
||||
TimestampMillis: now.UnixMilli(),
|
||||
})
|
||||
|
||||
encodedData, err := s.prepareObjectForStorage(ctx, obj)
|
||||
encodedData, _, err := s.prepareObjectForStorage(ctx, obj)
|
||||
require.NoError(t, err)
|
||||
|
||||
newObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{})
|
||||
@ -133,7 +133,7 @@ func TestPrepareObjectForStorage(t *testing.T) {
|
||||
meta.SetFolder("aaa")
|
||||
require.NoError(t, err)
|
||||
|
||||
encodedData, err := s.prepareObjectForStorage(ctx, obj)
|
||||
encodedData, _, err := s.prepareObjectForStorage(ctx, obj)
|
||||
require.NoError(t, err)
|
||||
|
||||
insertedObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{})
|
||||
@ -189,7 +189,7 @@ func TestPrepareObjectForStorage(t *testing.T) {
|
||||
dashboard := v1alpha1.Dashboard{}
|
||||
dashboard.Name = "test-name"
|
||||
|
||||
encodedData, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
encodedData, _, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
require.NoError(t, err)
|
||||
newObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{})
|
||||
require.NoError(t, err)
|
||||
@ -208,7 +208,7 @@ func TestPrepareObjectForStorage(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
meta.SetDeprecatedInternalID(1) // nolint:staticcheck
|
||||
|
||||
encodedData, err := s.prepareObjectForStorage(ctx, obj)
|
||||
encodedData, _, err := s.prepareObjectForStorage(ctx, obj)
|
||||
require.NoError(t, err)
|
||||
newObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{})
|
||||
require.NoError(t, err)
|
||||
@ -217,6 +217,24 @@ func TestPrepareObjectForStorage(t *testing.T) {
|
||||
require.Equal(t, meta.GetDeprecatedInternalID(), int64(1)) // nolint:staticcheck
|
||||
})
|
||||
|
||||
t.Run("Should remove grant permissions annotation", func(t *testing.T) {
|
||||
dashboard := v1alpha1.Dashboard{}
|
||||
dashboard.Name = "test-name"
|
||||
obj := dashboard.DeepCopyObject()
|
||||
meta, err := utils.MetaAccessor(obj)
|
||||
require.NoError(t, err)
|
||||
meta.SetAnnotation(utils.AnnoKeyGrantPermissions, "default")
|
||||
|
||||
encodedData, p, err := s.prepareObjectForStorage(ctx, obj)
|
||||
require.NoError(t, err)
|
||||
newObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{})
|
||||
require.NoError(t, err)
|
||||
meta, err = utils.MetaAccessor(newObject)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, meta.GetAnnotation(utils.AnnoKeyGrantPermissions))
|
||||
require.Equal(t, p, "default")
|
||||
})
|
||||
|
||||
t.Run("calculate generation", func(t *testing.T) {
|
||||
dash := &v1alpha1.Dashboard{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@ -286,7 +304,7 @@ func getPreparedObject(t *testing.T, ctx context.Context, s *Storage, obj runtim
|
||||
var err error
|
||||
|
||||
if old == nil {
|
||||
raw, err = s.prepareObjectForStorage(ctx, obj)
|
||||
raw, _, err = s.prepareObjectForStorage(ctx, obj)
|
||||
} else {
|
||||
raw, err = s.prepareObjectForUpdate(ctx, obj, old)
|
||||
}
|
||||
@ -323,7 +341,7 @@ func TestPrepareLargeObjectForStorage(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, err := f.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
_, _, err := f.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
require.Nil(t, err)
|
||||
require.True(t, los.deconstructed)
|
||||
})
|
||||
@ -341,7 +359,7 @@ func TestPrepareLargeObjectForStorage(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, err := f.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
_, _, err := f.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
|
||||
require.Nil(t, err)
|
||||
require.False(t, los.deconstructed)
|
||||
})
|
||||
|
@ -46,6 +46,8 @@ const (
|
||||
|
||||
var _ storage.Interface = (*Storage)(nil)
|
||||
|
||||
type DefaultPermissionSetter = func(ctx context.Context, key *resource.ResourceKey, id authtypes.AuthInfo, obj utils.GrafanaMetaAccessor) error
|
||||
|
||||
// Optional settings that apply to a single resource
|
||||
type StorageOptions struct {
|
||||
// ????: should we constrain this to only dashboards for now?
|
||||
@ -57,6 +59,9 @@ type StorageOptions struct {
|
||||
|
||||
// Add internalID label when missing
|
||||
RequireDeprecatedInternalID bool
|
||||
|
||||
// Temporary fix to support adding default permissions AfterCreate
|
||||
Permissions DefaultPermissionSetter
|
||||
}
|
||||
|
||||
// Storage implements storage.Interface and storage resources as JSON files on disk.
|
||||
@ -164,15 +169,23 @@ func (s *Storage) convertToObject(data []byte, obj runtime.Object) (runtime.Obje
|
||||
// set to the read value from database.
|
||||
func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, out runtime.Object, ttl uint64) error {
|
||||
var err error
|
||||
var permissions string
|
||||
req := &resource.CreateRequest{}
|
||||
req.Value, err = s.prepareObjectForStorage(ctx, obj)
|
||||
req.Value, permissions, err = s.prepareObjectForStorage(ctx, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Key, err = s.getKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
grantPermissions, err := afterCreatePermissionCreator(ctx, req.Key, permissions, obj, s.opts.Permissions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rsp, err := s.store.Create(ctx, req)
|
||||
if err != nil {
|
||||
return resource.GetError(resource.AsErrorResult(err))
|
||||
@ -203,6 +216,11 @@ func (s *Storage) Create(ctx context.Context, key string, obj runtime.Object, ou
|
||||
})
|
||||
}
|
||||
|
||||
// Synchronous AfterCreate permissions -- allows users to become "admin" of the thing they made
|
||||
if grantPermissions != nil {
|
||||
return grantPermissions(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -376,6 +376,10 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *Resour
|
||||
}
|
||||
}
|
||||
|
||||
if obj.GetAnnotation(utils.AnnoKeyGrantPermissions) != "" {
|
||||
return nil, NewBadRequestError("can not save annotation: " + utils.AnnoKeyGrantPermissions)
|
||||
}
|
||||
|
||||
check := claims.CheckRequest{
|
||||
Verb: utils.VerbCreate,
|
||||
Group: key.Group,
|
||||
|
@ -48,10 +48,11 @@ type TestContext struct {
|
||||
// TestIntegrationValidation tests the dashboard K8s API
|
||||
func TestIntegrationValidation(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
t.Skip("skipping integration test2")
|
||||
}
|
||||
|
||||
dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5}
|
||||
// TODO: Skip mode3 - borken due to race conditions while setting default permissions across storage backends
|
||||
dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode4, rest.Mode5}
|
||||
for _, dualWriterMode := range dualWriterModes {
|
||||
t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) {
|
||||
// Create a K8sTestHelper which will set up a real API server
|
||||
@ -264,9 +265,6 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
|
||||
|
||||
// Test generation conflict when updating concurrently
|
||||
t.Run("reject update with version conflict", func(t *testing.T) {
|
||||
// Depends on https://github.com/grafana/grafana/pull/102527
|
||||
ctx.skipIfMode(t, "Default permissions are not set yet in unified storage", rest.Mode3, rest.Mode4, rest.Mode5)
|
||||
|
||||
// Create a dashboard with admin
|
||||
dash, err := createDashboard(t, adminClient, "Dashboard for Version Conflict Test", nil, nil)
|
||||
require.NoError(t, err, "Failed to create dashboard for version conflict test")
|
||||
@ -538,6 +536,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) {
|
||||
// skipIfMode skips the current test if running in any of the specified modes
|
||||
// Usage: skipIfMode(t, rest.Mode1, rest.Mode4)
|
||||
// or with a message: skipIfMode(t, "Known issue with conflict detection", rest.Mode1, rest.Mode4)
|
||||
// nolint:unused
|
||||
func (c *TestContext) skipIfMode(t *testing.T, args ...interface{}) {
|
||||
t.Helper()
|
||||
|
||||
@ -789,6 +788,9 @@ func createDashboardObject(t *testing.T, title string, folderUID string, generat
|
||||
"kind": dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind().Kind,
|
||||
"metadata": map[string]interface{}{
|
||||
"generateName": "test-",
|
||||
"annotations": map[string]interface{}{
|
||||
"grafana.app/grant-permissions": "default",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"title": title,
|
||||
|
@ -55,6 +55,9 @@ export const AnnoKeySourceTimestamp = 'grafana.app/sourceTimestamp';
|
||||
// for auditing... when saving from the UI, mark which version saved it from where
|
||||
export const AnnoKeySavedFromUI = 'grafana.app/saved-from-ui';
|
||||
|
||||
// Grant permissions to the created resource
|
||||
export const AnnoKeyGrantPermissions = 'grafana.app/grant-permissions';
|
||||
|
||||
/** @deprecated NOT A REAL annotation -- this is just a shim */
|
||||
export const AnnoKeySlug = 'grafana.app/slug';
|
||||
/** @deprecated NOT A REAL annotation -- this is just a shim */
|
||||
@ -100,7 +103,7 @@ type GrafanaClientAnnotations = {
|
||||
[AnnoKeySavedFromUI]?: string;
|
||||
[AnnoKeyDashboardIsSnapshot]?: boolean;
|
||||
[AnnoKeyDashboardSnapshotOriginalUrl]?: string;
|
||||
|
||||
[AnnoKeyGrantPermissions]?: string;
|
||||
// TODO: This should be provided by the API
|
||||
// This is the dashboard ID for the Gcom API. This set when a dashboard is created through importing a dashboard from Grafana.com.
|
||||
[AnnoKeyDashboardGnetId]?: string;
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
ResourceForCreate,
|
||||
AnnoKeyMessage,
|
||||
AnnoKeyFolder,
|
||||
AnnoKeyGrantPermissions,
|
||||
Resource,
|
||||
DeprecatedInternalId,
|
||||
} from 'app/features/apiserver/types';
|
||||
@ -64,6 +65,10 @@ export class K8sDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard> {
|
||||
obj.metadata.name = dashboard.uid;
|
||||
return this.client.update(obj, { fieldValidation: 'Ignore' }).then((v) => this.asSaveDashboardResponseDTO(v));
|
||||
}
|
||||
obj.metadata.annotations = {
|
||||
...obj.metadata.annotations,
|
||||
[AnnoKeyGrantPermissions]: 'default',
|
||||
};
|
||||
return this.client.create(obj, { fieldValidation: 'Ignore' }).then((v) => this.asSaveDashboardResponseDTO(v));
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
AnnoKeyFolderTitle,
|
||||
AnnoKeyFolderUrl,
|
||||
AnnoKeyMessage,
|
||||
AnnoKeyGrantPermissions,
|
||||
DeprecatedInternalId,
|
||||
Resource,
|
||||
ResourceClient,
|
||||
@ -130,6 +131,10 @@ export class K8sDashboardV2API
|
||||
delete obj.metadata.resourceVersion;
|
||||
return this.client.update(obj).then((v) => this.asSaveDashboardResponseDTO(v));
|
||||
}
|
||||
obj.metadata.annotations = {
|
||||
...obj.metadata.annotations,
|
||||
[AnnoKeyGrantPermissions]: 'default',
|
||||
};
|
||||
return await this.client.create(obj).then((v) => this.asSaveDashboardResponseDTO(v));
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user