RBAC: Remove action set feature toggle (#101959)

* remove action set feature toggle

* don't pass feature toggles to action set service instantiation

* linting

* test fixes and frontend clean-up

* fix test
This commit is contained in:
Ieva
2025-03-13 15:18:23 +00:00
committed by GitHub
parent 6d61196e55
commit 9264431c81
22 changed files with 184 additions and 266 deletions
@@ -21,67 +21,66 @@ For more information about feature release stages, refer to [Release life cycle
Most [generally available](https://grafana.com/docs/release-life-cycle/#general-availability) features are enabled by default. You can disable these feature by setting the feature flag to "false" in the configuration.
| Feature toggle name | Description | Enabled by default |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ |
| `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | |
| `publicDashboardsScene` | Enables public dashboard rendering using scenes | Yes |
| `featureHighlights` | Highlight Grafana Enterprise features | |
| `correlations` | Correlations page | Yes |
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
| `nestedFolders` | Enable folder nesting | Yes |
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes |
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes |
| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes |
| `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes |
| `unifiedRequestLog` | Writes error logs to the request logger | Yes |
| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes |
| `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes |
| `transformationsRedesign` | Enables the transformations redesign | Yes |
| `traceQLStreaming` | Enables response streaming of TraceQL queries of the Tempo data source | |
| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes |
| `alertingNoDataErrorExecution` | Changes how Alerting state manager handles execution of NoData/Error | Yes |
| `angularDeprecationUI` | Display Angular warnings in dashboards and panels | Yes |
| `dashgpt` | Enable AI powered features in dashboards | Yes |
| `alertingInsights` | Show the new alerting insights landing page | Yes |
| `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes |
| `formatString` | Enable format string transformer | Yes |
| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s | Yes |
| `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | Yes |
| `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server | Yes |
| `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | Yes |
| `annotationPermissionUpdate` | Change the way annotation permissions work by scoping them to folders and dashboards. | Yes |
| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes |
| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes |
| `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes |
| `ssoSettingsApi` | Enables the SSO settings API and the OAuth configuration UIs in Grafana | Yes |
| `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes |
| `exploreMetrics` | Enables the new Grafana Metrics Drilldown core app | Yes |
| `alertingSimplifiedRouting` | Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule | Yes |
| `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes |
| `lokiQueryHints` | Enables query hints for Loki | Yes |
| `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | |
| `groupToNestedTableTransformation` | Enables the group to nested table transformation | Yes |
| `newPDFRendering` | New implementation for the dashboard-to-PDF rendering | Yes |
| `tlsMemcached` | Use TLS-enabled memcached in the enterprise caching feature | Yes |
| `ssoSettingsSAML` | Use the new SSO Settings API to configure the SAML connector | Yes |
| `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes |
| `accessActionSets` | Introduces action sets for resource permissions. Also ensures that all folder editors and admins can create subfolders without needing any additional permissions. | Yes |
| `newDashboardSharingComponent` | Enables the new sharing drawer design | Yes |
| `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | |
| `pinNavItems` | Enables pinning of nav items | Yes |
| `alertingApiServer` | Register Alerting APIs with the K8s API server | Yes |
| `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes |
| `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | Yes |
| `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions | Yes |
| `useSessionStorageForRedirection` | Use session storage for handling the redirection after login | Yes |
| `userStorageAPI` | Enables the user storage API | Yes |
| `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | |
| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes |
| `reportingUseRawTimeRange` | Uses the original report or dashboard time range instead of making an absolute transformation | Yes |
| `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes |
| `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes |
| `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes |
| `lokiLabelNamesQueryApi` | Defaults to using the Loki `/labels` API instead of `/series` | Yes |
| Feature toggle name | Description | Enabled by default |
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | |
| `publicDashboardsScene` | Enables public dashboard rendering using scenes | Yes |
| `featureHighlights` | Highlight Grafana Enterprise features | |
| `correlations` | Correlations page | Yes |
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
| `nestedFolders` | Enable folder nesting | Yes |
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes |
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals | Yes |
| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes |
| `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes |
| `unifiedRequestLog` | Writes error logs to the request logger | Yes |
| `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes |
| `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes |
| `transformationsRedesign` | Enables the transformations redesign | Yes |
| `traceQLStreaming` | Enables response streaming of TraceQL queries of the Tempo data source | |
| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes |
| `alertingNoDataErrorExecution` | Changes how Alerting state manager handles execution of NoData/Error | Yes |
| `angularDeprecationUI` | Display Angular warnings in dashboards and panels | Yes |
| `dashgpt` | Enable AI powered features in dashboards | Yes |
| `alertingInsights` | Show the new alerting insights landing page | Yes |
| `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes |
| `formatString` | Enable format string transformer | Yes |
| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s | Yes |
| `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | Yes |
| `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server | Yes |
| `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | Yes |
| `annotationPermissionUpdate` | Change the way annotation permissions work by scoping them to folders and dashboards. | Yes |
| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes |
| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes |
| `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes |
| `ssoSettingsApi` | Enables the SSO settings API and the OAuth configuration UIs in Grafana | Yes |
| `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes |
| `exploreMetrics` | Enables the new Grafana Metrics Drilldown core app | Yes |
| `alertingSimplifiedRouting` | Enables users to easily configure alert notifications by specifying a contact point directly when editing or creating an alert rule | Yes |
| `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | Yes |
| `lokiQueryHints` | Enables query hints for Loki | Yes |
| `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | |
| `groupToNestedTableTransformation` | Enables the group to nested table transformation | Yes |
| `newPDFRendering` | New implementation for the dashboard-to-PDF rendering | Yes |
| `tlsMemcached` | Use TLS-enabled memcached in the enterprise caching feature | Yes |
| `ssoSettingsSAML` | Use the new SSO Settings API to configure the SAML connector | Yes |
| `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes |
| `newDashboardSharingComponent` | Enables the new sharing drawer design | Yes |
| `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | |
| `pinNavItems` | Enables pinning of nav items | Yes |
| `alertingApiServer` | Register Alerting APIs with the K8s API server | Yes |
| `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes |
| `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | Yes |
| `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions | Yes |
| `useSessionStorageForRedirection` | Use session storage for handling the redirection after login | Yes |
| `userStorageAPI` | Enables the user storage API | Yes |
| `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | |
| `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes |
| `reportingUseRawTimeRange` | Uses the original report or dashboard time range instead of making an absolute transformation | Yes |
| `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | Yes |
| `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes |
| `alertingNotificationsStepMode` | Enables simplified step mode in the notifications section | Yes |
| `lokiLabelNamesQueryApi` | Defaults to using the Loki `/labels` API instead of `/series` | Yes |
## Public preview feature toggles
@@ -163,7 +163,6 @@ export interface FeatureToggles {
oauthRequireSubClaim?: boolean;
newDashboardWithFiltersAndGroupBy?: boolean;
cloudWatchNewLabelParsing?: boolean;
accessActionSets?: boolean;
disableNumericMetricsSortingInExpressions?: boolean;
grafanaManagedRecordingRules?: boolean;
queryLibrary?: boolean;
+1 -1
View File
@@ -460,7 +460,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
cfg := setting.NewCfg()
actionSets := resourcepermissions.NewActionSetService(features)
actionSets := resourcepermissions.NewActionSetService()
fStore := folderimpl.ProvideStore(sc.db)
folderServiceWithFlagOn := folderimpl.ProvideService(
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
@@ -65,7 +65,7 @@ func TestAccessControl_Evaluate(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets))
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
if tt.scopeResolver != nil {
ac.RegisterScopeAttributeResolver(tt.resolverPrefix, tt.scopeResolver)
+13 -25
View File
@@ -166,10 +166,8 @@ func (s *Service) getUserPermissions(ctx context.Context, user identity.Requeste
if err != nil {
return nil, err
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
dbPermissions = s.actionResolver.ExpandActionSets(dbPermissions)
}
dbPermissions = s.actionResolver.ExpandActionSets(dbPermissions)
return append(permissions, dbPermissions...), nil
}
@@ -188,10 +186,8 @@ func (s *Service) getBasicRolePermissions(ctx context.Context, role string, orgI
OrgID: orgID,
RolePrefixes: OSSRolesPrefixes,
})
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
dbPermissions = s.actionResolver.ExpandActionSets(dbPermissions)
}
dbPermissions = s.actionResolver.ExpandActionSets(dbPermissions)
return append(permissions, dbPermissions...), err
}
@@ -205,10 +201,8 @@ func (s *Service) getTeamsPermissions(ctx context.Context, teamIDs []int64, orgI
RolePrefixes: OSSRolesPrefixes,
})
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
for teamID, permissions := range teamPermissions {
teamPermissions[teamID] = s.actionResolver.ExpandActionSets(permissions)
}
for teamID, permissions := range teamPermissions {
teamPermissions[teamID] = s.actionResolver.ExpandActionSets(permissions)
}
return teamPermissions, err
@@ -237,9 +231,7 @@ func (s *Service) getUserDirectPermissions(ctx context.Context, user identity.Re
return nil, err
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
permissions = s.actionResolver.ExpandActionSets(permissions)
}
permissions = s.actionResolver.ExpandActionSets(permissions)
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
permissions = append(permissions, SharedWithMeFolderPermission)
}
@@ -545,11 +537,9 @@ func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Reque
return nil, err
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
options.ActionSets = s.actionResolver.ResolveAction(options.Action)
options.ActionSets = append(options.ActionSets,
s.actionResolver.ResolveActionPrefix(options.ActionPrefix)...)
}
options.ActionSets = s.actionResolver.ResolveAction(options.Action)
options.ActionSets = append(options.ActionSets,
s.actionResolver.ResolveActionPrefix(options.ActionPrefix)...)
// Get managed permissions (DB)
usersPermissions, err := s.store.SearchUsersPermissions(ctx, usr.GetOrgID(), options)
@@ -610,7 +600,7 @@ func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Reque
}
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) && len(options.ActionSets) > 0 {
if len(options.ActionSets) > 0 {
for id, perms := range res {
res[id] = s.actionResolver.ExpandActionSetsWithFilter(perms, GetActionFilter(options))
}
@@ -661,11 +651,9 @@ func (s *Service) searchUserPermissions(ctx context.Context, orgID int64, search
}
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
searchOptions.ActionSets = s.actionResolver.ResolveAction(searchOptions.Action)
searchOptions.ActionSets = append(searchOptions.ActionSets,
s.actionResolver.ResolveActionPrefix(searchOptions.ActionPrefix)...)
}
searchOptions.ActionSets = s.actionResolver.ResolveAction(searchOptions.Action)
searchOptions.ActionSets = append(searchOptions.ActionSets,
s.actionResolver.ResolveActionPrefix(searchOptions.ActionPrefix)...)
// Get permissions from the DB
dbPermissions, err := s.store.SearchUsersPermissions(ctx, orgID, searchOptions)
@@ -674,7 +662,7 @@ func (s *Service) searchUserPermissions(ctx context.Context, orgID int64, search
}
permissions = append(permissions, dbPermissions[searchOptions.UserID]...)
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) && len(searchOptions.ActionSets) != 0 {
if len(searchOptions.ActionSets) != 0 {
permissions = s.actionResolver.ExpandActionSetsWithFilter(permissions, GetActionFilter(searchOptions))
}
@@ -35,14 +35,15 @@ func setupTestEnv(t testing.TB) *Service {
cfg := setting.NewCfg()
ac := &Service{
cache: localcache.ProvideService(),
cfg: cfg,
features: featuremgmt.WithFeatures(),
log: log.New("accesscontrol"),
registrations: accesscontrol.RegistrationList{},
roles: accesscontrol.BuildBasicRoleDefinitions(),
store: database.ProvideService(db.InitTestDB(t)),
permRegistry: permreg.ProvidePermissionRegistry(),
cache: localcache.ProvideService(),
cfg: cfg,
features: featuremgmt.WithFeatures(),
log: log.New("accesscontrol"),
registrations: accesscontrol.RegistrationList{},
roles: accesscontrol.BuildBasicRoleDefinitions(),
store: database.ProvideService(db.InitTestDB(t)),
permRegistry: permreg.ProvidePermissionRegistry(),
actionResolver: resourcepermissions.NewActionSetService(),
}
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
return ac
@@ -805,8 +806,7 @@ func TestService_SearchUserPermissions(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ac := setupTestEnv(t)
if tt.withActionSets {
ac.features = featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets)
actionSetSvc := resourcepermissions.NewActionSetService(ac.features)
actionSetSvc := resourcepermissions.NewActionSetService()
for set, actions := range tt.actionSets {
actionSetName := resourcepermissions.GetActionSetName(strings.Split(set, ":")[0], strings.Split(set, ":")[1])
actionSetSvc.StoreActionSet(actionSetName, actions)
@@ -32,7 +32,7 @@ func ProvideFolderPermissions(
cfg *setting.Cfg,
sqlStore *sqlstore.SQLStore,
) (*ossaccesscontrol.FolderPermissionsService, error) {
actionSets := resourcepermissions.NewActionSetService(features)
actionSets := resourcepermissions.NewActionSetService()
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
@@ -76,9 +76,7 @@ func New(cfg *setting.Cfg,
for _, a := range actions {
actionSet[a] = struct{}{}
}
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
actionSetService.StoreActionSet(GetActionSetName(options.Resource, permission), actions)
}
actionSetService.StoreActionSet(GetActionSetName(options.Resource, permission), actions)
}
// Sort all permissions based on action length. Will be used when mapping between actions to permissions
@@ -151,13 +149,11 @@ func (s *Service) GetPermissions(ctx context.Context, user identity.Requester, r
}
actions := s.actions
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
for _, action := range s.actions {
actionSets := s.actionSetSvc.ResolveAction(action)
for _, actionSet := range actionSets {
if !slices.Contains(actions, actionSet) {
actions = append(actions, actionSet)
}
for _, action := range s.actions {
actionSets := s.actionSetSvc.ResolveAction(action)
for _, actionSet := range actionSets {
if !slices.Contains(actions, actionSet) {
actions = append(actions, actionSet)
}
}
}
@@ -176,33 +172,31 @@ func (s *Service) GetPermissions(ctx context.Context, user identity.Requester, r
return nil, err
}
if s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
for i := range resourcePermissions {
actions := resourcePermissions[i].Actions
var expandedActions []string
for _, action := range actions {
if isFolderOrDashboardAction(action) {
actionSetActions := s.actionSetSvc.ResolveActionSet(action)
if len(actionSetActions) > 0 {
// Add all actions for folder
if s.options.Resource == dashboards.ScopeFoldersRoot {
expandedActions = append(expandedActions, actionSetActions...)
continue
}
// This check is needed for resolving inherited permissions - we don't want to include
// actions that are not related to dashboards when expanding dashboard action sets
for _, actionSetAction := range actionSetActions {
if slices.Contains(s.actions, actionSetAction) {
expandedActions = append(expandedActions, actionSetAction)
}
}
for i := range resourcePermissions {
actions := resourcePermissions[i].Actions
var expandedActions []string
for _, action := range actions {
if isFolderOrDashboardAction(action) {
actionSetActions := s.actionSetSvc.ResolveActionSet(action)
if len(actionSetActions) > 0 {
// Add all actions for folder
if s.options.Resource == dashboards.ScopeFoldersRoot {
expandedActions = append(expandedActions, actionSetActions...)
continue
}
// This check is needed for resolving inherited permissions - we don't want to include
// actions that are not related to dashboards when expanding dashboard action sets
for _, actionSetAction := range actionSetActions {
if slices.Contains(s.actions, actionSetAction) {
expandedActions = append(expandedActions, actionSetAction)
}
}
continue
}
expandedActions = append(expandedActions, action)
}
resourcePermissions[i].Actions = expandedActions
expandedActions = append(expandedActions, action)
}
resourcePermissions[i].Actions = expandedActions
}
return resourcePermissions, nil
@@ -495,15 +489,13 @@ type ActionSetStore interface {
}
type ActionSetSvc struct {
features featuremgmt.FeatureToggles
store ActionSetStore
store ActionSetStore
}
// NewActionSetService returns a new instance of InMemoryActionSetService.
func NewActionSetService(features featuremgmt.FeatureToggles) ActionSetService {
func NewActionSetService() ActionSetService {
return &ActionSetSvc{
features: features,
store: NewInMemoryActionSetStore(features),
store: NewInMemoryActionSetStore(),
}
}
@@ -580,12 +572,9 @@ func (a *ActionSetSvc) ExpandActionSetsWithFilter(permissions []accesscontrol.Pe
// RegisterActionSets allow the caller to expand the existing action sets with additional permissions
// This is intended to be used by plugins, and currently supports extending folder and dashboard action sets
func (a *ActionSetSvc) RegisterActionSets(ctx context.Context, pluginID string, registrations []plugins.ActionSet) error {
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.RegisterActionSets")
_, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.RegisterActionSets")
defer span.End()
if !a.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
return nil
}
for _, reg := range registrations {
if err := pluginutils.ValidatePluginActionSet(pluginID, reg); err != nil {
return err
@@ -228,15 +228,13 @@ func TestService_SetPermissions(t *testing.T) {
func TestService_RegisterActionSets(t *testing.T) {
type registerActionSetsTest struct {
desc string
actionSetsEnabled bool
options Options
expectedActionSets []ActionSet
}
tests := []registerActionSetsTest{
{
desc: "should register folder action sets if action sets are enabled",
actionSetsEnabled: true,
desc: "should register folder action sets if action sets are enabled",
options: Options{
Resource: "folders",
PermissionsToActions: map[string][]string{
@@ -256,8 +254,7 @@ func TestService_RegisterActionSets(t *testing.T) {
},
},
{
desc: "should register dashboard action set if action sets are enabled",
actionSetsEnabled: true,
desc: "should register dashboard action set if action sets are enabled",
options: Options{
Resource: "dashboards",
PermissionsToActions: map[string][]string{
@@ -271,27 +268,13 @@ func TestService_RegisterActionSets(t *testing.T) {
},
},
},
{
desc: "should not register dashboard action set if action sets are not enabled",
actionSetsEnabled: false,
options: Options{
Resource: "dashboards",
PermissionsToActions: map[string][]string{
"View": {"dashboards:read"},
},
},
expectedActionSets: []ActionSet{},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
features := featuremgmt.WithFeatures()
if tt.actionSetsEnabled {
features = featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets)
}
ac := acimpl.ProvideAccessControl(features)
actionSets := NewActionSetService(features)
actionSets := NewActionSetService()
_, err := New(
setting.NewCfg(), tt.options, features, routing.NewRouteRegister(), licensingtest.NewFakeLicensing(),
ac, &actest.FakeService{}, db.InitTestDB(t), nil, nil, actionSets,
@@ -317,7 +300,6 @@ func TestService_RegisterActionSets(t *testing.T) {
func TestStore_RegisterActionSet(t *testing.T) {
type actionSetTest struct {
desc string
features featuremgmt.FeatureToggles
pluginID string
pluginActions []plugins.ActionSet
coreActionSets []ActionSet
@@ -327,8 +309,7 @@ func TestStore_RegisterActionSet(t *testing.T) {
tests := []actionSetTest{
{
desc: "should be able to register a plugin action set if the right feature toggles are enabled",
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
desc: "should be able to register a plugin action set",
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
@@ -343,21 +324,8 @@ func TestStore_RegisterActionSet(t *testing.T) {
},
},
},
{
desc: "should not register plugin action set if feature toggles are missing",
features: featuremgmt.WithFeatures(),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
Action: "folders:view",
Actions: []string{"test-app.resource:read"},
},
},
expectedActionSets: []ActionSet{},
},
{
desc: "should be able to register multiple plugin action sets",
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
@@ -382,7 +350,6 @@ func TestStore_RegisterActionSet(t *testing.T) {
},
{
desc: "action set actions should be added not replaced",
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
@@ -425,7 +392,6 @@ func TestStore_RegisterActionSet(t *testing.T) {
},
{
desc: "should not be able to register an action that doesn't have a plugin prefix",
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
@@ -441,7 +407,6 @@ func TestStore_RegisterActionSet(t *testing.T) {
},
{
desc: "should not be able to register action set that is not in the allow list",
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
@@ -454,7 +419,7 @@ func TestStore_RegisterActionSet(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
asService := NewActionSetService(tt.features)
asService := NewActionSetService()
err := asService.RegisterActionSets(context.Background(), tt.pluginID, tt.pluginActions)
if tt.expectedErr {
@@ -511,7 +476,7 @@ func setupTestEnvironment(t *testing.T, ops Options) (*Service, user.Service, te
ac := acimpl.ProvideAccessControl(features)
service, err := New(
cfg, ops, features, routing.NewRouteRegister(), license,
ac, acService, sql, teamSvc, userSvc, NewActionSetService(features),
ac, acService, sql, teamSvc, userSvc, NewActionSetService(),
)
require.NoError(t, err)
@@ -771,18 +771,16 @@ func managedPermission(action, resource string, resourceID, resourceAttribute st
// InMemoryActionSets is an in-memory implementation of the ActionSetStore.
type InMemoryActionSets struct {
features featuremgmt.FeatureToggles
log log.Logger
actionSetToActions map[string][]string
actionToActionSets map[string][]string
}
func NewInMemoryActionSetStore(features featuremgmt.FeatureToggles) *InMemoryActionSets {
func NewInMemoryActionSetStore() *InMemoryActionSets {
return &InMemoryActionSets{
actionSetToActions: make(map[string][]string),
actionToActionSets: make(map[string][]string),
log: log.New("resourcepermissions.actionsets"),
features: features,
}
}
@@ -781,7 +781,7 @@ func TestStore_StoreActionSet(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
asService := NewInMemoryActionSetStore(featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets))
asService := NewInMemoryActionSetStore()
asService.StoreActionSet(GetActionSetName(tt.resource, tt.action), tt.actions)
actionSetName := GetActionSetName(tt.resource, tt.action)
@@ -792,7 +792,7 @@ func TestStore_StoreActionSet(t *testing.T) {
}
func TestStore_ResolveActionSet(t *testing.T) {
actionSetService := NewActionSetService(featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets))
actionSetService := NewActionSetService()
actionSetService.StoreActionSet("folders:edit", []string{"folders:read", "folders:write", "dashboards:read", "dashboards:write"})
actionSetService.StoreActionSet("folders:view", []string{"folders:read", "dashboards:read"})
actionSetService.StoreActionSet("dashboards:view", []string{"dashboards:read"})
@@ -835,7 +835,7 @@ func TestStore_ResolveActionSet(t *testing.T) {
}
func TestStore_ExpandActions(t *testing.T) {
actionSetService := NewActionSetService(featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets))
actionSetService := NewActionSetService()
actionSetService.StoreActionSet("folders:edit", []string{"folders:read", "folders:write", "dashboards:read", "dashboards:write"})
actionSetService.StoreActionSet("folders:view", []string{"folders:read", "dashboards:read"})
actionSetService.StoreActionSet("dashboards:view", []string{"dashboards:read"})
-7
View File
@@ -1105,13 +1105,6 @@ var (
FrontendOnly: false,
AllowSelfServe: false,
},
{
Name: "accessActionSets",
Description: "Introduces action sets for resource permissions. Also ensures that all folder editors and admins can create subfolders without needing any additional permissions.",
Stage: FeatureStageGeneralAvailability,
Owner: identityAccessTeam,
Expression: "true", // enabled by default
},
{
Name: "disableNumericMetricsSortingInExpressions",
Description: "In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels.",
-1
View File
@@ -144,7 +144,6 @@ ssoSettingsSAML,GA,@grafana/identity-access-team,false,false,false
oauthRequireSubClaim,experimental,@grafana/identity-access-team,false,false,false
newDashboardWithFiltersAndGroupBy,experimental,@grafana/dashboards-squad,false,false,false
cloudWatchNewLabelParsing,GA,@grafana/aws-datasources,false,false,false
accessActionSets,GA,@grafana/identity-access-team,false,false,false
disableNumericMetricsSortingInExpressions,experimental,@grafana/oss-big-tent,false,true,false
grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,false
queryLibrary,experimental,@grafana/grafana-frontend-platform,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
144 oauthRequireSubClaim experimental @grafana/identity-access-team false false false
145 newDashboardWithFiltersAndGroupBy experimental @grafana/dashboards-squad false false false
146 cloudWatchNewLabelParsing GA @grafana/aws-datasources false false false
accessActionSets GA @grafana/identity-access-team false false false
147 disableNumericMetricsSortingInExpressions experimental @grafana/oss-big-tent false true false
148 grafanaManagedRecordingRules experimental @grafana/alerting-squad false false false
149 queryLibrary experimental @grafana/grafana-frontend-platform false false false
-4
View File
@@ -587,10 +587,6 @@ const (
// Updates CloudWatch label parsing to be more accurate
FlagCloudWatchNewLabelParsing = "cloudWatchNewLabelParsing"
// FlagAccessActionSets
// Introduces action sets for resource permissions. Also ensures that all folder editors and admins can create subfolders without needing any additional permissions.
FlagAccessActionSets = "accessActionSets"
// FlagDisableNumericMetricsSortingInExpressions
// In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels.
FlagDisableNumericMetricsSortingInExpressions = "disableNumericMetricsSortingInExpressions"
@@ -36,6 +36,7 @@
"name": "accessActionSets",
"resourceVersion": "1731413707429",
"creationTimestamp": "2024-04-12T16:19:25Z",
"deletionTimestamp": "2025-03-11T16:34:55Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-11-12 12:15:07.42916 +0000 UTC"
}
-6
View File
@@ -1248,12 +1248,6 @@ func (s *Service) canMove(ctx context.Context, cmd *folder.MoveFolderCommand) (b
var evaluators []accesscontrol.Evaluator
currentFolderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.UID)
for action, scopes := range permissions {
// Skip unexpanded action sets - they have no impact if action sets are not enabled
if !s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
if action == "folders:view" || action == "folders:edit" || action == "folders:admin" {
continue
}
}
for _, scope := range newFolderAndParentUIDs {
if slices.Contains(scopes, scope) {
evaluators = append(evaluators, accesscontrol.EvalPermission(action, currentFolderScope))
@@ -842,12 +842,6 @@ func (s *Service) canMoveViaApiServer(ctx context.Context, cmd *folder.MoveFolde
var evaluators []accesscontrol.Evaluator
currentFolderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.UID)
for action, scopes := range permissions {
// Skip unexpanded action sets - they have no impact if action sets are not enabled
if !s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
if action == "folders:view" || action == "folders:edit" || action == "folders:admin" {
continue
}
}
for _, scope := range newFolderAndParentUIDs {
if slices.Contains(scopes, scope) {
evaluators = append(evaluators, accesscontrol.EvalPermission(action, currentFolderScope))
+14 -33
View File
@@ -2,7 +2,6 @@ package permissions
import (
"bytes"
"context"
"fmt"
"slices"
"strings"
@@ -61,59 +60,41 @@ func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissi
var dashboardActionSets []string
if queryType == searchstore.TypeFolder {
folderAction = dashboards.ActionFoldersRead
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
}
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
if needEdit {
folderAction = dashboards.ActionDashboardsCreate
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:edit", "folders:admin"}
}
folderActionSets = []string{"folders:edit", "folders:admin"}
}
} else if queryType == searchstore.TypeDashboard {
dashboardAction = dashboards.ActionDashboardsRead
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
}
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
if needEdit {
dashboardAction = dashboards.ActionDashboardsWrite
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:edit", "dashboards:admin"}
}
folderActionSets = []string{"folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:edit", "dashboards:admin"}
}
} else if queryType == searchstore.TypeAlertFolder {
folderAction = accesscontrol.ActionAlertingRuleRead
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
}
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
if needEdit {
folderAction = accesscontrol.ActionAlertingRuleCreate
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:edit", "folders:admin"}
}
folderActionSets = []string{"folders:edit", "folders:admin"}
}
} else if queryType == searchstore.TypeAnnotation {
dashboardAction = accesscontrol.ActionAnnotationsRead
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
}
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
} else {
folderAction = dashboards.ActionFoldersRead
dashboardAction = dashboards.ActionDashboardsRead
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
}
folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
if needEdit {
folderAction = dashboards.ActionDashboardsCreate
dashboardAction = dashboards.ActionDashboardsWrite
if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) {
folderActionSets = []string{"folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:edit", "dashboards:admin"}
}
folderActionSets = []string{"folders:edit", "folders:admin"}
dashboardActionSets = []string{"dashboards:edit", "dashboards:admin"}
}
}
@@ -417,7 +417,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll},
},
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"dashboard under the root", "dashboard under parent folder", "dashboard under subfolder"},
},
{
@@ -459,7 +459,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), tc.permissions)}}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagAccessActionSets)...), featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@@ -616,20 +616,17 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
permission dashboardaccess.PermissionType
signedInUserPermissions []accesscontrol.Permission
expectedResult []string
features []any
}{
{
desc: "Should not list any dashboards if user has no permissions",
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: nil,
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: nil,
},
{
desc: "Should not list any folders if user has no permissions",
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: nil,
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: nil,
},
{
@@ -639,7 +636,6 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
},
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: []string{"parent", "subfolder"},
},
{
@@ -649,7 +645,6 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
signedInUserPermissions: []accesscontrol.Permission{
{Action: "folders:view", Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"},
},
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: []string{"parent", "subfolder"},
},
{
@@ -659,7 +654,6 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
signedInUserPermissions: []accesscontrol.Permission{
{Action: "folders:admin", Scope: "folders:uid:subfolder", Kind: "folders", Identifier: "subfolder"},
},
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: []string{"subfolder"},
},
{
@@ -670,7 +664,6 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
{Action: "folders:edit", Scope: "folders:uid:subfolder", Kind: "folders", Identifier: "subfolder"},
{Action: "folders:view", Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"},
},
features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets},
expectedResult: []string{"subfolder"},
},
}
@@ -696,7 +689,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
Scope: "folders:uid:unrelated"})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), tc.signedInUserPermissions)}}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@@ -153,6 +153,9 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:read",
"dashboards:view",
"dashboards:edit",
"dashboards:admin",
int64(1),
int64(1),
int64(1),
@@ -161,6 +164,9 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:read",
"folders:view",
"folders:edit",
"folders:admin",
int64(1),
int64(1),
0,
@@ -168,6 +174,9 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"folders:read",
"folders:view",
"folders:edit",
"folders:admin",
},
},
{
@@ -186,6 +195,8 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:write",
"dashboards:edit",
"dashboards:admin",
int64(1),
int64(1),
int64(1),
@@ -194,6 +205,8 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:write",
"folders:edit",
"folders:admin",
int64(1),
int64(1),
0,
@@ -201,6 +214,8 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:create",
"folders:edit",
"folders:admin",
},
},
{
@@ -219,6 +234,9 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:read",
"folders:view",
"folders:edit",
"folders:admin",
int64(1),
int64(1),
int64(1),
@@ -227,6 +245,9 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"folders:read",
"folders:view",
"folders:edit",
"folders:admin",
int64(1),
int64(1),
int64(1),
@@ -235,6 +256,9 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:read",
"dashboards:view",
"dashboards:edit",
"dashboards:admin",
int64(1),
},
},
@@ -254,6 +278,9 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:read",
"dashboards:view",
"dashboards:edit",
"dashboards:admin",
int64(1),
int64(1),
0,
@@ -261,6 +288,9 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:read",
"folders:view",
"folders:edit",
"folders:admin",
int64(1),
int64(1),
0,
@@ -268,6 +298,9 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"folders:read",
"folders:view",
"folders:edit",
"folders:admin",
},
},
{
@@ -286,6 +319,8 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:write",
"folders:edit",
"folders:admin",
int64(1),
int64(1),
int64(1),
@@ -294,6 +329,8 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:create",
"folders:edit",
"folders:admin",
int64(1),
int64(1),
int64(1),
@@ -302,6 +339,8 @@ func TestBuilder_RBAC(t *testing.T) {
int64(1),
0,
"dashboards:write",
"dashboards:edit",
"dashboards:admin",
},
},
}
+1 -1
View File
@@ -607,7 +607,7 @@ func (c *K8sTestHelper) AddOrUpdateTeamMember(user User, teamID int64, permissio
c.env.Server.HTTPServer.AlertNG.AccesscontrolService,
c.teamSvc,
c.userSvc,
resourcepermissions.NewActionSetService(c.env.FeatureToggles),
resourcepermissions.NewActionSetService(),
)
require.NoError(c.t, err)
@@ -12,16 +12,6 @@ function checkCanCreateFolders(folderDTO?: FolderDTO) {
return false;
}
if (!config.featureToggles.accessActionSets) {
if (!folderDTO || folderDTO.uid === 'general') {
return checkFolderPermission(AccessControlAction.FoldersCreate);
}
return (
checkFolderPermission(AccessControlAction.FoldersCreate) &&
checkFolderPermission(AccessControlAction.FoldersWrite, folderDTO)
);
}
return checkFolderPermission(AccessControlAction.FoldersCreate, folderDTO);
}