Skip to content

Commit

Permalink
[ML] Adding capabilities checks to shared functions (#70069) (#70479)
Browse files Browse the repository at this point in the history
* [ML] Adding capabilities checks to shared functions

* small refactor

* disabling capabilities checks for functions called by SIEM alerting

* testing git

* removing comment

* using constant for ml app id

* tiny type clean up

* removing check in ml_capabilities

* fixing types

* removing capabilities checks from ml_capabilities endpoint

* updating types

* better error handling

* improving capabilities check

* adding custom errors

Co-authored-by: Elastic Machine <[email protected]>

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
jgowdyelastic and elasticmachine authored Jul 1, 2020
1 parent d22fd25 commit 4acd700
Show file tree
Hide file tree
Showing 20 changed files with 229 additions and 137 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/apm/server/lib/helpers/setup_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ function getMlSetup(context: APMRequestHandlerContext, request: KibanaRequest) {
const mlClient = ml.mlClient.asScoped(request).callAsCurrentUser;
return {
mlSystem: ml.mlSystemProvider(mlClient, request),
anomalyDetectors: ml.anomalyDetectorsProvider(mlClient),
anomalyDetectors: ml.anomalyDetectorsProvider(mlClient, request),
mlClient,
};
}
2 changes: 1 addition & 1 deletion x-pack/plugins/infra/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export class InfraServerPlugin {
plugins.ml?.mlSystemProvider(context.ml?.mlClient.callAsCurrentUser, request);
const mlAnomalyDetectors =
context.ml &&
plugins.ml?.anomalyDetectorsProvider(context.ml?.mlClient.callAsCurrentUser);
plugins.ml?.anomalyDetectorsProvider(context.ml?.mlClient.callAsCurrentUser, request);
const spaceId = plugins.spaces?.spacesService.getSpaceId(request) || 'default';

return {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/ml/common/types/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const adminMlCapabilities = {
export type UserMlCapabilities = typeof userMlCapabilities;
export type AdminMlCapabilities = typeof adminMlCapabilities;
export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities;
export type MlCapabilitiesKey = keyof MlCapabilities;

export const basicLicenseMlCapabilities = ['canAccessML', 'canFindFileStructure'] as Array<
keyof MlCapabilities
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { LegacyAPICaller } from 'kibana/server';
import { getAdminCapabilities, getUserCapabilities } from './__mocks__/ml_capabilities';
import { capabilitiesProvider } from './check_capabilities';
import { MlLicense } from '../../../common/license';
Expand All @@ -22,8 +23,12 @@ const mlLicenseBasic = {
const mlIsEnabled = async () => true;
const mlIsNotEnabled = async () => false;

const callWithRequestNonUpgrade = async () => ({ upgrade_mode: false });
const callWithRequestUpgrade = async () => ({ upgrade_mode: true });
const callWithRequestNonUpgrade = ((async () => ({
upgrade_mode: false,
})) as unknown) as LegacyAPICaller;
const callWithRequestUpgrade = ((async () => ({
upgrade_mode: true,
})) as unknown) as LegacyAPICaller;

describe('check_capabilities', () => {
describe('getCapabilities() - right number of capabilities', () => {
Expand Down
36 changes: 34 additions & 2 deletions x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { ILegacyScopedClusterClient } from 'kibana/server';
import { LegacyAPICaller, KibanaRequest } from 'kibana/server';
import { mlLog } from '../../client/log';
import {
MlCapabilities,
adminMlCapabilities,
MlCapabilitiesResponse,
ResolveMlCapabilities,
MlCapabilitiesKey,
} from '../../../common/types/capabilities';
import { upgradeCheckProvider } from './upgrade';
import { MlLicense } from '../../../common/license';
import {
InsufficientMLCapabilities,
UnknownMLCapabilitiesError,
MLPrivilegesUninitialized,
} from './errors';

export function capabilitiesProvider(
callAsCurrentUser: ILegacyScopedClusterClient['callAsCurrentUser'],
callAsCurrentUser: LegacyAPICaller,
capabilities: MlCapabilities,
mlLicense: MlLicense,
isMlEnabledInSpace: () => Promise<boolean>
Expand Down Expand Up @@ -47,3 +55,27 @@ function disableAdminPrivileges(capabilities: MlCapabilities) {
capabilities.canCreateAnnotation = false;
capabilities.canDeleteAnnotation = false;
}

export type HasMlCapabilities = (capabilities: MlCapabilitiesKey[]) => Promise<void>;

export function hasMlCapabilitiesProvider(resolveMlCapabilities: ResolveMlCapabilities) {
return (request: KibanaRequest): HasMlCapabilities => {
let mlCapabilities: MlCapabilities | null = null;
return async (capabilities: MlCapabilitiesKey[]) => {
try {
mlCapabilities = await resolveMlCapabilities(request);
} catch (e) {
mlLog.error(e);
throw new UnknownMLCapabilitiesError(`Unable to perform ML capabilities check ${e}`);
}

if (mlCapabilities === null) {
throw new MLPrivilegesUninitialized('ML capabilities have not been initialized');
}

if (capabilities.every((c) => mlCapabilities![c] === true) === false) {
throw new InsufficientMLCapabilities('Insufficient privileges to access feature');
}
};
};
}
28 changes: 28 additions & 0 deletions x-pack/plugins/ml/server/lib/capabilities/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.
*/

/* eslint-disable max-classes-per-file */

export class UnknownMLCapabilitiesError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}

export class InsufficientMLCapabilities extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}

export class MLPrivilegesUninitialized extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
6 changes: 5 additions & 1 deletion x-pack/plugins/ml/server/lib/capabilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/

export { capabilitiesProvider } from './check_capabilities';
export {
capabilitiesProvider,
hasMlCapabilitiesProvider,
HasMlCapabilities,
} from './check_capabilities';
export { setupCapabilitiesSwitcher } from './capabilities_switcher';
6 changes: 2 additions & 4 deletions x-pack/plugins/ml/server/lib/capabilities/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { ILegacyScopedClusterClient } from 'kibana/server';
import { LegacyAPICaller } from 'kibana/server';
import { mlLog } from '../../client/log';

export function upgradeCheckProvider(
callAsCurrentUser: ILegacyScopedClusterClient['callAsCurrentUser']
) {
export function upgradeCheckProvider(callAsCurrentUser: LegacyAPICaller) {
async function isUpgradeInProgress(): Promise<boolean> {
let upgradeInProgress = false;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { LegacyCallAPIOptions, ILegacyScopedClusterClient } from 'kibana/server';
import { LegacyCallAPIOptions, LegacyAPICaller } from 'kibana/server';
import _ from 'lodash';
import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types';
import { getSafeAggregationName } from '../../../common/util/job_utils';
Expand Down Expand Up @@ -113,7 +113,7 @@ export class DataVisualizer {
options?: LegacyCallAPIOptions
) => Promise<any>;

constructor(callAsCurrentUser: ILegacyScopedClusterClient['callAsCurrentUser']) {
constructor(callAsCurrentUser: LegacyAPICaller) {
this.callAsCurrentUser = callAsCurrentUser;
}

Expand Down
25 changes: 12 additions & 13 deletions x-pack/plugins/ml/server/models/filter/filter_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import Boom from 'boom';
import { ILegacyScopedClusterClient } from 'kibana/server';
import { LegacyAPICaller } from 'kibana/server';

import { DetectorRule, DetectorRuleScope } from '../../../common/types/detector_rules';

Expand Down Expand Up @@ -58,18 +58,14 @@ interface PartialJob {
}

export class FilterManager {
private _client: ILegacyScopedClusterClient['callAsCurrentUser'];

constructor(client: ILegacyScopedClusterClient['callAsCurrentUser']) {
this._client = client;
}
constructor(private callAsCurrentUser: LegacyAPICaller) {}

async getFilter(filterId: string) {
try {
const [JOBS, FILTERS] = [0, 1];
const results = await Promise.all([
this._client('ml.jobs'),
this._client('ml.filters', { filterId }),
this.callAsCurrentUser('ml.jobs'),
this.callAsCurrentUser('ml.filters', { filterId }),
]);

if (results[FILTERS] && results[FILTERS].filters.length) {
Expand All @@ -91,7 +87,7 @@ export class FilterManager {

async getAllFilters() {
try {
const filtersResp = await this._client('ml.filters');
const filtersResp = await this.callAsCurrentUser('ml.filters');
return filtersResp.filters;
} catch (error) {
throw Boom.badRequest(error);
Expand All @@ -101,7 +97,10 @@ export class FilterManager {
async getAllFilterStats() {
try {
const [JOBS, FILTERS] = [0, 1];
const results = await Promise.all([this._client('ml.jobs'), this._client('ml.filters')]);
const results = await Promise.all([
this.callAsCurrentUser('ml.jobs'),
this.callAsCurrentUser('ml.filters'),
]);

// Build a map of filter_ids against jobs and detectors using that filter.
let filtersInUse: FiltersInUse = {};
Expand Down Expand Up @@ -138,7 +137,7 @@ export class FilterManager {
delete filter.filterId;
try {
// Returns the newly created filter.
return await this._client('ml.addFilter', { filterId, body: filter });
return await this.callAsCurrentUser('ml.addFilter', { filterId, body: filter });
} catch (error) {
throw Boom.badRequest(error);
}
Expand All @@ -158,7 +157,7 @@ export class FilterManager {
}

// Returns the newly updated filter.
return await this._client('ml.updateFilter', {
return await this.callAsCurrentUser('ml.updateFilter', {
filterId,
body,
});
Expand All @@ -168,7 +167,7 @@ export class FilterManager {
}

async deleteFilter(filterId: string) {
return this._client('ml.deleteFilter', { filterId });
return this.callAsCurrentUser('ml.deleteFilter', { filterId });
}

buildFiltersInUse(jobsList: PartialJob[]) {
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/ml/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import { registerKibanaSettings } from './lib/register_settings';

declare module 'kibana/server' {
interface RequestHandlerContext {
ml?: {
[PLUGIN_ID]?: {
mlClient: ILegacyScopedClusterClient;
};
}
Expand Down
3 changes: 0 additions & 3 deletions x-pack/plugins/ml/server/routes/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,6 @@ export function systemRoutes(
{
path: '/api/ml/ml_capabilities',
validate: false,
options: {
tags: ['access:ml:canAccessML'],
},
},
mlLicense.basicLicenseAPIGuard(async (context, request, response) => {
try {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/ml/server/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@

export * from '../common/types/anomalies';
export * from '../common/types/anomaly_detection_jobs';
export * from './lib/capabilities/errors';
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,30 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { LegacyAPICaller } from 'kibana/server';
import { LicenseCheck } from '../license_checks';
import { LegacyAPICaller, KibanaRequest } from 'kibana/server';
import { Job } from '../../../common/types/anomaly_detection_jobs';
import { SharedServicesChecks } from '../shared_services';

export interface AnomalyDetectorsProvider {
anomalyDetectorsProvider(
callAsCurrentUser: LegacyAPICaller
callAsCurrentUser: LegacyAPICaller,
request: KibanaRequest
): {
jobs(jobId?: string): Promise<{ count: number; jobs: Job[] }>;
};
}

export function getAnomalyDetectorsProvider(isFullLicense: LicenseCheck): AnomalyDetectorsProvider {
export function getAnomalyDetectorsProvider({
isFullLicense,
getHasMlCapabilities,
}: SharedServicesChecks): AnomalyDetectorsProvider {
return {
anomalyDetectorsProvider(callAsCurrentUser: LegacyAPICaller) {
anomalyDetectorsProvider(callAsCurrentUser: LegacyAPICaller, request: KibanaRequest) {
const hasMlCapabilities = getHasMlCapabilities(request);
return {
jobs(jobId?: string) {
async jobs(jobId?: string) {
isFullLicense();
await hasMlCapabilities(['canGetJobs']);
return callAsCurrentUser('ml.jobs', jobId !== undefined ? { jobId } : {});
},
};
Expand Down
35 changes: 28 additions & 7 deletions x-pack/plugins/ml/server/shared_services/providers/job_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,40 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { LegacyAPICaller } from 'kibana/server';
import { LicenseCheck } from '../license_checks';
import { LegacyAPICaller, KibanaRequest } from 'kibana/server';
import { jobServiceProvider } from '../../models/job_service';
import { SharedServicesChecks } from '../shared_services';

type OrigJobServiceProvider = ReturnType<typeof jobServiceProvider>;

export interface JobServiceProvider {
jobServiceProvider(callAsCurrentUser: LegacyAPICaller): ReturnType<typeof jobServiceProvider>;
jobServiceProvider(
callAsCurrentUser: LegacyAPICaller,
request: KibanaRequest
): {
jobsSummary: OrigJobServiceProvider['jobsSummary'];
};
}

export function getJobServiceProvider(isFullLicense: LicenseCheck): JobServiceProvider {
export function getJobServiceProvider({
isFullLicense,
getHasMlCapabilities,
}: SharedServicesChecks): JobServiceProvider {
return {
jobServiceProvider(callAsCurrentUser: LegacyAPICaller) {
isFullLicense();
return jobServiceProvider(callAsCurrentUser);
jobServiceProvider(callAsCurrentUser: LegacyAPICaller, request: KibanaRequest) {
// const hasMlCapabilities = getHasMlCapabilities(request);
const { jobsSummary } = jobServiceProvider(callAsCurrentUser);
return {
async jobsSummary(...args) {
isFullLicense();
// Removed while https://github.com/elastic/kibana/issues/64588 exists.
// SIEM are calling this endpoint with a dummy request object from their alerting
// integration and currently alerting does not supply a request object.
// await hasMlCapabilities(['canGetJobs']);

return jobsSummary(...args);
},
};
},
};
}
Loading

0 comments on commit 4acd700

Please sign in to comment.