Skip to content

Commit

Permalink
[ML] Space permision checks for job deletion (#83871)
Browse files Browse the repository at this point in the history
* [ML] Space permision checks for job deletion

* updating spaces dependency

* updating endpoint comments

* adding delete job capabilities check

* small change based on review

* improving permissions checks

* renaming function and endpoint

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
jgowdyelastic and kibanamachine committed Nov 24, 2020
1 parent e892b03 commit 24f262b
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 15 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/ml/common/types/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export function getPluginPrivileges() {
catalogue: [],
savedObject: {
all: [],
read: ['ml-job'],
read: [ML_SAVED_OBJECT_TYPE],
},
api: apmUserMlCapabilitiesKeys.map((k) => `ml:${k}`),
ui: apmUserMlCapabilitiesKeys,
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/ml/common/types/saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ export interface InitializeSavedObjectResponse {
success: boolean;
error?: any;
}

export interface DeleteJobCheckResponse {
[jobId: string]: DeleteJobPermission;
}

export interface DeleteJobPermission {
canDelete: boolean;
canUntag: boolean;
}
24 changes: 20 additions & 4 deletions x-pack/plugins/ml/server/lib/spaces_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { Legacy } from 'kibana';
import { KibanaRequest } from '../../../../../src/core/server';
import { SpacesPluginStart } from '../../../spaces/server';
import { PLUGIN_ID } from '../../common/constants/app';

export type RequestFacade = KibanaRequest | Legacy.Request;

Expand All @@ -22,19 +23,34 @@ export function spacesUtilsProvider(
const space = await (await getSpacesPlugin()).spacesService.getActiveSpace(
request instanceof KibanaRequest ? request : KibanaRequest.from(request)
);
return space.disabledFeatures.includes('ml') === false;
return space.disabledFeatures.includes(PLUGIN_ID) === false;
}

async function getAllSpaces(): Promise<string[] | null> {
async function getAllSpaces() {
if (getSpacesPlugin === undefined) {
return null;
}
const client = (await getSpacesPlugin()).spacesService.createSpacesClient(
request instanceof KibanaRequest ? request : KibanaRequest.from(request)
);
const spaces = await client.getAll();
return await client.getAll();
}

async function getAllSpaceIds(): Promise<string[] | null> {
const spaces = await getAllSpaces();
if (spaces === null) {
return null;
}
return spaces.map((s) => s.id);
}

return { isMlEnabledInSpace, getAllSpaces };
async function getMlSpaceIds(): Promise<string[] | null> {
const spaces = await getAllSpaces();
if (spaces === null) {
return null;
}
return spaces.filter((s) => s.disabledFeatures.includes(PLUGIN_ID) === false).map((s) => s.id);
}

return { isMlEnabledInSpace, getAllSpaces, getAllSpaceIds, getMlSpaceIds };
}
Original file line number Diff line number Diff line change
Expand Up @@ -1095,7 +1095,9 @@ export class DataRecognizer {
job.config.analysis_limits.model_memory_limit = modelMemoryLimit;
}
} catch (error) {
mlLog.warn(`Data recognizer could not estimate model memory limit ${error.body}`);
mlLog.warn(
`Data recognizer could not estimate model memory limit ${JSON.stringify(error.body)}`
);
}
}

