Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for sub-feature privileges #60563

Merged
merged 6 commits into from Mar 24, 2020
Merged

Support for sub-feature privileges #60563

merged 6 commits into from Mar 24, 2020

Conversation

@legrego
Copy link
Contributor

legrego commented Mar 18, 2020

Summary

Adds support for Kibana sub-feature privileges. Previously, Kibana features could register their own All and Read privileges. This was useful for easily granting readonly access to certain features, and denying access to other features entirely.

While this model has served us well, it is not always granular enough. For example, there was no way for users to have "readonly" access to Dashboards, but with the ability to create "short urls" to them. There are many other potential use-cases for sub-feature privileges, but those are outside the scope of this PR.

The primary goal of this PR is to introduce the primitives to allow features to define their own set of sub-features, and subsequently provide a set of privileges to grant access to those sub-features. To prove the concept, this PR adds a single "Create Short URL" sub-feature privilege to the Discover, Visualize, and Dashboard features, which can be individually toggled if desired.

Licensing

The ability to create roles which take advantage of sub-feature privileges is available to clusters running a Gold license or better. Users with a Basic (free) license will continue to enjoy the existing experience.

馃攳 Code owners

Heya code owners, don't get scared by the size of this PR! Chances are, your changes are very minimal.

Kibana Platform Team

You have the most to review outside of the security team, but this should still be managable. This PR updated the features plugin in order to introduce the concept of sub-features, and sub-feature privileges.

Kibana App Team

In addition to the items listed below, the Dashboard app was updated to support the new "Create short URL" sub-feature privilege.

All other teams

  1. When features are registered, they can now provide an optional order value. I added an order to your feature to make its display consistent with the order of Kibana's main navigation bar.
  2. Feature privileges used to cascade from the "feature" level to the "privilege" level. You might find that we added an app, catalogue, and/or management section to your existing privileges.

Implementation

Definitions

Primary Feature Privilege: The main privilege assigned to a feature. This is currently enforced by the system to be one of All, Read, or None. Primary feature privileges will grant all sub features which haven't excluded themselves (see below).

Minimal Feature Privilege: A minimal version of each Primary Feature Privilege is automatically created, using minimal_ as a prefix on the privilege id (minimal_all, minimal_read). These minimal privileges include everything explicitly granted via feature registration, but exclude all sub feature privileges.

Sub Feature: A named collection of privileges which are individually assignable for fine-grained access control. Sub-feature privileges optionally include themselves into the Primary Feature Privileges, and by extension, Base Privileges.

Notable Changes

Optional subFeatures specification during feature registration.

Sub Features are comprised of one or more privilegeGroups. Each privilege group is one two types:

  • independent: Privileges within an independent group have no restrictions, and can be selected in any combination.
  • mutually_exclusive: Privileges within a mutually exclusive group must be specified in descending order of permissiveness (e.g., All/Read/None, not Read/All/None). Users can assign at most one privilege from each mutually exclusive group on the role management screen. It's important to note that there are no API restrictions on this. It's merely a cosmetic feature.

