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

View file

@ -7,6 +7,10 @@
import { KibanaRequest } from 'kibana/server';
import { PLUGIN_ID } from '../constants/app';
export const apmUserMlCapabilities = {
canGetJobs: false,
};
export const userMlCapabilities = {
canAccessML: false,
// Anomaly Detection
@ -68,6 +72,7 @@ export function getDefaultCapabilities(): MlCapabilities {
}
export function getPluginPrivileges() {
const apmUserMlCapabilitiesKeys = Object.keys(apmUserMlCapabilities);
const userMlCapabilitiesKeys = Object.keys(userMlCapabilities);
const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities);
const allMlCapabilitiesKeys = [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys];
@ -101,6 +106,17 @@ export function getPluginPrivileges() {
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 {
const { admin, user } = getPluginPrivileges();
const { admin, user, apmUser } = getPluginPrivileges();
plugins.features.registerFeature({
id: PLUGIN_ID,
@ -108,6 +108,10 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug
id: 'ml_admin',
privilege: admin,
},
{
id: 'ml_apm_user',
privilege: apmUser,
},
],
},
});

View file

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

View file

@ -18,12 +18,11 @@ export function disableUICapabilitiesFactory(
logger: Logger,
authz: AuthorizationServiceSetup
) {
// nav links are sourced from two places:
// 1) The `navLinkId` property. This is deprecated and will be removed (https://github.com/elastic/kibana/issues/66217)
// 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 above.
// nav links are sourced from 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.
const featureNavLinkIds = features
.flatMap((feature) => [feature.navLinkId, ...feature.app])
.flatMap((feature) => feature.app)
.filter((navLinkId) => navLinkId != null);
const shouldDisableFeatureUICapability = (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -52,7 +52,7 @@ export class UICapabilitiesService {
}): Promise<GetUICapabilitiesResult> {
const features = await this.featureService.get();
const applications = Object.values(features)
.map((feature) => feature.navLinkId)
.flatMap((feature) => feature.app)
.filter((link) => !!link);
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.value).to.have.property('navLinks');
expect(uiCapabilities.value!.navLinks).to.eql(
navLinksBuilder.only('management', 'foo')
navLinksBuilder.only('management', 'foo', 'kibana')
);
break;
case 'legacy_all':