Skip to content

Commit

Permalink
add api security control tests
Browse files Browse the repository at this point in the history
  • Loading branch information
shahzad31 committed Nov 12, 2024
1 parent 6bd2601 commit 66f73a1
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { isEmpty } from 'lodash';
import { PrivateLocationAttributes } from '../../runtime_types/private_locations';
import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor/utils';
import { SyntheticsRestApiRouteFactory } from '../types';
Expand All @@ -31,6 +32,9 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () =
}): Promise<any> => {
const monitor = request.body as MonitorFields;
const { monitorId } = request.params;
if (isEmpty(monitor)) {
return response.badRequest({ body: { message: 'Monitor data is empty.' } });
}

const validationResult = validateMonitor(monitor);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { v4 as uuidv4 } from 'uuid';
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
import { getDecryptedMonitor } from '../../saved_objects/synthetics_monitor';
import { PrivateLocationAttributes } from '../../runtime_types/private_locations';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
Expand All @@ -14,6 +15,7 @@ import { ConfigKey, MonitorFields } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { normalizeSecrets } from '../../synthetics_service/utils/secrets';
import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor/utils';
import { getMonitorNotFoundResponse } from './service_errors';

export const testNowMonitorRoute: SyntheticsRestApiRouteFactory<TestNowResponse> = () => ({
method: 'POST',
Expand All @@ -34,47 +36,55 @@ export const triggerTestNow = async (
monitorId: string,
routeContext: RouteContext
): Promise<TestNowResponse> => {
const { server, spaceId, syntheticsMonitorClient, savedObjectsClient } = routeContext;
const { server, spaceId, syntheticsMonitorClient, savedObjectsClient, response } = routeContext;

const monitorWithSecrets = await getDecryptedMonitor(server, monitorId, spaceId);
const normalizedMonitor = normalizeSecrets(monitorWithSecrets);
try {
const monitorWithSecrets = await getDecryptedMonitor(server, monitorId, spaceId);
const normalizedMonitor = normalizeSecrets(monitorWithSecrets);

const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } =
monitorWithSecrets.attributes;
const { [ConfigKey.SCHEDULE]: schedule, [ConfigKey.LOCATIONS]: locations } =
monitorWithSecrets.attributes;

const privateLocations: PrivateLocationAttributes[] = await getPrivateLocationsForMonitor(
savedObjectsClient,
normalizedMonitor.attributes
);
const testRunId = uuidv4();
const privateLocations: PrivateLocationAttributes[] = await getPrivateLocationsForMonitor(
savedObjectsClient,
normalizedMonitor.attributes
);
const testRunId = uuidv4();

const [, errors] = await syntheticsMonitorClient.testNowConfigs(
{
monitor: normalizedMonitor.attributes as MonitorFields,
id: monitorId,
testRunId,
},
savedObjectsClient,
privateLocations,
spaceId
);
const [, errors] = await syntheticsMonitorClient.testNowConfigs(
{
monitor: normalizedMonitor.attributes as MonitorFields,
id: monitorId,
testRunId,
},
savedObjectsClient,
privateLocations,
spaceId
);

if (errors && errors?.length > 0) {
return {
errors,
testRunId,
schedule,
locations,
configId: monitorId,
monitor: normalizedMonitor.attributes,
};
}

if (errors && errors?.length > 0) {
return {
errors,
testRunId,
schedule,
locations,
configId: monitorId,
monitor: normalizedMonitor.attributes,
};
}
} catch (getErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
return getMonitorNotFoundResponse(response, monitorId);
}

return {
testRunId,
schedule,
locations,
configId: monitorId,
monitor: normalizedMonitor.attributes,
};
throw getErr;
}
};
1 change: 1 addition & 0 deletions x-pack/test/api_integration/apis/synthetics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
await esDeleteAllIndices('synthetics*');
});