Each sub-feature privilege specifies which "Primary Feature Privilege" it should be included in, via the includeIn field. Acceptable values are all, read, and none. Privileges included in readare also included inall` automatically.

Sub features cannot be specified if the privileges definition is not provided. In other words, Features with reserved privileges, or features without any privileges are unable to specify sub-feature privileges.

Feature entities no longer cascade to privileges

Previously, the app, catalogue, and management entities specified at the Feature level would automatically cascade to all privileges, unless the privileges themselves specified their own subset. With the addition of sub-feature privileges, this logic does not make sense anymore, so all privileges must be explicit about the entities they grant access to.

There is existing validation to ensure that privileges do not specify entities which do not exist at the feature level. This PR adds additional validation which ensures that entities defined at the feature level are used somewhere in the privilege definitions.

Introduction of minimal privileges

As introduced above, minimal privileges are automatically created by the Security plugin.

Implement "Short URL" sub-feature privileges

As part of the sub-feature initiative, this PR also implements a "Create Short URL" sub-feature privilege to the discover, visualize, and dashboard applications. This will allow administrators to create users with read-only access to these applications, but with the ability to generate short urls.

View/Edit sub-feature privileges

Sub-feature privileges can now be viewed and edited in the Role Management UI.

image

image

Note: Spaces management does not show sub-features, and does not allow toggling sub-features at the space-level.

Relaxed privilege form restrictions

The previous version prevented users from creating privileges that would otherwise be superseded by other privileges. For example, if a role had "global all" assigned, then it did not make sense to configure a "global read" privilege at a specific space.

These restrictions were problematic for two reasons:

  • They were difficult for end-users to reason about
  • The implementation was difficult for engineers to reason about

This redesigned form relaxes the restrictions, so users can create privilege sets which will otherwise be superseded by other privileges. The new UI surfaces a warning when this happens, and the updated Privilege Summary will still show users the final "effective privileges".

Privilege Calculator

As a result of the relaxed form restrictions, we were able to replace the existing "effective" and "actual" privilege calculators with targeted, UI-centric calculators:

  • The Privilege Form Calculator is responsible for driving (as the name implies), the privilege forms.
  • The Privilege Summary Calculator is responsible for driving (as the name implies) the Privilege Summary ("matrix") view.

actions.allHack removed

As a result of the redesigned calculators, we were able to remove the allHack from the ES privilege definitions.

Explicit feature ordering

Previously, registered features were not presented in a predictable order. This PR adds an optional order field to feature registration, which drives the display order of these features.

Since grouped navigation is not implemented yet, this PR adds an order which is more or less consistent with the display order in the main Kibana nav.

Introduced Features client-side plugin

Previously, the security and spaces plugins were making direct calls to the /api/features endpoint. This PR adds a client-side plugin which both security and spaces leverage to retrieve the list of available features.

PR Organization

This PR is starting with 3 commits before reviews. Each of these commits have independently undergone review by the kibana security team, but no other teams. The vast majority of the changes were the responsibility of the security team, so in theory a bulk of this PR has already been reviewed and approved.

  • 29ec796 introduces server-side support for sub-feature privileges, and underwent review in #57507

  • d8acb63 introduces client-side support for sub-feature privileges, and underwent review in #59198

  • fd9485e introduces licensing support for sub-feature privileges. Sub-feature privileges will be visible and enabled with a Gold license or better. Clusters with a basic license will continue to enjoy the existing feature privilege experience. This underwent review in #59750

Resolves #35616
Resolves #38101

legrego added 3 commits Mar 2, 2020
* initial server-side support for sub-feature privileges

* start addressing PR feedback

* renaming interfaces

* move privilege id collision check to security plugin

* additional testing

* change featurePrivilegeIterator import location

* fix link assertions following rebase from master
* Initial UI support for sub-feature privileges

* Address PR feedback

* display deleted spaces correctly in the privilege summary

* additional testing

* update snapshot
* enables sub-feature privileges for gold+ licenses

* Address PR feedback
@legrego legrego force-pushed the security/sub-feature-privileges branch from fd9485e to 376091e Mar 19, 2020
@elasticmachine

This comment has been minimized.

Copy link
Contributor

elasticmachine commented Mar 19, 2020

Pinging @elastic/kibana-security (Team:Security)

@legrego legrego marked this pull request as ready for review Mar 19, 2020
@legrego legrego requested review from elastic/apm-ui as code owners Mar 19, 2020
@legrego legrego requested a review from jportner Mar 19, 2020
@sqren

This comment has been minimized.

Copy link
Member

sqren commented Mar 19, 2020

This is awesome and comes in very handy for APM! Thanks @legrego!

Copy link
Member

jgowdyelastic left a comment

ML changes LGTM

Copy link
Contributor

mikecote left a comment

Alerting changes LGTM

Copy link
Contributor

jportner left a comment

Spent most of my time looking at the server-side code. A couple of minor nits / questions, other than that LGTM!

groupType: Joi.string(),
privileges: Joi.array().items(subFeaturePrivilegeSchema),
Comment on lines 78 to 79

This comment has been minimized.

Copy link
@jportner

jportner Mar 19, 2020

Contributor

Should these attributes be required?

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

Looks like it:
privileges: SubFeaturePrivilegeConfig[];
groupType: 'mutually_exclusive' | 'independent';

Also groupType should be Joi.string().valid('mutually_exclusive', 'independent').required()

name: Joi.string(),
privilegeGroups: Joi.array().items(
Joi.object({
name: Joi.string(),

This comment has been minimized.

Copy link
@jportner

jportner Mar 19, 2020

Contributor

It looks like this attribute is unused and could be removed?

@@ -9,7 +9,7 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';

export class FeaturePrivilegeAppBuilder extends BaseFeaturePrivilegeBuilder {
public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] {

This comment has been minimized.

Copy link
@jportner

jportner Mar 19, 2020

Contributor

Can we get rid of the feature parameter now that it is unused?

@@ -9,7 +9,7 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';

export class FeaturePrivilegeCatalogueBuilder extends BaseFeaturePrivilegeBuilder {
public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] {

This comment has been minimized.

Copy link
@jportner

jportner Mar 19, 2020

Contributor

Can we get rid of the feature parameter now that it is unused?

@@ -9,7 +9,7 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';

export class FeaturePrivilegeManagementBuilder extends BaseFeaturePrivilegeBuilder {
public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] {

This comment has been minimized.

Copy link
@jportner

jportner Mar 19, 2020

Contributor

Can we get rid of the feature parameter now that it is unused?

Copy link
Contributor

chrisronline left a comment

LGTM from Stack Monitoring

Copy link
Contributor

jen-huang left a comment

LGTM from Ingest Management

@sqren
sqren approved these changes Mar 19, 2020
Copy link
Member

sqren left a comment

APM changes lgtm

Copy link
Contributor

aaronjcaldwell left a comment

Maps changes lgtm!

  • code review
  • tested locally in chrome
Copy link
Contributor

pgayvallet left a comment

Only reviewed platform team owner files.

Overall, LGTM when the x-pack/plugins/features/server/feature_schema.ts schema fix is addressed.

Some NITs and minors.

export * from './feature';
export * from './sub_feature';
Comment on lines 8 to 9

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

NIT: (was already present, but) avoid * exports when possible.

* - `independent`::
* Users will be able to select any combination of privileges within this group.
*/
groupType: 'mutually_exclusive' | 'independent';

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

NIT: would probably extract to proper type, wdyt?

export class SubFeature {
constructor(protected readonly config: RecursiveReadonly<SubFeatureConfig>) {}

Comment on lines +62 to +64

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

Just wondering: what are the upsides to use an accessor wrapper class around SubFeatureConfig ? Is it to avoid using RecursiveReadonly<SubFeatureConfig> in the codebase?

This comment has been minimized.

Copy link
@legrego

legrego Mar 23, 2020

Author Contributor

Valid question! On its own, it's honestly not all that useful, but I took this approach so that consumers had a consistent way to access Features and SubFeatures. The security plugin extends both of these models in x-pack/plugins/security/public/management/roles/model to add security-specific constructs

import { FeatureConfig, Feature } from '.';

export class FeaturesAPIClient {
constructor(private readonly http: CoreSetup['http']) {}

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

NIT: avoid the CoreSetup['xxx'] syntax. import & use HttpSetup instead.

This comment has been minimized.

Copy link
@legrego

legrego Mar 23, 2020

Author Contributor

I had gotten in to the CoreSetup['xxx'] habit because I was bitten by inconsistent types in the past. I remember cases where the service's exposed Setup interface differed from that CoreSetup returned. (e.g., CoreSetup might Pick<> a subset of properties). That's not the case here, and I'm sure the service APIs have stablized since then, but I at least wanted to explain my thinking. I'll update this to use HttpSetup.

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 24, 2020

Contributor

All these inconsistencies should have been fixed by now. Do not hesitate to ping us if you find any.

privileges: {
all: FeatureKibanaPrivileges;
read: FeatureKibanaPrivileges;
} | null;

/**
* Optional sub-feature privilege definitions. This can only be specified if `privileges` are are also defined.
*/
subFeatures?: SubFeatureConfig[];
Comment on lines +106 to +114

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

is there a reason to prefer | null instead of privileges? ? seems inconsistent with other optional properties of the type.

This comment has been minimized.

Copy link
@legrego

legrego Mar 23, 2020

Author Contributor

You're right this is rather inconsistent, but there is a reason. Our thinking here is that we want plugin authors to be very explicit when they choose to opt-out of defining privileges for their feature. If we were to use privileges?, then we wouldn't know if they were intentionally omitted, or if the author made a mistake defining their feature.

const subFeaturePrivilegeSchema = Joi.object({
id: Joi.string()
.regex(subFeaturePrivilegePartRegex)
.required(),
Comment on lines +48 to +51

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

(outside of the scope of the PR) this whole file would need a migration to config-schema...

groupType: Joi.string(),
privileges: Joi.array().items(subFeaturePrivilegeSchema),

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

Looks like it:
privileges: SubFeaturePrivilegeConfig[];
groupType: 'mutually_exclusive' | 'independent';

Also groupType should be Joi.string().valid('mutually_exclusive', 'independent').required()

@@ -5,6 +5,8 @@
*/

import { buildOSSFeatures } from './oss_features';
import { featurePrivilegeIterator } from '../../security/server/authorization';

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

馃檲 import from non-dependant plugin + import from nested path (why doesn't eslint complains about that last point btw?). Maybe featurePrivilegeIterator belongs more in the features plugin?

This comment has been minimized.

Copy link
@legrego

legrego Mar 23, 2020

Author Contributor

Yeah.... we have a discussion around this here: #57507 (comment).

I'm torn on what to do. The current situation is not at all ideal..We only ever expect featurePrivilegeIterator to be consumed in two scenarios:

  1. internal to the security plugin, as it has knowledge of how the privileges are merged together.
  2. external to the security plugin as part of another plugin's test suite only. We wanted to provide a mechanism for plugin authors to write tests against their feature registration, to ensure their privileges are created in the way they expect.

I'm not opposed to moving this to the features plugin, but I don't love that we'd be moving security-specific logic out of security and into features. What do you think?

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 24, 2020

Contributor

If the only usages outside of the security plugin are meant to be for testing purposes, maybe exposing it from a security/server/test_utils or something would be more explicit?

Note that if we do have conventions about mocks and plugin_name/[server|public]/mocks, do we not have the equivalent for exposing test utilities to other plugins atm.

This can be done at a later time though, not a blocker here.

ping @restrry @joshdover

This comment has been minimized.

Copy link
@restrry

restrry Mar 24, 2020

Member

not Ideal, but we can expose test utils from mocks

request.query.ignoreValidLicenses ||
!feature.validLicenses ||
!feature.validLicenses.length ||
getLegacyAPI().xpackInfo.license.isOneOf(feature.validLicenses)

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

(outside of the scope of the PR) @restrry: can this be replaced with a call to one of the licensing plugin API now?

This comment has been minimized.

Copy link
@restrry

restrry Mar 24, 2020

Member

I believe so, licensing plugin exposes license$`

