Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { createAction } from 'redux-actions';
import { createAsyncAction } from './utils';
import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges';
import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities';
import { AnomaliesTableRecord } from '../../../../../../plugins/ml/common/types/anomalies';
import {
CreateMLJobSuccess,
Expand All @@ -27,7 +27,7 @@ export const createMLJobAction = createAsyncAction<
CreateMLJobSuccess | null
>('CREATE_ML_JOB');

export const getMLCapabilitiesAction = createAsyncAction<any, PrivilegesResponse>(
export const getMLCapabilitiesAction = createAsyncAction<any, MlCapabilitiesResponse>(
'GET_ML_CAPABILITIES'
);

Expand Down
4 changes: 2 additions & 2 deletions x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import moment from 'moment';
import { apiService } from './utils';
import { AnomalyRecords, AnomalyRecordsParams } from '../actions';
import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants';
import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges';
import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities';
import {
CreateMLJobSuccess,
DeleteJobResults,
Expand All @@ -20,7 +20,7 @@ import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_r

export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`.toLowerCase();

export const getMLCapabilities = async (): Promise<PrivilegesResponse> => {
export const getMLCapabilities = async (): Promise<MlCapabilitiesResponse> => {
return await apiService.get(API_URLS.ML_CAPABILITIES);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { getAsyncInitialState, handleAsyncAction } from './utils';
import { IHttpFetchError } from '../../../../../../../target/types/core/public/http';
import { AsyncInitialState } from './types';
import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges';
import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities';
import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types';
import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer';

Expand All @@ -26,7 +26,7 @@ export interface MLJobState {
createJob: AsyncInitialState<CreateMLJobSuccess>;
deleteJob: AsyncInitialState<DeleteJobResults>;
anomalies: AsyncInitialState<AnomalyRecords>;
mlCapabilities: AsyncInitialState<PrivilegesResponse>;
mlCapabilities: AsyncInitialState<MlCapabilitiesResponse>;
}

const initialState: MLJobState = {
Expand Down
9 changes: 8 additions & 1 deletion x-pack/plugins/ml/common/license/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/

export { MlLicense, LicenseStatus, MINIMUM_FULL_LICENSE, MINIMUM_LICENSE } from './ml_license';
export {
MlLicense,
LicenseStatus,
MINIMUM_FULL_LICENSE,
MINIMUM_LICENSE,
isFullLicense,
isMinimumLicense,
} from './ml_license';
12 changes: 10 additions & 2 deletions x-pack/plugins/ml/common/license/ml_license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export class MlLicense {
this._isSecurityEnabled = securityIsEnabled;
this._hasLicenseExpired = this._license.status === 'expired';
this._isMlEnabled = this._license.getFeature(PLUGIN_ID).isEnabled;
this._isMinimumLicense = this._license.check(PLUGIN_ID, MINIMUM_LICENSE).state === 'valid';
this._isFullLicense = this._license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid';
this._isMinimumLicense = isMinimumLicense(this._license);
this._isFullLicense = isFullLicense(this._license);

if (this._initialized === false && postInitFunctions !== undefined) {
postInitFunctions.forEach(f => f(this));
Expand Down Expand Up @@ -74,3 +74,11 @@ export class MlLicense {
return this._isFullLicense;
}
}

export function isFullLicense(license: ILicense) {
return license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid';
}

export function isMinimumLicense(license: ILicense) {
return license.check(PLUGIN_ID, MINIMUM_LICENSE).state === 'valid';
}
66 changes: 66 additions & 0 deletions x-pack/plugins/ml/common/types/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { KibanaRequest } from 'kibana/server';

export const userMlCapabilities = {
// Anomaly Detection
canGetJobs: false,
canGetDatafeeds: false,
// Calendars
canGetCalendars: false,
// File Data Visualizer
canFindFileStructure: false,
// Filters
canGetFilters: false,
// Data Frame Analytics
canGetDataFrameAnalytics: false,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I log in as a user with the machine_learning_user role. the clone item is showing for data frame analytics jobs:

image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bug in master. I think it is outside the scope of this PR to fix here.
I've raised #64235 to cover it.

};

export const adminMlCapabilities = {
// Anomaly Detection
canCreateJob: false,
canDeleteJob: false,
canOpenJob: false,
canCloseJob: false,
canForecastJob: false,
canStartStopDatafeed: false,
canUpdateJob: false,
canUpdateDatafeed: false,
canPreviewDatafeed: false,
// Calendars
canCreateCalendar: false,
canDeleteCalendar: false,
// Filters
canCreateFilter: false,
canDeleteFilter: false,
// Data Frame Analytics
canDeleteDataFrameAnalytics: false,
canCreateDataFrameAnalytics: false,
canStartStopDataFrameAnalytics: false,
};

export type UserMlCapabilities = typeof userMlCapabilities;
export type AdminMlCapabilities = typeof adminMlCapabilities;
export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities;

export const basicLicenseMlCapabilities = ['canFindFileStructure'] as Array<keyof MlCapabilities>;

export function getDefaultCapabilities(): MlCapabilities {
return {
...userMlCapabilities,
...adminMlCapabilities,
};
}

export interface MlCapabilitiesResponse {
capabilities: MlCapabilities;
upgradeInProgress: boolean;
isPlatinumOrTrialLicense: boolean;
mlFeatureEnabledInSpace: boolean;
}

export type ResolveMlCapabilities = (request: KibanaRequest) => Promise<MlCapabilities | null>;
75 changes: 0 additions & 75 deletions x-pack/plugins/ml/common/types/privileges.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import { i18n } from '@kbn/i18n';

import { hasLicenseExpired } from '../license';

import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges';
import { getPrivileges, getManageMlPrivileges } from './get_privileges';
import { MlCapabilities, getDefaultCapabilities } from '../../../common/types/capabilities';
import { getCapabilities, getManageMlCapabilities } from './get_capabilities';
import { ACCESS_DENIED_PATH } from '../management/management_urls';

let privileges: Privileges = getDefaultPrivileges();
// manage_ml requires all monitor and admin cluster privileges: https://github.com/elastic/elasticsearch/blob/664a29c8905d8ce9ba8c18aa1ed5c5de93a0eabc/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java#L53
export function checkGetManagementMlJobs() {
let _capabilities: MlCapabilities = getDefaultCapabilities();

export function checkGetManagementMlJobsResolver() {
return new Promise<{ mlFeatureEnabledInSpace: boolean }>((resolve, reject) => {
getManageMlPrivileges().then(
getManageMlCapabilities().then(
({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => {
privileges = capabilities;
// Loop through all privileges to ensure they are all set to true.
const isManageML = Object.values(privileges).every(p => p === true);
_capabilities = capabilities;
// Loop through all capabilities to ensure they are all set to true.
const isManageML = Object.values(_capabilities).every(p => p === true);

if (isManageML === true && isPlatinumOrTrialLicense === true) {
return resolve({ mlFeatureEnabledInSpace });
Expand All @@ -33,17 +33,17 @@ export function checkGetManagementMlJobs() {
});
}

export function checkGetJobsPrivilege(): Promise<Privileges> {
export function checkGetJobsCapabilitiesResolver(): Promise<MlCapabilities> {
return new Promise((resolve, reject) => {
getPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => {
privileges = capabilities;
getCapabilities().then(({ capabilities, isPlatinumOrTrialLicense }) => {
_capabilities = capabilities;
// the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list.
// all other functionality is controlled by the return privileges object.
// all other functionality is controlled by the return capabilities object.
// if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect,
// allow the promise to resolve as the separate license check will redirect then user to
// a basic feature
if (privileges.canGetJobs || isPlatinumOrTrialLicense === false) {
return resolve(privileges);
if (_capabilities.canGetJobs || isPlatinumOrTrialLicense === false) {
return resolve(_capabilities);
} else {
window.location.href = '#/access-denied';
return reject();
Expand All @@ -52,15 +52,15 @@ export function checkGetJobsPrivilege(): Promise<Privileges> {
});
}

export function checkCreateJobsPrivilege(): Promise<Privileges> {
export function checkCreateJobsCapabilitiesResolver(): Promise<MlCapabilities> {
return new Promise((resolve, reject) => {
getPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => {
privileges = capabilities;
getCapabilities().then(({ capabilities, isPlatinumOrTrialLicense }) => {
_capabilities = capabilities;
// if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect,
// allow the promise to resolve as the separate license check will redirect then user to
// a basic feature
if (privileges.canCreateJob || isPlatinumOrTrialLicense === false) {
return resolve(privileges);
if (_capabilities.canCreateJob || isPlatinumOrTrialLicense === false) {
return resolve(_capabilities);
} else {
// if the user has no permission to create a job,
// redirect them back to the Transforms Management page
Expand All @@ -71,14 +71,14 @@ export function checkCreateJobsPrivilege(): Promise<Privileges> {
});
}

export function checkFindFileStructurePrivilege(): Promise<Privileges> {
export function checkFindFileStructurePrivilegeResolver(): Promise<MlCapabilities> {
return new Promise((resolve, reject) => {
getPrivileges().then(({ capabilities }) => {
privileges = capabilities;
getCapabilities().then(({ capabilities }) => {
_capabilities = capabilities;
// the minimum privilege for using ML with a basic license is being able to use the datavisualizer.
// all other functionality is controlled by the return privileges object
if (privileges.canFindFileStructure) {
return resolve(privileges);
// all other functionality is controlled by the return _capabilities object
if (_capabilities.canFindFileStructure) {
return resolve(_capabilities);
} else {
window.location.href = '#/access-denied';
return reject();
Expand All @@ -89,14 +89,14 @@ export function checkFindFileStructurePrivilege(): Promise<Privileges> {

// check the privilege type and the license to see whether a user has permission to access a feature.
// takes the name of the privilege variable as specified in get_privileges.js
export function checkPermission(privilegeType: keyof Privileges) {
export function checkPermission(capability: keyof MlCapabilities) {
const licenseHasExpired = hasLicenseExpired();
return privileges[privilegeType] === true && licenseHasExpired !== true;
return _capabilities[capability] === true && licenseHasExpired !== true;
}

// create the text for the button's tooltips if the user's license has
// expired or if they don't have the privilege to press that button
export function createPermissionFailureMessage(privilegeType: keyof Privileges) {
export function createPermissionFailureMessage(privilegeType: keyof MlCapabilities) {
let message = '';
const licenseHasExpired = hasLicenseExpired();
if (licenseHasExpired) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
import { ml } from '../services/ml_api_service';

import { setUpgradeInProgress } from '../services/upgrade_service';
import { PrivilegesResponse } from '../../../common/types/privileges';
import { MlCapabilitiesResponse } from '../../../common/types/capabilities';

export function getPrivileges(): Promise<PrivilegesResponse> {
export function getCapabilities(): Promise<MlCapabilitiesResponse> {
return new Promise((resolve, reject) => {
ml.checkMlPrivileges()
.then((resp: PrivilegesResponse) => {
ml.checkMlCapabilities()
.then((resp: MlCapabilitiesResponse) => {
if (resp.upgradeInProgress === true) {
setUpgradeInProgress(true);
}
Expand All @@ -24,10 +24,10 @@ export function getPrivileges(): Promise<PrivilegesResponse> {
});
}

export function getManageMlPrivileges(): Promise<PrivilegesResponse> {
export function getManageMlCapabilities(): Promise<MlCapabilitiesResponse> {
return new Promise((resolve, reject) => {
ml.checkManageMLPrivileges()
.then((resp: PrivilegesResponse) => {
ml.checkManageMLCapabilities()
.then((resp: MlCapabilitiesResponse) => {
if (resp.upgradeInProgress === true) {
setUpgradeInProgress(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import React from 'react';
import mockAnomaliesTableData from '../../explorer/__mocks__/mock_anomalies_table_data.json';
import { getColumns } from './anomalies_table_columns';

jest.mock('../../privilege/check_privilege', () => ({
jest.mock('../../capabilities/check_capabilities', () => ({
checkPermission: () => false,
}));
jest.mock('../../license', () => ({
hasLicenseExpired: () => false,
}));
jest.mock('../../privilege/get_privileges', () => ({
getPrivileges: () => {},
jest.mock('../../capabilities/get_capabilities', () => ({
getCapabilities: () => {},
}));
jest.mock('../../services/field_format_service', () => ({
getFieldFormat: () => {},
Expand Down
Loading