loadTestFile(require.resolve('./synthetics_api_security'));
loadTestFile(require.resolve('./edit_monitor_public_api'));
loadTestFile(require.resolve('./add_monitor_public_api'));
loadTestFile(require.resolve('./synthetics_enablement'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export class SyntheticsMonitorTestService {
}
}

async addsNewSpace() {
async addsNewSpace(uptimePermissions: string[] = ['all']) {
const username = 'admin';
const password = `${username}-password`;
const roleName = 'uptime-role';
Expand All @@ -190,7 +190,8 @@ export class SyntheticsMonitorTestService {
kibana: [
{
feature: {
uptime: ['all'],
uptime: uptimePermissions,
slo: ['all'],
},
spaces: ['*'],
},
Expand Down
162 changes: 162 additions & 0 deletions x-pack/test/api_integration/apis/synthetics/synthetics_api_security.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import {
syntheticsAppPublicRestApiRoutes,
syntheticsAppRestApiRoutes,
} from '@kbn/synthetics-plugin/server/routes';
import expect from '@kbn/expect';
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
import { FtrProviderContext } from '../../ftr_provider_context';
import { SyntheticsMonitorTestService } from './services/synthetics_monitor_test_service';

export default function ({ getService }: FtrProviderContext) {
describe('SyntheticsAPISecurity', function () {
this.tags('skipCloud');

const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');

const monitorTestService = new SyntheticsMonitorTestService(getService);
const kibanaServer = getService('kibanaServer');

const assertPermissions = async (
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
path: string,
options: {
statusCodes: number[];
SPACE_ID: string;
username: string;
password: string;
writeAccess?: boolean;
tags?: string;
}
) => {
let resp;
const { statusCodes, SPACE_ID, username, password, writeAccess } = options;
const tags = !writeAccess ? '[uptime-read]' : options.tags ?? '[uptime-read,uptime-write]';
const getStatusMessage = (respStatus: string) =>
`Expected ${statusCodes?.join(
','
)}, got ${respStatus} status code doesn't match, for path: ${path} and method ${method}`;

const getBodyMessage = (tg?: string) =>
`API [${method} ${path}] is unauthorized for user, this action is granted by the Kibana privileges ${
tg ?? tags
}`;

const verifyPermissionsBody = (res: any, msg: string) => {
if (res.status === 403 && !res.body.message.includes('MissingIndicesPrivileges:')) {
expect(decodeURIComponent(res.body.message)).to.eql(msg);
}
};

switch (method) {
case 'GET':
resp = await supertestWithoutAuth
.get(`/s/${SPACE_ID}${path}`)
.auth(username, password)
.set('kbn-xsrf', 'true')
.send({});
expect(statusCodes.includes(resp.status)).to.eql(true, getStatusMessage(resp.status));
verifyPermissionsBody(resp, getBodyMessage('[uptime-read]'));
break;
case 'PUT':
resp = await supertestWithoutAuth
.put(`/s/${SPACE_ID}${path}`)
.auth(username, password)
.set('kbn-xsrf', 'true')
.send({});
expect(statusCodes.includes(resp.status)).to.eql(true, getStatusMessage(resp.status));
verifyPermissionsBody(resp, getBodyMessage());
break;
case 'POST':
resp = await supertestWithoutAuth
.post(`/s/${SPACE_ID}${path}`)
.auth(username, password)
.set('kbn-xsrf', 'true')
.send({});
expect(statusCodes.includes(resp.status)).to.eql(true, getStatusMessage(resp.status));
verifyPermissionsBody(resp, getBodyMessage());
break;
case 'DELETE':
resp = await supertestWithoutAuth
.delete(`/s/${SPACE_ID}${path}`)
.auth(username, password)
.set('kbn-xsrf', 'true')
.send({});
expect(statusCodes.includes(resp.status)).to.eql(true, getStatusMessage(resp.status));
verifyPermissionsBody(resp, getBodyMessage());
break;
}

return resp;
};

before(async () => {
await kibanaServer.savedObjects.cleanStandardList();
});

const allRoutes = syntheticsAppRestApiRoutes.concat(syntheticsAppPublicRestApiRoutes);

it('throws permissions errors for un-auth user', async () => {
const { SPACE_ID, username, password } = await monitorTestService.addsNewSpace([]);

for (const routeFn of allRoutes) {
const route = routeFn();
await assertPermissions(route.method, route.path, {
statusCodes: [403],
SPACE_ID,
username,
password,
writeAccess: route.writeAccess ?? true,
});
}
});

it('throws permissions errors for read user', async () => {
const { SPACE_ID, username, password } = await monitorTestService.addsNewSpace(['read']);

for (const routeFn of allRoutes) {
const route = routeFn();
if (route.writeAccess === false) {
continue;
}
await assertPermissions(route.method, route.path, {
statusCodes: [200, 403, 500, 400, 404],
SPACE_ID,
username,
password,
writeAccess: route.writeAccess ?? true,
tags: '[uptime-write]',
});
}
});

it('no permissions errors for all user', async () => {
const { SPACE_ID, username, password } = await monitorTestService.addsNewSpace(['all']);

for (const routeFn of allRoutes) {
const route = routeFn();
if (
(route.method === 'DELETE' && route.path === SYNTHETICS_API_URLS.SYNTHETICS_ENABLEMENT) ||
SYNTHETICS_API_URLS.SYNTHETICS_PROJECT_APIKEY
) {
continue;
}
await assertPermissions(route.method, route.path, {
statusCodes: [400, 200, 404],
SPACE_ID,
username,
password,
writeAccess: route.writeAccess ?? true,
tags: '[uptime-write]',
});
}
});
});
}

0 comments on commit 66f73a1

Please sign in to comment.