privileges: ({
...createFeaturePrivilege('all'),
},
},
} as unknown) as FeatureConfig['privileges'],
Comment on lines 50 to 52

This comment has been minimized.

Copy link
@pgayvallet

pgayvallet Mar 23, 2020

Contributor

(did not check reasons of mismatch, but) Can we adapt createFeaturePrivilege to avoid this as unknown) as FeatureConfig['privileges'] ?

This comment has been minimized.

Copy link
@legrego

legrego Mar 23, 2020

Author Contributor

Ah, this is complaining because the privileges object, when defined, expects both a read and an all property, and these tests, for the sake of brevity, do not include both. I can likely clean this up though, let me take a stab at it.

legrego added 2 commits Mar 23, 2020
鈥eature-privileges
@legrego legrego requested a review from elastic/siem as a code owner Mar 23, 2020
鈥eature-privileges
@kibanamachine

This comment has been minimized.

Copy link

kibanamachine commented Mar 24, 2020

馃挌 Build Succeeded

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@legrego legrego merged commit b82cc6e into master Mar 24, 2020
56 checks passed
56 checks passed
API integration tests node scripts/functional_tests --config test/api_integration/config.js --bail --debug
Details
Browser tests yarn run grunt test:karma-ci
Details
Build kbn_tp_sample_panel_action yarn build
Details
CLA All commits passed the check
Details
Check core API changes node scripts/check_published_api_changes
Details
Check file casing node scripts/check_file_casing --quiet
Details
Check licenses node scripts/check_licenses --dev
Details
Check lockfile symlinks node scripts/check_lockfile_symlinks --quiet
Details
Example functional tests node scripts/functional_tests --config test/examples/config.js --bail --debug
Details
Functional tests / Group 1 yarn run grunt run:functionalTests_ciGroup1
Details
Functional tests / Group 10 yarn run grunt run:functionalTests_ciGroup10
Details
Functional tests / Group 11 yarn run grunt run:functionalTests_ciGroup11
Details
Functional tests / Group 12 yarn run grunt run:functionalTests_ciGroup12
Details
Functional tests / Group 2 yarn run grunt run:functionalTests_ciGroup2
Details
Functional tests / Group 3 yarn run grunt run:functionalTests_ciGroup3
Details
Functional tests / Group 4 yarn run grunt run:functionalTests_ciGroup4
Details
Functional tests / Group 5 yarn run grunt run:functionalTests_ciGroup5
Details
Functional tests / Group 6 yarn run grunt run:functionalTests_ciGroup6
Details
Functional tests / Group 7 yarn run grunt run:functionalTests_ciGroup7
Details
Functional tests / Group 8 yarn run grunt run:functionalTests_ciGroup8
Details
Functional tests / Group 9 yarn run grunt run:functionalTests_ciGroup9
Details
Internationalization check node scripts/i18n_check --ignore-missing
Details
Interpreter functional tests node scripts/functional_tests --config test/interpreter_functional/config.ts --bail --debug --kibana-install-dir /dev/shm/workspace/kibana/build/oss/kibana-8.0.0-SNAPSHOT-linux-x86_64-1
Details
Jest integration tests yarn run grunt test:jest_integration
Details
Jest tests yarn run grunt test:jest
Details
Kibana accessibility tests node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/kibana/build/oss/kibana-8.0.0-SNAPSHOT-linux-x86_64 --config test/accessibility/config.ts
Details
Mocha tests node scripts/mocha
Details
Node.js hardening tests node scripts/test_hardening.js
Details
Plugin functional tests node scripts/functional_tests --config test/plugin_functional/config.js --bail --debug
Details
Project tests yarn run grunt test:projects
Details
SIEM Cypress Tests node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana- --config test/siem_cypress/config.ts
Details
Type check node scripts/type_check
Details
TypeScript - all files belong to a TypeScript project node scripts/check_ts_projects
Details
Verify NOTICE.txt node scripts/notice --validate
Details
Verify dependency versions yarn run grunt verifyDependencyVersions
Details
X-Pack Chrome Functional tests / Group 1 node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana-1 --include-tag ciGroup1
Details
X-Pack Chrome Functional tests / Group 10 node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana-10 --include-tag ciGroup10
Details
X-Pack Chrome Functional tests / Group 2 node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana-2 --include-tag ciGroup2
Details
X-Pack Chrome Functional tests / Group 3 node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana-3 --include-tag ciGroup3
Details
X-Pack Chrome Functional tests / Group 4 node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana-4 --include-tag ciGroup4
Details
X-Pack Chrome Functional tests / Group 5 node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana-5 --include-tag ciGroup5
Details
X-Pack Chrome Functional tests / Group 6 node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana-6 --include-tag ciGroup6
Details
X-Pack Chrome Functional tests / Group 7 node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana-7 --include-tag ciGroup7
Details
X-Pack Chrome Functional tests / Group 8 node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana-8 --include-tag ciGroup8
Details
X-Pack Chrome Functional tests / Group 9 node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana-9 --include-tag ciGroup9
Details
X-Pack Jest node --max-old-space-size=6144 scripts/jest --ci --verbose --detectOpenHandles
Details
X-Pack Karma Tests yarn test:karma
Details
X-Pack SIEM cyclic dependency test node legacy/plugins/siem/scripts/check_circular_deps
Details
X-Pack accessibility tests node scripts/functional_tests --debug --bail --kibana-install-dir /dev/shm/workspace/install/kibana --config test/accessibility/config.ts
Details
elasticsearch-ci/docs Build finished.
Details
eslint node scripts/eslint --no-cache
Details
kibana-ci Build finished.
Details
prbot:outdated
prbot:release note labels
prbot:release version labels
sasslint node scripts/sasslint
Details
@legrego legrego deleted the security/sub-feature-privileges branch Mar 24, 2020
legrego added a commit to legrego/kibana that referenced this pull request Mar 24, 2020
* initial server-side support for sub-feature privileges (elastic#57507)

* initial server-side support for sub-feature privileges

* start addressing PR feedback

* renaming interfaces

* move privilege id collision check to security plugin

* additional testing

* change featurePrivilegeIterator import location

* fix link assertions following rebase from master

* Initial UI support for sub-feature privileges (elastic#59198)

* Initial UI support for sub-feature privileges

* Address PR feedback

* display deleted spaces correctly in the privilege summary

* additional testing

* update snapshot

* Enables sub-feature privileges for gold+ licenses (elastic#59750)

* enables sub-feature privileges for gold+ licenses

* Address PR feedback

* address platform review feedback
legrego added a commit that referenced this pull request Mar 24, 2020
* initial server-side support for sub-feature privileges (#57507)

* initial server-side support for sub-feature privileges

* start addressing PR feedback

* renaming interfaces

* move privilege id collision check to security plugin

* additional testing

* change featurePrivilegeIterator import location

* fix link assertions following rebase from master

* Initial UI support for sub-feature privileges (#59198)

* Initial UI support for sub-feature privileges

* Address PR feedback

* display deleted spaces correctly in the privilege summary

* additional testing

* update snapshot

* Enables sub-feature privileges for gold+ licenses (#59750)

* enables sub-feature privileges for gold+ licenses

* Address PR feedback

* address platform review feedback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can鈥檛 perform that action at this time.