diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 306c42301e43a..ed0f3595cb94c 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -96,7 +96,7 @@ export function getPluginPrivileges() { ]; const privilege = { app: [PLUGIN_ID, 'kibana'], - excludeFromBasePrivileges: true, + excludeFromBasePrivileges: false, management: { insightsAndAlerting: ['jobsListLink'], }, diff --git a/x-pack/test/functional/apps/management/feature_controls/management_security.ts b/x-pack/test/functional/apps/management/feature_controls/management_security.ts index 66dc697804ce2..8235bf6e1e9e2 100644 --- a/x-pack/test/functional/apps/management/feature_controls/management_security.ts +++ b/x-pack/test/functional/apps/management/feature_controls/management_security.ts @@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(sections).to.have.length(2); expect(sections[0]).to.eql({ sectionId: 'insightsAndAlerting', - sectionLinks: ['triggersActions'], + sectionLinks: ['triggersActions', 'jobsListLink'], }); expect(sections[1]).to.eql({ sectionId: 'kibana', diff --git a/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts b/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts index d3833552a062d..63912b7af5557 100644 --- a/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/ml/feature_controls/ml_security.ts @@ -77,9 +77,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await security.user.delete('global_all'); }); - it(`doesn't show ml navlink`, async () => { + it(`shows ml navlink`, async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).not.to.contain('Machine Learning'); + expect(navLinks).to.contain('Machine Learning'); }); }); @@ -103,5 +103,75 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.contain('Machine Learning'); }); }); + + describe('ml read', () => { + before(async () => { + await security.role.create('ml_role_read', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + base: [], + feature: { ml: ['read'], savedObjectsManagement: ['read'] }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('ml_read_user', { + password: 'ml_read-password', + roles: ['ml_role_read'], + full_name: 'ml read', + }); + + await PageObjects.security.login('ml_read_user', 'ml_read-password'); + }); + + after(async () => { + await security.role.delete('ml_role_read'); + await security.user.delete('ml_read_user'); + }); + + it('shows ML navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map((link) => link.text); + expect(navLinks).to.contain('Machine Learning'); + }); + }); + + describe('ml none', () => { + before(async () => { + await security.role.create('ml_role_none', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + base: [], + feature: { discover: ['read'] }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('ml_none_user', { + password: 'ml_none-password', + roles: ['ml_role_none'], + full_name: 'ml none', + }); + + await PageObjects.security.login('ml_none_user', 'ml_none-password'); + }); + + after(async () => { + await security.role.delete('ml_role_none'); + await security.user.delete('ml_none_user'); + }); + + it('does NOT show ML navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map((link) => link.text); + expect(navLinks).to.not.contain('Machine Learning'); + }); + }); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts index 431c0550b9271..33ec80f16225e 100644 --- a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -13,10 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'error']); const ml = getService('ml'); - const testUsers = [ - { user: USER.ML_UNAUTHORIZED, discoverAvailable: true }, - { user: USER.ML_UNAUTHORIZED_SPACES, discoverAvailable: true }, - ]; + const testUsers = [{ user: USER.ML_UNAUTHORIZED, discoverAvailable: true }]; describe('for user with no ML access', function () { this.tags(['skipFirefox', 'mlqa']); diff --git a/x-pack/test/functional/services/ml/security_common.ts b/x-pack/test/functional/services/ml/security_common.ts index 847730ca73548..54d2fa48a826f 100644 --- a/x-pack/test/functional/services/ml/security_common.ts +++ b/x-pack/test/functional/services/ml/security_common.ts @@ -21,7 +21,6 @@ export enum USER { ML_VIEWER_SPACE1 = 'ft_ml_viewer_space1', ML_VIEWER_ALL_SPACES = 'ft_ml_viewer_all_spaces', ML_UNAUTHORIZED = 'ft_ml_unauthorized', - ML_UNAUTHORIZED_SPACES = 'ft_ml_unauthorized_spaces', } export function MachineLearningSecurityCommonProvider({ getService }: FtrProviderContext) { @@ -90,8 +89,7 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [ { - base: [], - feature: { ml: ['all'], savedObjectsManagement: ['all'] }, + base: ['all'], spaces: ['*'], }, ], @@ -123,8 +121,7 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [ { - base: [], - feature: { ml: ['read'], savedObjectsManagement: ['read'] }, + base: ['read'], spaces: ['*'], }, ], @@ -134,6 +131,31 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide elasticsearch: { cluster: [], indices: [], run_as: [] }, kibana: [{ base: [], feature: { discover: ['read'] }, spaces: ['default'] }], }, + { + name: 'ft_all_space_ml_none', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + base: [], + // This role is intended to be used by the "ft_ml_poweruser" and "ft_ml_viewer" users; they should have access to ML by virtue of + // the "machine_learning_admin" and "machine_learning_user" roles. However, a user needs _at least_ one Kibana privilege to log + // into Kibana. This role allows these users to log in, but explicitly omits ML from the feature privileges. + // In addition: several functional tests that use these users also rely on UI elements that are enabled by other Kibana features, + // such as "View in Lens", "Add to Dashboard", and creating anomaly detection rules. These feature privileges are the minimal ones + // necessary to satisfy all of those functional tests. + feature: { + discover: ['read'], + visualize: ['read'], + dashboard: ['all'], + actions: ['all'], + savedObjectsManagement: ['all'], + advancedSettings: ['all'], + indexPatterns: ['all'], + }, + spaces: ['*'], + }, + ], + }, ]; const users = [ @@ -142,7 +164,7 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide full_name: 'ML Poweruser', password: 'mlp001', roles: [ - 'kibana_admin', + 'ft_all_space_ml_none', 'machine_learning_admin', 'ft_ml_source', 'ft_ml_dest', @@ -172,7 +194,7 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide full_name: 'ML Viewer', password: 'mlv001', roles: [ - 'kibana_admin', + 'ft_all_space_ml_none', 'machine_learning_user', 'ft_ml_source_readonly', 'ft_ml_dest_readonly', @@ -200,12 +222,6 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide name: 'ft_ml_unauthorized', full_name: 'ML Unauthorized', password: 'mlu001', - roles: ['kibana_admin', 'ft_ml_source_readonly'], - }, - { - name: 'ft_ml_unauthorized_spaces', - full_name: 'ML Unauthorized', - password: 'mlus001', roles: ['ft_default_space_ml_none', 'ft_ml_source_readonly'], }, ]; diff --git a/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts index 8d3aa3c6b6ada..12fc7b8122c99 100644 --- a/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional_basic/apps/ml/permissions/no_ml_access.ts @@ -13,10 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'error']); const ml = getService('ml'); - const testUsers = [ - { user: USER.ML_UNAUTHORIZED, discoverAvailable: true }, - { user: USER.ML_UNAUTHORIZED_SPACES, discoverAvailable: true }, - ]; + const testUsers = [{ user: USER.ML_UNAUTHORIZED, discoverAvailable: true }]; describe('for user with no ML access', function () { for (const testUser of testUsers) { diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index 3d272977be625..7347f201807ab 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -46,12 +46,10 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'dual_privileges_all at everything_space': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml, monitoring, and ES features are enabled + // everything except monitoring, and ES features are enabled const expected = mapValues( uiCapabilities.value!.catalogue, (enabled, catalogueId) => - catalogueId !== 'ml' && - catalogueId !== 'ml_file_data_visualizer' && catalogueId !== 'monitoring' && catalogueId !== 'osquery' && !esFeatureExceptions.includes(catalogueId) @@ -59,16 +57,35 @@ export default function catalogueTests({ getService }: FtrProviderContext) { expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } - case 'everything_space_all at everything_space': + case 'everything_space_all at everything_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything except spaces, monitoring, the enterprise search suite, and ES features are enabled + // (easier to say: all "proper" Kibana features are enabled) + const exceptions = [ + 'monitoring', + 'enterpriseSearch', + 'appSearch', + 'workplaceSearch', + 'spaces', + 'osquery', + ...esFeatureExceptions, + ]; + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => !exceptions.includes(catalogueId) + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } case 'global_read at everything_space': case 'dual_privileges_read at everything_space': case 'everything_space_read at everything_space': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except spaces, ml, monitoring, the enterprise search suite, and ES features are enabled + // everything except spaces, ml_file_data_visualizer, monitoring, the enterprise search suite, and ES features are enabled // (easier to say: all "proper" Kibana features are enabled) const exceptions = [ - 'ml', 'ml_file_data_visualizer', 'monitoring', 'enterpriseSearch', diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index 5712cfeb8c141..479e3090151b9 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -42,7 +42,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.except('ml', 'monitoring', 'osquery') + navLinksBuilder.except('monitoring', 'osquery') ); break; case 'everything_space_all at everything_space': @@ -53,7 +53,6 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( navLinksBuilder.except( - 'ml', 'monitoring', 'enterpriseSearch', 'appSearch',