Introduce reserved ml privilege for the apm_user role (#72266)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Larry Gregory 2020-07-28 07:44:37 -04:00 committed by GitHub
parent 46fb8475f3
commit 09b11b61f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 102 additions and 107 deletions

View file

@ -17,12 +17,12 @@ export const METRICS_FEATURE = {
order: 700, order: 700,
icon: 'metricsApp', icon: 'metricsApp',
navLinkId: 'metrics', navLinkId: 'metrics',
app: ['infra', 'kibana'], app: ['infra', 'metrics', 'kibana'],
catalogue: ['infraops'], catalogue: ['infraops'],
alerting: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], alerting: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID],
privileges: { privileges: {
all: { all: {
app: ['infra', 'kibana'], app: ['infra', 'metrics', 'kibana'],
catalogue: ['infraops'], catalogue: ['infraops'],
api: ['infra'], api: ['infra'],
savedObject: { savedObject: {
@ -35,7 +35,7 @@ export const METRICS_FEATURE = {
ui: ['show', 'configureSource', 'save', 'alerting:show'], ui: ['show', 'configureSource', 'save', 'alerting:show'],
}, },
read: { read: {
app: ['infra', 'kibana'], app: ['infra', 'metrics', 'kibana'],
catalogue: ['infraops'], catalogue: ['infraops'],
api: ['infra'], api: ['infra'],
savedObject: { savedObject: {
@ -58,12 +58,12 @@ export const LOGS_FEATURE = {
order: 800, order: 800,
icon: 'logsApp', icon: 'logsApp',
navLinkId: 'logs', navLinkId: 'logs',
app: ['infra', 'kibana'], app: ['infra', 'logs', 'kibana'],
catalogue: ['infralogging'], catalogue: ['infralogging'],
alerting: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], alerting: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID],
privileges: { privileges: {
all: { all: {
app: ['infra', 'kibana'], app: ['infra', 'logs', 'kibana'],
catalogue: ['infralogging'], catalogue: ['infralogging'],
api: ['infra'], api: ['infra'],
savedObject: { savedObject: {
@ -76,7 +76,7 @@ export const LOGS_FEATURE = {
ui: ['show', 'configureSource', 'save'], ui: ['show', 'configureSource', 'save'],
}, },
read: { read: {
app: ['infra', 'kibana'], app: ['infra', 'logs', 'kibana'],
catalogue: ['infralogging'], catalogue: ['infralogging'],
api: ['infra'], api: ['infra'],
alerting: { alerting: {

View file

@ -7,6 +7,10 @@
import { KibanaRequest } from 'kibana/server'; import { KibanaRequest } from 'kibana/server';
import { PLUGIN_ID } from '../constants/app'; import { PLUGIN_ID } from '../constants/app';
export const apmUserMlCapabilities = {
canGetJobs: false,
};
export const userMlCapabilities = { export const userMlCapabilities = {
canAccessML: false, canAccessML: false,
// Anomaly Detection // Anomaly Detection
@ -68,6 +72,7 @@ export function getDefaultCapabilities(): MlCapabilities {
} }
export function getPluginPrivileges() { export function getPluginPrivileges() {
const apmUserMlCapabilitiesKeys = Object.keys(apmUserMlCapabilities);
const userMlCapabilitiesKeys = Object.keys(userMlCapabilities); const userMlCapabilitiesKeys = Object.keys(userMlCapabilities);
const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities); const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities);
const allMlCapabilitiesKeys = [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys]; const allMlCapabilitiesKeys = [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys];
@ -101,6 +106,17 @@ export function getPluginPrivileges() {
read: savedObjects, read: savedObjects,
}, },
}, },
apmUser: {
excludeFromBasePrivileges: true,
app: [],
catalogue: [],
savedObject: {
all: [],
read: [],
},
api: apmUserMlCapabilitiesKeys.map((k) => `ml:${k}`),
ui: apmUserMlCapabilitiesKeys,
},
}; };
} }

View file

@ -75,7 +75,7 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
} }
public setup(coreSetup: CoreSetup, plugins: PluginsSetup): MlPluginSetup { public setup(coreSetup: CoreSetup, plugins: PluginsSetup): MlPluginSetup {
const { admin, user } = getPluginPrivileges(); const { admin, user, apmUser } = getPluginPrivileges();
plugins.features.registerFeature({ plugins.features.registerFeature({
id: PLUGIN_ID, id: PLUGIN_ID,
@ -108,6 +108,10 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
id: 'ml_admin', id: 'ml_admin',
privilege: admin, privilege: admin,
}, },
{
id: 'ml_apm_user',
privilege: apmUser,
},
], ],
}, },
}); });

View file

@ -53,7 +53,7 @@ describe('usingPrivileges', () => {
new Feature({ new Feature({
id: 'fooFeature', id: 'fooFeature',
name: 'Foo Feature', name: 'Foo Feature',
app: ['fooApp'], app: ['fooApp', 'foo'],
navLinkId: 'foo', navLinkId: 'foo',
privileges: null, privileges: null,
}), }),
@ -129,7 +129,7 @@ describe('usingPrivileges', () => {
new Feature({ new Feature({
id: 'fooFeature', id: 'fooFeature',
name: 'Foo Feature', name: 'Foo Feature',
app: [], app: ['foo'],
navLinkId: 'foo', navLinkId: 'foo',
privileges: null, privileges: null,
}), }),
@ -262,7 +262,7 @@ describe('usingPrivileges', () => {
id: 'barFeature', id: 'barFeature',
name: 'Bar Feature', name: 'Bar Feature',
navLinkId: 'bar', navLinkId: 'bar',
app: [], app: ['bar'],
privileges: null, privileges: null,
}), }),
], ],
@ -412,7 +412,7 @@ describe('all', () => {
new Feature({ new Feature({
id: 'fooFeature', id: 'fooFeature',
name: 'Foo Feature', name: 'Foo Feature',
app: [], app: ['foo'],
navLinkId: 'foo', navLinkId: 'foo',
privileges: null, privileges: null,
}), }),

View file

@ -18,12 +18,11 @@ export function disableUICapabilitiesFactory(
logger: Logger, logger: Logger,
authz: AuthorizationServiceSetup authz: AuthorizationServiceSetup
) { ) {
// nav links are sourced from two places: // nav links are sourced from the apps property.
// 1) The `navLinkId` property. This is deprecated and will be removed (https://github.com/elastic/kibana/issues/66217) // The Kibana Platform associates nav links to the app which registers it, in a 1:1 relationship.
// 2) The apps property. The Kibana Platform associates nav links to the app which registers it, in a 1:1 relationship. // This behavior is replacing the `navLinkId` property.
// This behavior is replacing the `navLinkId` property above.
const featureNavLinkIds = features const featureNavLinkIds = features
.flatMap((feature) => [feature.navLinkId, ...feature.app]) .flatMap((feature) => feature.app)
.filter((navLinkId) => navLinkId != null); .filter((navLinkId) => navLinkId != null);
const shouldDisableFeatureUICapability = ( const shouldDisableFeatureUICapability = (

View file

@ -9,9 +9,6 @@ import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
export class FeaturePrivilegeNavlinkBuilder extends BaseFeaturePrivilegeBuilder { export class FeaturePrivilegeNavlinkBuilder extends BaseFeaturePrivilegeBuilder {
public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] {
const appNavLinks = feature.app.map((app) => this.actions.ui.get('navLinks', app)); return (privilegeDefinition.app ?? []).map((app) => this.actions.ui.get('navLinks', app));
return feature.navLinkId
? [this.actions.ui.get('navLinks', feature.navLinkId), ...appNavLinks]
: appNavLinks;
} }
} }

View file

@ -54,20 +54,8 @@ describe('features', () => {
const actual = privileges.get(); const actual = privileges.get();
expect(actual).toHaveProperty('features.foo-feature', { expect(actual).toHaveProperty('features.foo-feature', {
all: [ all: [actions.login, actions.version],
actions.login, read: [actions.login, actions.version],
actions.version,
actions.ui.get('navLinks', 'kibana:foo'),
actions.ui.get('navLinks', 'app-1'),
actions.ui.get('navLinks', 'app-2'),
],
read: [
actions.login,
actions.version,
actions.ui.get('navLinks', 'kibana:foo'),
actions.ui.get('navLinks', 'app-1'),
actions.ui.get('navLinks', 'app-2'),
],
}); });
}); });
@ -275,7 +263,6 @@ describe('features', () => {
actions.ui.get('catalogue', 'all-catalogue-2'), actions.ui.get('catalogue', 'all-catalogue-2'),
actions.ui.get('management', 'all-management', 'all-management-1'), actions.ui.get('management', 'all-management', 'all-management-1'),
actions.ui.get('management', 'all-management', 'all-management-2'), actions.ui.get('management', 'all-management', 'all-management-2'),
actions.ui.get('navLinks', 'kibana:foo'),
actions.savedObject.get('all-savedObject-all-1', 'bulk_get'), actions.savedObject.get('all-savedObject-all-1', 'bulk_get'),
actions.savedObject.get('all-savedObject-all-1', 'get'), actions.savedObject.get('all-savedObject-all-1', 'get'),
actions.savedObject.get('all-savedObject-all-1', 'find'), actions.savedObject.get('all-savedObject-all-1', 'find'),
@ -386,7 +373,6 @@ describe('features', () => {
actions.ui.get('catalogue', 'read-catalogue-2'), actions.ui.get('catalogue', 'read-catalogue-2'),
actions.ui.get('management', 'read-management', 'read-management-1'), actions.ui.get('management', 'read-management', 'read-management-1'),
actions.ui.get('management', 'read-management', 'read-management-2'), actions.ui.get('management', 'read-management', 'read-management-2'),
actions.ui.get('navLinks', 'kibana:foo'),
actions.savedObject.get('read-savedObject-all-1', 'bulk_get'), actions.savedObject.get('read-savedObject-all-1', 'bulk_get'),
actions.savedObject.get('read-savedObject-all-1', 'get'), actions.savedObject.get('read-savedObject-all-1', 'get'),
actions.savedObject.get('read-savedObject-all-1', 'find'), actions.savedObject.get('read-savedObject-all-1', 'find'),
@ -644,12 +630,7 @@ describe('reserved', () => {
const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService);
const actual = privileges.get(); const actual = privileges.get();
expect(actual).toHaveProperty('reserved.foo', [ expect(actual).toHaveProperty('reserved.foo', [actions.version]);
actions.version,
actions.ui.get('navLinks', 'kibana:foo'),
actions.ui.get('navLinks', 'app-1'),
actions.ui.get('navLinks', 'app-2'),
]);
}); });
test(`actions only specified at the privilege are alright too`, () => { test(`actions only specified at the privilege are alright too`, () => {

View file

@ -23,7 +23,7 @@ const features = ([
id: 'feature_2', id: 'feature_2',
name: 'Feature 2', name: 'Feature 2',
navLinkId: 'feature2', navLinkId: 'feature2',
app: [], app: ['feature2'],
catalogue: ['feature2Entry'], catalogue: ['feature2Entry'],
management: { management: {
kibana: ['somethingElse'], kibana: ['somethingElse'],

View file

@ -83,8 +83,7 @@ function toggleDisabledFeatures(
for (const feature of disabledFeatures) { for (const feature of disabledFeatures) {
// Disable associated navLink, if one exists // Disable associated navLink, if one exists
const featureNavLinks = feature.navLinkId ? [feature.navLinkId, ...feature.app] : feature.app; feature.app.forEach((app) => {
featureNavLinks.forEach((app) => {
if (navLinks.hasOwnProperty(app) && !enabledAppEntries.has(app)) { if (navLinks.hasOwnProperty(app) && !enabledAppEntries.has(app)) {
navLinks[app] = false; navLinks[app] = false;
} }

View file

@ -43,7 +43,7 @@ export default function ({ getService }: FtrProviderContext) {
}, },
global: ['all', 'read'], global: ['all', 'read'],
space: ['all', 'read'], space: ['all', 'read'],
reserved: ['ml_user', 'ml_admin', 'monitoring'], reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
}; };
await supertest await supertest

View file

@ -41,7 +41,7 @@ export default function ({ getService }: FtrProviderContext) {
}, },
global: ['all', 'read'], global: ['all', 'read'],
space: ['all', 'read'], space: ['all', 'read'],
reserved: ['ml_user', 'ml_admin', 'monitoring'], reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
}; };
await supertest await supertest

View file

@ -423,19 +423,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(navLinks).to.not.contain(['Metrics']); expect(navLinks).to.not.contain(['Metrics']);
}); });
it(`metrics app is inaccessible and Application Not Found message is rendered`, async () => { it(`metrics app is inaccessible and returns a 404`, async () => {
await PageObjects.common.navigateToApp('infraOps'); await PageObjects.common.navigateToActualUrl('infraOps', '', {
await testSubjects.existOrFail('~appNotFoundPageContent'); ensureCurrentUrl: false,
await PageObjects.common.navigateToUrlWithBrowserHistory( shouldLoginIfPrompted: false,
'infraOps', });
'/inventory', const messageText = await PageObjects.common.getBodyText();
undefined, expect(messageText).to.eql(
{ JSON.stringify({
ensureCurrentUrl: false, statusCode: 404,
shouldLoginIfPrompted: false, error: 'Not Found',
} message: 'Not Found',
})
); );
await testSubjects.existOrFail('~appNotFoundPageContent');
}); });
}); });
}); });

View file

@ -79,21 +79,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
}); });
it(`metrics app is inaccessible and Application Not Found message is rendered`, async () => { it(`metrics app is inaccessible and Application Not Found message is rendered`, async () => {
await PageObjects.common.navigateToApp('infraOps', { await PageObjects.common.navigateToActualUrl('infraOps', '', {
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
basePath: '/s/custom_space', basePath: '/s/custom_space',
}); });
await testSubjects.existOrFail('~appNotFoundPageContent'); const messageText = await PageObjects.common.getBodyText();
await PageObjects.common.navigateToUrlWithBrowserHistory( expect(messageText).to.eql(
'infraOps', JSON.stringify({
'/inventory', statusCode: 404,
undefined, error: 'Not Found',
{ message: 'Not Found',
basePath: '/s/custom_space', })
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
}
); );
await testSubjects.existOrFail('~appNotFoundPageContent');
}); });
}); });

