diff --git a/go.mod b/go.mod index bac8f15dacd..e5f070f04d1 100644 --- a/go.mod +++ b/go.mod @@ -518,7 +518,7 @@ require ( github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect diff --git a/go.sum b/go.sum index 2d2c164f510..45bec762338 100644 --- a/go.sum +++ b/go.sum @@ -2108,8 +2108,8 @@ github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/openfga/api/proto v0.0.0-20250127102726-f9709139a369 h1:wEsCZ4oBuu8LfEJ3VXbveXO8uEhCthrxA40WSvxO044= github.com/openfga/api/proto v0.0.0-20250127102726-f9709139a369/go.mod h1:m74TNgnAAIJ03gfHcx+xaRWnr+IbQy3y/AVNwwCFrC0= github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250220223040-ed0cfba54336 h1:pYuYanFfgYrvDoSu/nnThT9P60mw5Yx7PMEI7FYychM= diff --git a/go.work.sum b/go.work.sum index 66413aede3c..9702910b972 100644 --- a/go.work.sum +++ b/go.work.sum @@ -629,6 +629,8 @@ github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhi github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atc0005/go-teams-notify/v2 v2.13.0 h1:nbDeHy89NjYlF/PEfLVF6lsserY9O5SnN1iOIw3AxXw= github.com/atc0005/go-teams-notify/v2 v2.13.0/go.mod h1:WSv9moolRsBcpZbwEf6gZxj7h0uJlJskJq5zkEWKO8Y= github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI= @@ -639,6 +641,8 @@ github.com/aws/aws-sdk-go-v2/service/kinesis v1.33.0 h1:JPXkrQk5OS/+Q81fKH97Ll/V github.com/aws/aws-sdk-go-v2/service/kinesis v1.33.0/go.mod h1:dJngkoVMrq0K7QvRkdRZYM4NUp6cdWa2GBdpm8zoY8U= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3 h1:UPTdlTOwWUX49fVi7cymEN6hDqCwe3LNv1vi7TXUutk= github.com/aws/aws-sdk-go-v2/service/kms v1.35.3/go.mod h1:gjDP16zn+WWalyaUqwCCioQ8gU8lzttCCc9jYsiQI/8= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1 h1:tecq7+mAav5byF+Mr+iONJnCBf4B4gon8RSp4BrweSc= +github.com/aws/aws-sdk-go-v2/service/kms v1.38.1/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4 h1:NgRFYyFpiMD62y4VPXh4DosPFbZd4vdMVBWKk0VmWXc= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.4/go.mod h1:TKKN7IQoM7uTnyuFm9bm9cw5P//ZYTl4m3htBWQ1G/c= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2 h1:vlYXbindmagyVA3RS2SPd47eKZ00GZZQcr+etTviHtc= @@ -1191,6 +1195,8 @@ github.com/mithrandie/readline-csvq v1.3.0 h1:VTJEOGouJ8j27jJCD4kBBbNTxM0OdBvE1a github.com/mithrandie/readline-csvq v1.3.0/go.mod h1:FKyYqDgf/G4SNov7SMFXRWO6LQLXIOeTog/NB97FZl0= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= @@ -1203,6 +1209,7 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8 github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mostynb/go-grpc-compression v1.2.3 h1:42/BKWMy0KEJGSdWvzqIyOZ95YcR9mLPqKctH7Uo//I= github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1Vjs47Km/Y2FE6ouQ7Lg= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= diff --git a/pkg/apimachinery/utils/folder_key.go b/pkg/apimachinery/utils/folder_key.go new file mode 100644 index 00000000000..a7f6d4cd03e --- /dev/null +++ b/pkg/apimachinery/utils/folder_key.go @@ -0,0 +1,20 @@ +package utils + +import ( + "context" +) + +type folderRemovePermissions string + +const removePermissionKey = folderRemovePermissions("RemovePermission") + +func SetFolderRemovePermissions(ctx context.Context, value bool) context.Context { + return context.WithValue(ctx, removePermissionKey, value) +} + +func GetFolderRemovePermissions(ctx context.Context, defaultVal bool) bool { + if val, ok := ctx.Value(removePermissionKey).(bool); ok { + return val + } + return defaultVal +} diff --git a/pkg/build/go.mod b/pkg/build/go.mod index 909811de4a1..3012a2bab49 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -62,7 +62,7 @@ require ( github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/pkg/build/go.sum b/pkg/build/go.sum index d49747c61fb..abb46b91775 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -207,8 +207,8 @@ github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go index 89053e6b932..f6bdc211f15 100644 --- a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go +++ b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go @@ -81,6 +81,7 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err provisioning, nil, // no librarypanels.Service sort.ProvideService(), + nil, // we don't delete during migration, and this is only need to delete permission. acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featureToggles, ) diff --git a/pkg/extensions/enterprise_imports.go b/pkg/extensions/enterprise_imports.go index 3585c68ebf3..d33ccf561aa 100644 --- a/pkg/extensions/enterprise_imports.go +++ b/pkg/extensions/enterprise_imports.go @@ -27,6 +27,7 @@ import ( _ "github.com/robfig/cron/v3" _ "github.com/russellhaering/goxmldsig" _ "github.com/spf13/cobra" // used by the standalone apiserver cli + _ "github.com/spyzhov/ajson" _ "github.com/stretchr/testify/require" _ "gocloud.dev/secrets/awskms" _ "gocloud.dev/secrets/azurekeyvault" @@ -52,5 +53,4 @@ import ( _ "github.com/grafana/e2e" _ "github.com/grafana/gofpdf" _ "github.com/grafana/gomemcache/memcache" - _ "github.com/spyzhov/ajson" ) diff --git a/pkg/registry/apis/dashboard/dashboard_storage.go b/pkg/registry/apis/dashboard/dashboard_storage.go new file mode 100644 index 00000000000..d2d54c8d815 --- /dev/null +++ b/pkg/registry/apis/dashboard/dashboard_storage.go @@ -0,0 +1,35 @@ +package dashboard + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" +) + +// dashboardStoragePermissionWrapper is a wrapper around the grafanarest.Storage that adds dashboard permissions handling +// when dual writing is enabled. +type dashboardStoragePermissionWrapper struct { + dashboardPermissionsSvc accesscontrol.DashboardPermissionsService + grafanarest.Storage +} + +func (d dashboardStoragePermissionWrapper) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + obj, async, err := d.Storage.Delete(ctx, name, deleteValidation, options) + if err != nil { + return obj, async, err + } + if accessErr := d.dashboardPermissionsSvc.DeleteResourcePermissions(ctx, info.OrgID, name); accessErr != nil { + return obj, async, accessErr + } + return obj, async, nil +} diff --git a/pkg/registry/apis/dashboard/legacy/migrate.go b/pkg/registry/apis/dashboard/legacy/migrate.go index 73d01caaade..bdc47bf985a 100644 --- a/pkg/registry/apis/dashboard/legacy/migrate.go +++ b/pkg/registry/apis/dashboard/legacy/migrate.go @@ -11,6 +11,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" authlib "github.com/grafana/authlib/types" + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -51,11 +52,12 @@ func ProvideLegacyMigrator( sql db.DB, // direct access to tables provisioning provisioning.ProvisioningService, // only needed for dashboard settings libraryPanelSvc librarypanels.Service, + dashboardPermissionSvc accesscontrol.DashboardPermissionsService, accessControl accesscontrol.AccessControl, features featuremgmt.FeatureToggles, ) LegacyMigrator { dbp := legacysql.NewDatabaseProvider(sql) - return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService(), accessControl, features) + return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, libraryPanelSvc, sort.ProvideService(), dashboardPermissionSvc, accessControl, features) } type BlobStoreInfo struct { diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index f019a39a799..ae5e54393e3 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -15,6 +15,7 @@ import ( "k8s.io/utils/ptr" claims "github.com/grafana/authlib/types" + dashboardOG "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" @@ -66,8 +67,9 @@ type dashboardSqlAccess struct { invalidDashboardParseFallbackEnabled bool // Use for writing (not reading) - dashStore dashboards.Store - dashboardSearchClient legacysearcher.DashboardSearchClient + dashStore dashboards.Store + dashboardSearchClient legacysearcher.DashboardSearchClient + dashboardPermissionSvc accesscontrol.DashboardPermissionsService accessControl accesscontrol.AccessControl libraryPanelSvc librarypanels.Service @@ -84,6 +86,7 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, provisioning provisioning.ProvisioningService, libraryPanelSvc librarypanels.Service, sorter sort.Service, + dashboardPermissionSvc accesscontrol.DashboardPermissionsService, accessControl accesscontrol.AccessControl, features featuremgmt.FeatureToggles, ) DashboardAccess { @@ -94,6 +97,7 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider, dashStore: dashStore, provisioning: provisioning, dashboardSearchClient: *dashboardSearchClient, + dashboardPermissionSvc: dashboardPermissionSvc, libraryPanelSvc: libraryPanelSvc, accessControl: accessControl, log: log.New("dashboard.legacysql"), diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index f2ed3ee138c..3014893ea71 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -20,6 +20,7 @@ import ( "k8s.io/kube-openapi/pkg/validation/spec" claims "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" @@ -82,6 +83,7 @@ type DashboardsAPIBuilder struct { unified resource.ResourceClient dashboardProvisioningService dashboards.DashboardProvisioningService dashboardPermissions dashboards.PermissionsRegistrationService + dashboardPermissionsSvc accesscontrol.DashboardPermissionsService scheme *runtime.Scheme search *SearchHandler dashStore dashboards.Store @@ -105,6 +107,7 @@ func RegisterAPIService( pluginStore pluginstore.Store, datasourceService datasources.DataSourceService, dashboardPermissions dashboards.PermissionsRegistrationService, + dashboardPermissionsSvc accesscontrol.DashboardPermissionsService, accessControl accesscontrol.AccessControl, accessClient claims.AccessClient, provisioning provisioning.ProvisioningService, @@ -130,6 +133,7 @@ func RegisterAPIService( dashboardService: dashboardService, dashboardPermissions: dashboardPermissions, + dashboardPermissionsSvc: dashboardPermissionsSvc, features: features, accessControl: accessControl, accessClient: accessClient, @@ -145,7 +149,7 @@ func RegisterAPIService( folderClient: folderClient, legacy: &DashboardStorage{ - Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, accessControl, features), + Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features), DashboardService: dashboardService, }, reg: reg, @@ -530,10 +534,14 @@ func (b *DashboardsAPIBuilder) storageForVersion( } gr := dashboards.GroupResource() - storage[dashboards.StoragePath()], err = opts.DualWriteBuilder(gr, legacyStore, store) + dw, err := opts.DualWriteBuilder(gr, legacyStore, store) if err != nil { return err } + storage[dashboards.StoragePath()] = dashboardStoragePermissionWrapper{ + dashboardPermissionsSvc: b.dashboardPermissionsSvc, + Storage: dw, + } // Register the DTO endpoint that will consolidate all dashboard bits storage[dashboards.StoragePath("dto")], err = NewDTOConnector( diff --git a/pkg/registry/apis/folders/folder_storage.go b/pkg/registry/apis/folders/folder_storage.go index b2084051824..3e544d46cb8 100644 --- a/pkg/registry/apis/folders/folder_storage.go +++ b/pkg/registry/apis/folders/folder_storage.go @@ -10,11 +10,13 @@ import ( "k8s.io/apiserver/pkg/registry/rest" claims "github.com/grafana/authlib/types" + folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/pkg/api/apierrors" "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/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" @@ -127,7 +129,23 @@ func (s *folderStorage) Update(ctx context.Context, // GracefulDeleter func (s *folderStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - return s.store.Delete(ctx, name, deleteValidation, options) + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, false, err + } + + obj, async, err := s.store.Delete(ctx, name, deleteValidation, options) + if err != nil { + return obj, async, err + } + + if accessErr := s.folderPermissionsSvc.DeleteResourcePermissions(ctx, info.OrgID, name); accessErr != nil { + // TODO: add a proper logger to this struct. + logger := log.New().FromContext(ctx) + logger.Warn("failed to delete folder permission after successfully deleting folder resource", "folder", name, "error", accessErr) + } + + return obj, async, err } // GracefulDeleter diff --git a/pkg/registry/apis/folders/legacy_storage.go b/pkg/registry/apis/folders/legacy_storage.go index d8ebda1fd65..4ae4a94a7f5 100644 --- a/pkg/registry/apis/folders/legacy_storage.go +++ b/pkg/registry/apis/folders/legacy_storage.go @@ -315,13 +315,15 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio if !ok { return v, false, fmt.Errorf("expected a folder response from Get") } + err = s.service.DeleteLegacy(ctx, &folder.DeleteFolderCommand{ UID: name, OrgID: info.OrgID, SignedInUser: user, // This would cascade delete into alert rules - ForceDeleteRules: false, + ForceDeleteRules: false, + RemovePermissions: utils.GetFolderRemovePermissions(ctx, true), }) return p, true, err // true is instant delete } diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 9779e16a121..11a06b02dbb 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -785,7 +785,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, pluginstoreService, service15, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, pluginstoreService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) featureFlagAPIBuilder := featuretoggle.RegisterAPIService(featureManager, accessControl, apiserverService, cfg, registerer) dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer) @@ -805,7 +805,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) factory := github.ProvideFactory() - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl, featureToggles) + legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer) decryptStorage, err := metadata.ProvideDecryptStorage(tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer) if err != nil { @@ -1364,7 +1364,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, pluginstoreService, service15, dashboardServiceImpl, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, pluginstoreService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, dashboardFolderStoreImpl, libraryPanelService, eventualRestConfigProvider, userService) snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) featureFlagAPIBuilder := featuretoggle.RegisterAPIService(featureManager, accessControl, apiserverService, cfg, registerer) dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, accessControl, registerer) @@ -1384,7 +1384,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } userStorageAPIBuilder := userstorage.RegisterAPIService(featureToggles, apiserverService, registerer) factory := github.ProvideFactory() - legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, accessControl, featureToggles) + legacyMigrator := legacy.ProvideLegacyMigrator(sqlStore, provisioningServiceImpl, libraryPanelService, dashboardPermissionsService, accessControl, featureToggles) decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer) decryptStorage, err := metadata.ProvideDecryptStorage(tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer) if err != nil { diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index a8c7419921d..b6bfc346f51 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -589,14 +589,20 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand, return err } - // remove all access control permission with folder scope - err := d.deleteResourcePermissions(sess, dashboard.OrgID, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.UID)) - if err != nil { - return err + // While migrating to unified storage, we might execute commands in both stores, so we delete the permissions + // only when the command is executed on both stores, thus we can skip it here. + if cmd.RemovePermissions { + if err := d.deleteResourcePermissions(sess, dashboard.OrgID, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.UID)); err != nil { + return err + } } } else { - if err := d.deleteResourcePermissions(sess, dashboard.OrgID, ac.GetResourceScopeUID("dashboards", dashboard.UID)); err != nil { - return err + // While migrating to unified storage, we might execute commands in both stores, so we delete the permissions + // only when the command is executed on both stores, thus we can skip it here. + if cmd.RemovePermissions { + if err := d.deleteResourcePermissions(sess, dashboard.OrgID, ac.GetResourceScopeUID("dashboards", dashboard.UID)); err != nil { + return err + } } } diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 9dbe4696d1b..f48fafc6a81 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -233,6 +233,7 @@ type DeleteDashboardCommand struct { UID string OrgID int64 ForceDeleteFolderRules bool + RemovePermissions bool } type DeleteOrphanedProvisionedDashboardsCommand struct { diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 62fc24bbd86..3cf952ebd64 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -996,7 +996,7 @@ func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderComm // Delete all dashboards in the folders for _, folderUID := range folderUIDs { // nolint:staticcheck - deleteCmd := dashboards.DeleteDashboardCommand{OrgID: cmd.OrgID, UID: folderUID, ForceDeleteFolderRules: cmd.ForceDeleteRules} + deleteCmd := dashboards.DeleteDashboardCommand{OrgID: cmd.OrgID, UID: folderUID, ForceDeleteFolderRules: cmd.ForceDeleteRules, RemovePermissions: cmd.RemovePermissions} if err := s.dashboardStore.DeleteDashboard(ctx, &deleteCmd); err != nil { return toFolderError(err) } diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index b19cf1ddae0..3d09d372896 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -170,7 +170,8 @@ type DeleteFolderCommand struct { OrgID int64 `json:"orgId" xorm:"org_id"` ForceDeleteRules bool `json:"forceDeleteRules"` - SignedInUser identity.Requester `json:"-"` + SignedInUser identity.Requester `json:"-"` + RemovePermissions bool `json:"-"` } // GetFolderQuery is used for all folder Get requests. Only one of UID, ID, or diff --git a/pkg/storage/legacysql/dualwrite/dualwriter.go b/pkg/storage/legacysql/dualwrite/dualwriter.go index afb97bb5432..7907db3af65 100644 --- a/pkg/storage/legacysql/dualwrite/dualwriter.go +++ b/pkg/storage/legacysql/dualwrite/dualwriter.go @@ -277,10 +277,20 @@ func (d *dualWriter) Delete(ctx context.Context, name string, deleteValidation r // we want to delete from legacy first, otherwise if the delete from unistore was successful, // but legacy failed, the user would get a failure, but not be able to retry the delete // as they would not be able to see the object in unistore anymore. + + // By setting RemovePermissions to false in the context, we will skip the deletion of permissions + // in the legacy store. This is needed as otherwise the permissions would be missing when executing + // the delete operation in the unified storage store. + ctx = utils.SetFolderRemovePermissions(ctx, false) + objFromLegacy, asyncLegacy, err := d.legacy.Delete(ctx, name, deleteValidation, options) if err != nil && (!d.readUnified || !d.errorIsOK && !apierrors.IsNotFound(err)) { return nil, false, err } + + // We can now flip it again. + ctx = utils.SetFolderRemovePermissions(ctx, true) + // If unified storage is our primary store, just delete it and return if d.readUnified { objFromStorage, asyncStorage, err := d.unified.Delete(ctx, name, deleteValidation, options)