Expand Down
5 changes: 4 additions & 1 deletion x-pack/plugins/ml/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,10 @@ export class MlServerPlugin
notificationRoutes(routeInit);
resultsServiceRoutes(routeInit);
jobValidationRoutes(routeInit, this.version);
savedObjectsRoutes(routeInit);
savedObjectsRoutes(routeInit, {
getSpaces,
resolveMlCapabilities,
});
systemRoutes(routeInit, {
getSpaces,
cloud: plugins.cloud,
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/ml/server/routes/apidoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
"AssignJobsToSpaces",
"RemoveJobsFromSpaces",
"JobsSpaces",
"DeleteJobCheck",

"TrainedModels",
"GetTrainedModel",
Expand Down
56 changes: 53 additions & 3 deletions x-pack/plugins/ml/server/routes/saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
*/

import { wrapError } from '../client/error_wrapper';
import { RouteInitialization } from '../types';
import { RouteInitialization, SavedObjectsRouteDeps } from '../types';
import { checksFactory, repairFactory } from '../saved_objects';
import { jobsAndSpaces, repairJobObjects } from './schemas/saved_objects';
import { jobsAndSpaces, repairJobObjects, jobTypeSchema } from './schemas/saved_objects';
import { jobIdsSchema } from './schemas/job_service_schema';

/**
* Routes for job saved object management
*/
export function savedObjectsRoutes({ router, routeGuard }: RouteInitialization) {
export function savedObjectsRoutes(
{ router, routeGuard }: RouteInitialization,
{ getSpaces, resolveMlCapabilities }: SavedObjectsRouteDeps
) {
/**
* @apiGroup JobSavedObjects
*
Expand Down Expand Up @@ -220,4 +224,50 @@ export function savedObjectsRoutes({ router, routeGuard }: RouteInitialization)
}
})
);

/**
* @apiGroup JobSavedObjects
*
* @api {get} /api/ml/saved_objects/delete_job_check Check whether user can delete a job
* @apiName DeleteJobCheck
* @apiDescription Check the user's ability to delete jobs. Returns whether they are able
* to fully delete the job and whether they are able to remove it from
* the current space.
*
* @apiSchema (body) jobIdsSchema (params) jobTypeSchema
*
*/
router.post(
{
path: '/api/ml/saved_objects/can_delete_job/{jobType}',
validate: {
params: jobTypeSchema,
body: jobIdsSchema,
},
options: {
tags: ['access:ml:canGetJobs', 'access:ml:canGetDataFrameAnalytics'],
},
},
routeGuard.fullLicenseAPIGuard(async ({ request, response, jobSavedObjectService, client }) => {
try {
const { jobType } = request.params;
const { jobIds }: { jobIds: string[] } = request.body;

const { canDeleteJobs } = checksFactory(client, jobSavedObjectService);
const body = await canDeleteJobs(
request,
jobType,
jobIds,
getSpaces !== undefined,
resolveMlCapabilities
);

return response.ok({
body,
});
} catch (e) {
return response.customError(wrapError(e));
}
})
);
}
4 changes: 4 additions & 0 deletions x-pack/plugins/ml/server/routes/schemas/saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ export const jobsAndSpaces = schema.object({
});

export const repairJobObjects = schema.object({ simulate: schema.maybe(schema.boolean()) });

export const jobTypeSchema = schema.object({
jobType: schema.string(),
});
3 changes: 2 additions & 1 deletion x-pack/plugins/ml/server/saved_objects/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { KibanaRequest } from 'kibana/server';
import type { SecurityPluginSetup } from '../../../security/server';
import { ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects';

export function authorizationProvider(authorization: SecurityPluginSetup['authz']) {
async function authorizationCheck(request: KibanaRequest) {
Expand All @@ -18,7 +19,7 @@ export function authorizationProvider(authorization: SecurityPluginSetup['authz'
request
);
const createMLJobAuthorizationAction = authorization.actions.savedObject.get(
'ml-job',
ML_SAVED_OBJECT_TYPE,
'create'
);
const canCreateGlobally = (
Expand Down
108 changes: 105 additions & 3 deletions x-pack/plugins/ml/server/saved_objects/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { IScopedClusterClient } from 'kibana/server';
import Boom from '@hapi/boom';
import { IScopedClusterClient, KibanaRequest } from 'kibana/server';
import type { JobSavedObjectService } from './service';
import { JobType } from '../../common/types/saved_objects';
import { JobType, DeleteJobCheckResponse } from '../../common/types/saved_objects';

import { Job } from '../../common/types/anomaly_detection_jobs';
import { Datafeed } from '../../common/types/anomaly_detection_jobs';
import { DataFrameAnalyticsConfig } from '../../common/types/data_frame_analytics';
import { ResolveMlCapabilities } from '../../common/types/capabilities';

interface JobSavedObjectStatus {
jobId: string;
Expand Down Expand Up @@ -154,5 +156,105 @@ export function checksFactory(
};
}

return { checkStatus };
async function canDeleteJobs(
request: KibanaRequest,
jobType: JobType,
jobIds: string[],
spacesEnabled: boolean,
resolveMlCapabilities: ResolveMlCapabilities
) {
if (jobType !== 'anomaly-detector' && jobType !== 'data-frame-analytics') {
throw Boom.badRequest('Job type must be "anomaly-detector" or "data-frame-analytics"');
}

const mlCapabilities = await resolveMlCapabilities(request);
if (mlCapabilities === null) {
throw Boom.internal('mlCapabilities is not defined');
}

if (
(jobType === 'anomaly-detector' && mlCapabilities.canDeleteJob === false) ||
(jobType === 'data-frame-analytics' && mlCapabilities.canDeleteDataFrameAnalytics === false)
) {
// user does not have access to delete jobs.
return jobIds.reduce((results, jobId) => {
results[jobId] = {
canDelete: false,
canUntag: false,
};
return results;
}, {} as DeleteJobCheckResponse);
}

if (spacesEnabled === false) {
// spaces are disabled, delete only no untagging
return jobIds.reduce((results, jobId) => {
results[jobId] = {
canDelete: true,
canUntag: false,
};
return results;
}, {} as DeleteJobCheckResponse);
}
const canCreateGlobalJobs = await jobSavedObjectService.canCreateGlobalJobs(request);

const jobObjects = await Promise.all(
jobIds.map((id) => jobSavedObjectService.getJobObject(jobType, id))
);

return jobIds.reduce((results, jobId) => {
const jobObject = jobObjects.find((j) => j?.attributes.job_id === jobId);
if (jobObject === undefined || jobObject.namespaces === undefined) {
// job saved object not found
results[jobId] = {
canDelete: false,
canUntag: false,
};
return results;
}

const { namespaces } = jobObject;
const isGlobalJob = namespaces.includes('*');

// job is in * space, user can see all spaces - delete and no option to untag
if (canCreateGlobalJobs && isGlobalJob) {
results[jobId] = {
canDelete: true,
canUntag: false,
};
return results;
}

// job is in * space, user cannot see all spaces - no untagging, no deleting
if (isGlobalJob) {
results[jobId] = {
canDelete: false,
canUntag: false,
};
return results;
}

// jobs with are in individual spaces can only be untagged
// from current space if the job is in more than 1 space
const canUntag = namespaces.length > 1;

// job is in individual spaces, user cannot see all of them - untag only, no delete
if (namespaces.includes('?')) {
results[jobId] = {
canDelete: false,
canUntag,
};
return results;
}

// job is individual spaces, user can see all of them - delete and option to untag
results[jobId] = {
canDelete: true,
canUntag,
};
return results;
}, {} as DeleteJobCheckResponse);
}

return { checkStatus, canDeleteJobs };
}
17 changes: 16 additions & 1 deletion x-pack/plugins/ml/server/saved_objects/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
*/

import RE2 from 're2';
import { KibanaRequest, SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/server';
import {
KibanaRequest,
SavedObjectsClientContract,
SavedObjectsFindOptions,
SavedObjectsFindResult,
} from 'kibana/server';
import type { SecurityPluginSetup } from '../../../security/server';
import { JobType, ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects';
import { MLJobNotFound } from '../lib/ml_client';
Expand Down Expand Up @@ -133,6 +138,15 @@ export function jobSavedObjectServiceFactory(
return await _getJobObjects(jobType, undefined, undefined, currentSpaceOnly);
}

async function getJobObject(
jobType: JobType,
jobId: string,
currentSpaceOnly: boolean = true
): Promise<SavedObjectsFindResult<JobObject> | undefined> {
const [jobObject] = await _getJobObjects(jobType, jobId, undefined, currentSpaceOnly);
return jobObject;
}

async function getAllJobObjectsForAllSpaces(jobType?: JobType) {
await isMlReady();
const filterObject: JobObjectFilter = {};
Expand Down Expand Up @@ -307,6 +321,7 @@ export function jobSavedObjectServiceFactory(

return {
getAllJobObjects,
getJobObject,
createAnomalyDetectionJob,
createDataFrameAnalyticsJob,
deleteAnomalyDetectionJob,
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/ml/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export interface SystemRouteDeps {
resolveMlCapabilities: ResolveMlCapabilities;
}

export interface SavedObjectsRouteDeps {
getSpaces?: () => Promise<SpacesPluginStart>;
resolveMlCapabilities: ResolveMlCapabilities;
}

export interface PluginsSetup {
cloud: CloudSetup;
features: FeaturesPluginSetup;
Expand Down

0 comments on commit 24f262b

Please sign in to comment.