View file

@ -187,19 +187,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(navLinks).to.not.contain('Logs'); expect(navLinks).to.not.contain('Logs');
}); });
it(`logs app is inaccessible and Application Not Found message is rendered`, async () => { it(`logs app is inaccessible and returns a 404`, async () => {
await PageObjects.common.navigateToApp('infraLogs'); await PageObjects.common.navigateToActualUrl('infraLogs', '', {
await testSubjects.existOrFail('~appNotFoundPageContent'); ensureCurrentUrl: false,
await PageObjects.common.navigateToUrlWithBrowserHistory( shouldLoginIfPrompted: false,
'infraLogs', });
'/stream', const messageText = await PageObjects.common.getBodyText();
undefined, expect(messageText).to.eql(
{ JSON.stringify({
ensureCurrentUrl: false, statusCode: 404,
shouldLoginIfPrompted: false, error: 'Not Found',
} message: 'Not Found',
})
); );
await testSubjects.existOrFail('~appNotFoundPageContent');
}); });
}); });
}); });

View file

@ -80,21 +80,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
}); });
it(`logs app is inaccessible and Application Not Found message is rendered`, async () => { it(`logs app is inaccessible and Application Not Found message is rendered`, async () => {
await PageObjects.common.navigateToApp('infraLogs', { await PageObjects.common.navigateToActualUrl('infraLogs', '', {
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
basePath: '/s/custom_space', basePath: '/s/custom_space',
}); });
await testSubjects.existOrFail('~appNotFoundPageContent'); const messageText = await PageObjects.common.getBodyText();
await PageObjects.common.navigateToUrlWithBrowserHistory( expect(messageText).to.eql(
'infraLogs', JSON.stringify({
'/stream', statusCode: 404,
undefined, error: 'Not Found',
{ message: 'Not Found',
basePath: '/s/custom_space', })
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
}
); );
await testSubjects.existOrFail('~appNotFoundPageContent');
}); });
}); });
}); });

