-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(security): extend Feature
definition to support explicit feature replacements
#206660
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,9 +21,9 @@ import { validateKibanaFeature, validateElasticsearchFeature } from './feature_s | |
import type { ConfigOverridesType } from './config'; | ||
|
||
/** | ||
* Describes parameters used to retrieve all Kibana features. | ||
* Describes parameters used to retrieve all Kibana features (for internal consumers). | ||
*/ | ||
export interface GetKibanaFeaturesParams { | ||
export interface GetKibanaFeaturesParamsInternal { | ||
/** | ||
* If provided, the license will be used to filter out features that require a license higher than the specified one. | ||
* */ | ||
|
@@ -41,6 +41,17 @@ export interface GetKibanaFeaturesParams { | |
omitDeprecated?: boolean; | ||
} | ||
|
||
/** | ||
* Describes parameters used to retrieve all Kibana features (for public consumers). | ||
*/ | ||
export interface GetKibanaFeaturesParams { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: Due to the way the start contract of the Feature plugin was defined, it was technically possible to pass any arguments to the getKibanaFeatures: this.featureRegistry.getAllKibanaFeatures.bind(this.featureRegistry), This change explicitly defines the list of accepted arguments, ensuring that only getKibanaFeatures: (params) => this.featureRegistry.getAllKibanaFeatures(
params && { omitDeprecated: params.omitDeprecated }
), |
||
/** | ||
* If true, deprecated features will be omitted. For backward compatibility reasons, deprecated features are included | ||
* in the result by default. | ||
*/ | ||
omitDeprecated: boolean; | ||
} | ||
|
||
export class FeatureRegistry { | ||
private locked = false; | ||
private kibanaFeatures: Record<string, KibanaFeatureConfig> = {}; | ||
|
@@ -207,6 +218,7 @@ export class FeatureRegistry { | |
|
||
// Iterate over all top-level and sub-feature privileges. | ||
const isFeatureDeprecated = !!feature.deprecated; | ||
const replacementFeatureIds = new Set(); | ||
for (const [privilegeId, privilege] of [ | ||
...Object.entries(feature.privileges), | ||
...collectSubFeaturesPrivileges(feature), | ||
|
@@ -263,6 +275,32 @@ export class FeatureRegistry { | |
); | ||
} | ||
} | ||
|
||
replacementFeatureIds.add(featureReference.feature); | ||
} | ||
} | ||
|
||
const featureReplacedBy = feature.deprecated?.replacedBy; | ||
if (featureReplacedBy) { | ||
if (featureReplacedBy.length === 0) { | ||
throw new Error( | ||
`Feature “${feature.id}” is deprecated and must have at least one feature ID added to the “replacedBy” property, or the property must be left out completely.` | ||
); | ||
} | ||
|
||
// The feature can be marked as replaced by another feature only if that feature is actually used to replace any | ||
// of the deprecated feature’s privileges. | ||
const invalidFeatureIds = featureReplacedBy.filter( | ||
(featureId) => !replacementFeatureIds.has(featureId) | ||
); | ||
if (invalidFeatureIds.length > 0) { | ||
throw new Error( | ||
`Cannot replace deprecated feature “${ | ||
feature.id | ||
}” with the following features, as they aren’t used to replace feature privileges: ${invalidFeatureIds.join( | ||
', ' | ||
)}.` | ||
); | ||
} | ||
} | ||
} | ||
|
@@ -272,7 +310,7 @@ export class FeatureRegistry { | |
license, | ||
ignoreLicense = false, | ||
omitDeprecated = false, | ||
}: GetKibanaFeaturesParams = {}): KibanaFeature[] { | ||
}: GetKibanaFeaturesParamsInternal = {}): KibanaFeature[] { | ||
if (!this.locked) { | ||
throw new Error('Cannot retrieve Kibana features while registration is still open'); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,25 +6,25 @@ | |
*/ | ||
|
||
import type { CoreSetup, Logger } from '@kbn/core/server'; | ||
import type { FeaturesPluginStart } from '@kbn/features-plugin/server'; | ||
|
||
import type { Space } from '../../../common'; | ||
import { addSpaceIdToPath } from '../../../common'; | ||
import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants'; | ||
import type { PluginsSetup } from '../../plugin'; | ||
import type { SpacesServiceStart } from '../../spaces_service/spaces_service'; | ||
import type { SpacesServiceStart } from '../../spaces_service'; | ||
import { wrapError } from '../errors'; | ||
import { getSpaceSelectorUrl } from '../get_space_selector_url'; | ||
import { withSpaceSolutionDisabledFeatures } from '../utils/space_solution_disabled_features'; | ||
|
||
export interface OnPostAuthInterceptorDeps { | ||
http: CoreSetup['http']; | ||
features: PluginsSetup['features']; | ||
getFeatures: () => Promise<FeaturesPluginStart>; | ||
getSpacesService: () => SpacesServiceStart; | ||
log: Logger; | ||
} | ||
|
||
export function initSpacesOnPostAuthRequestInterceptor({ | ||
features, | ||
getFeatures, | ||
getSpacesService, | ||
log, | ||
http, | ||
|
@@ -38,7 +38,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ | |
|
||
const spaceId = spacesService.getSpaceId(request); | ||
|
||
// The root of kibana is also the root of the defaut space, | ||
// The root of kibana is also the root of the default space, | ||
// since the default space does not have a URL Identifier (i.e., `/s/foo`). | ||
const isRequestingKibanaRoot = path === '/' && spaceId === DEFAULT_SPACE_ID; | ||
const isRequestingSpaceRoot = path === '/' && spaceId !== DEFAULT_SPACE_ID; | ||
|
@@ -106,7 +106,10 @@ export function initSpacesOnPostAuthRequestInterceptor({ | |
} | ||
} | ||
|
||
const allFeatures = features.getKibanaFeatures(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: |
||
// The Spaces client returns migrated feature IDs in `disabledFeatures`, so we need to omit | ||
// deprecated features. Otherwise, apps granted by deprecated features will be considered | ||
// available when they shouldn't be, since their IDs won't be present in `disabledFeatures`. | ||
const allFeatures = (await getFeatures()).getKibanaFeatures({ omitDeprecated: true }); | ||
const disabledFeatureKeys = withSpaceSolutionDisabledFeatures( | ||
allFeatures, | ||
space.disabledFeatures, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: I couldn't come up with a better name to describe what it's for, but open to suggestions...