View file

@ -5,7 +5,7 @@
*/ */
interface Feature { interface Feature {
navLinkId: string; app: string[];
} }
export interface Features { export interface Features {

View file

@ -19,11 +19,11 @@ class FooPlugin implements Plugin {
name: 'Foo', name: 'Foo',
icon: 'upArrow', icon: 'upArrow',
navLinkId: 'foo_plugin', navLinkId: 'foo_plugin',
app: ['kibana'], app: ['foo_plugin', 'kibana'],
catalogue: ['foo'], catalogue: ['foo'],
privileges: { privileges: {
all: { all: {
app: ['kibana'], app: ['foo_plugin', 'kibana'],
catalogue: ['foo'], catalogue: ['foo'],
savedObject: { savedObject: {
all: ['foo'], all: ['foo'],
@ -32,7 +32,7 @@ class FooPlugin implements Plugin {
ui: ['create', 'edit', 'delete', 'show'], ui: ['create', 'edit', 'delete', 'show'],
}, },
read: { read: {
app: ['kibana'], app: ['foo_plugin', 'kibana'],
catalogue: ['foo'], catalogue: ['foo'],
savedObject: { savedObject: {
all: [], all: [],

View file

@ -13,11 +13,14 @@ export class NavLinksBuilder {
...features, ...features,
// management isn't a first-class "feature", but it makes our life easier here to pretend like it is // management isn't a first-class "feature", but it makes our life easier here to pretend like it is
management: { management: {
navLinkId: 'kibana:stack_management', app: ['kibana:stack_management'],
}, },
// TODO: Temp until navLinkIds fix is merged in // TODO: Temp until navLinkIds fix is merged in
appSearch: { appSearch: {
navLinkId: 'appSearch', app: ['appSearch', 'workplaceSearch'],
},
kibana: {
app: ['kibana'],
}, },
}; };
} }
@ -38,9 +41,9 @@ export class NavLinksBuilder {
private build(callback: buildCallback): Record<string, boolean> { private build(callback: buildCallback): Record<string, boolean> {
const navLinks = {} as Record<string, boolean>; const navLinks = {} as Record<string, boolean>;
for (const [featureId, feature] of Object.entries(this.features)) { for (const [featureId, feature] of Object.entries(this.features)) {
if (feature.navLinkId) { feature.app.forEach((app) => {
navLinks[feature.navLinkId] = callback(featureId); navLinks[app] = callback(featureId);
} });
} }
return navLinks; return navLinks;

View file

@ -40,7 +40,7 @@ export class FeaturesService {
(acc: Features, feature: any) => ({ (acc: Features, feature: any) => ({
...acc, ...acc,
[feature.id]: { [feature.id]: {
navLinkId: feature.navLinkId, app: feature.app,
}, },
}), }),
{} {}

View file

@ -52,7 +52,7 @@ export class UICapabilitiesService {
}): Promise<GetUICapabilitiesResult> { }): Promise<GetUICapabilitiesResult> {
const features = await this.featureService.get(); const features = await this.featureService.get();
const applications = Object.values(features) const applications = Object.values(features)
.map((feature) => feature.navLinkId) .flatMap((feature) => feature.app)
.filter((link) => !!link); .filter((link) => !!link);
const spaceUrlPrefix = spaceId ? `/s/${spaceId}` : ''; const spaceUrlPrefix = spaceId ? `/s/${spaceId}` : '';

View file

@ -57,7 +57,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) {
expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.success).to.be(true);
expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql( expect(uiCapabilities.value!.navLinks).to.eql(
navLinksBuilder.only('management', 'foo') navLinksBuilder.only('management', 'foo', 'kibana')
); );
break; break;
case 'legacy_all': case 'legacy_all':