Skip to content

Commit

Permalink
[7.x] [APM] Add license guard for annotations (#65995) (#66623)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgieselaar authored May 19, 2020
1 parent 5f58a11 commit 31c680a
Show file tree
Hide file tree
Showing 26 changed files with 392 additions and 61 deletions.
13 changes: 5 additions & 8 deletions x-pack/plugins/apm/server/routes/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import * as t from 'io-ts';
import Boom from 'boom';
import { unique } from 'lodash';
import { ScopedAnnotationsClient } from '../../../observability/server';
import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceAgentName } from '../lib/services/get_service_agent_name';
import { getServices } from '../lib/services/get_services';
Expand Down Expand Up @@ -95,13 +94,10 @@ export const serviceAnnotationsRoute = createRoute(() => ({
const { serviceName } = context.params.path;
const { environment } = context.params.query;

let annotationsClient: ScopedAnnotationsClient | undefined;

if (context.plugins.observability) {
annotationsClient = await context.plugins.observability.getScopedAnnotationsClient(
request
);
}
const annotationsClient = await context.plugins.observability?.getScopedAnnotationsClient(
context,
request
);

return getServiceAnnotations({
setup,
Expand Down Expand Up @@ -143,6 +139,7 @@ export const serviceAnnotationsCreateRoute = createRoute(() => ({
},
handler: async ({ request, context }) => {
const annotationsClient = await context.plugins.observability?.getScopedAnnotationsClient(
context,
request
);

Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/observability/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"xpack",
"observability"
],
"optionalPlugins": [
"licensing"
],
"ui": true,
"server": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, PluginInitializerContext, KibanaRequest } from 'kibana/server';
import {
CoreSetup,
PluginInitializerContext,
KibanaRequest,
RequestHandlerContext,
} from 'kibana/server';
import { PromiseReturnType } from '../../../typings/common';
import { createAnnotationsClient } from './create_annotations_client';
import { registerAnnotationAPIs } from './register_annotation_apis';
Expand Down Expand Up @@ -31,11 +36,12 @@ export async function bootstrapAnnotations({ index, core, context }: Params) {
});

return {
getScopedAnnotationsClient: (request: KibanaRequest) => {
getScopedAnnotationsClient: (requestContext: RequestHandlerContext, request: KibanaRequest) => {
return createAnnotationsClient({
index,
apiCaller: core.elasticsearch.dataClient.asScoped(request).callAsCurrentUser,
logger,
license: requestContext.licensing?.license,
});
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import { APICaller, Logger } from 'kibana/server';
import * as t from 'io-ts';
import { Client } from 'elasticsearch';
import Boom from 'boom';
import { ILicense } from '../../../../licensing/server';
import {
createAnnotationRt,
deleteAnnotationRt,
Expand Down Expand Up @@ -40,8 +42,9 @@ export function createAnnotationsClient(params: {
index: string;
apiCaller: APICaller;
logger: Logger;
license?: ILicense;
}) {
const { index, apiCaller, logger } = params;
const { index, apiCaller, logger, license } = params;

const initIndex = () =>
createOrUpdateIndex({
Expand All @@ -51,48 +54,59 @@ export function createAnnotationsClient(params: {
logger,
});

function ensureGoldLicense<T extends (...args: any[]) => any>(fn: T): T {
return ((...args) => {
if (!license?.hasAtLeast('gold')) {
throw Boom.forbidden('Annotations require at least a gold license or a trial license.');
}
return fn(...args);
}) as T;
}

return {
get index() {
return index;
},
create: async (
createParams: CreateParams
): Promise<{ _id: string; _index: string; _source: Annotation }> => {
const indexExists = await apiCaller('indices.exists', {
index,
});
create: ensureGoldLicense(
async (
createParams: CreateParams
): Promise<{ _id: string; _index: string; _source: Annotation }> => {
const indexExists = await apiCaller('indices.exists', {
index,
});

if (!indexExists) {
await initIndex();
}
if (!indexExists) {
await initIndex();
}

const annotation = {
...createParams,
event: {
created: new Date().toISOString(),
},
};
const annotation = {
...createParams,
event: {
created: new Date().toISOString(),
},
};

const response = (await apiCaller('index', {
index,
body: annotation,
refresh: 'wait_for',
})) as IndexDocumentResponse;
const response = (await apiCaller('index', {
index,
body: annotation,
refresh: 'wait_for',
})) as IndexDocumentResponse;

return apiCaller('get', {
index,
id: response._id,
});
},
getById: async (getByIdParams: GetByIdParams) => {
return apiCaller('get', {
index,
id: response._id,
});
}
),
getById: ensureGoldLicense(async (getByIdParams: GetByIdParams) => {
const { id } = getByIdParams;

return apiCaller('get', {
id,
index,
});
},
delete: async (deleteParams: DeleteParams) => {
}),
delete: ensureGoldLicense(async (deleteParams: DeleteParams) => {
const { id } = deleteParams;

const response = (await apiCaller('delete', {
Expand All @@ -101,6 +115,6 @@ export function createAnnotationsClient(params: {
refresh: 'wait_for',
})) as PromiseReturnType<Client['delete']>;
return response;
},
}),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function registerAnnotationAPIs({
handler: (params: { data: t.TypeOf<TType>; client: ScopedAnnotationsClient }) => Promise<any>
): RequestHandler {
return async (...args: Parameters<RequestHandler>) => {
const [, request, response] = args;
const [context, request, response] = args;

const rt = types;

Expand All @@ -56,16 +56,26 @@ export function registerAnnotationAPIs({
index,
apiCaller,
logger,
license: context.licensing?.license,
});

const res = await handler({
data: validation.right,
client,
});
try {
const res = await handler({
data: validation.right,
client,
});

return response.ok({
body: res,
});
return response.ok({
body: res,
});
} catch (err) {
return response.custom({
statusCode: err.output?.statusCode ?? 500,
body: {
message: err.output?.payload?.message ?? 'An internal server error occured',
},
});
}
};
}

Expand Down
4 changes: 4 additions & 0 deletions x-pack/scripts/functional_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ require('@kbn/test').runTestsCli([
require.resolve('../test/alerting_api_integration/basic/config.ts'),
require.resolve('../test/alerting_api_integration/spaces_only/config.ts'),
require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'),
require.resolve('../test/apm_api_integration/basic/config.ts'),
require.resolve('../test/apm_api_integration/trial/config.ts'),
require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'),
require.resolve('../test/detection_engine_api_integration/basic/config.ts'),
require.resolve('../test/plugin_api_integration/config.ts'),
Expand All @@ -27,6 +29,8 @@ require('@kbn/test').runTestsCli([
require.resolve('../test/token_api_integration/config'),
require.resolve('../test/oidc_api_integration/config'),
require.resolve('../test/oidc_api_integration/implicit_flow.config'),
require.resolve('../test/observability_api_integration/basic/config.ts'),
require.resolve('../test/observability_api_integration/trial/config.ts'),
require.resolve('../test/pki_api_integration/config'),
require.resolve('../test/login_selector_api_integration/config'),
require.resolve('../test/encrypted_saved_objects_api_integration/config'),
Expand Down
2 changes: 0 additions & 2 deletions x-pack/test/api_integration/apis/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,12 @@ export default function({ loadTestFile }) {
loadTestFile(require.resolve('./management'));
loadTestFile(require.resolve('./uptime'));
loadTestFile(require.resolve('./maps'));
loadTestFile(require.resolve('./apm'));
loadTestFile(require.resolve('./siem'));
loadTestFile(require.resolve('./short_urls'));
loadTestFile(require.resolve('./lens'));
loadTestFile(require.resolve('./fleet'));
loadTestFile(require.resolve('./ingest'));
loadTestFile(require.resolve('./endpoint'));
loadTestFile(require.resolve('./ml'));
loadTestFile(require.resolve('./observability'));
});
}
14 changes: 14 additions & 0 deletions x-pack/test/apm_api_integration/basic/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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 { createTestConfig } from '../common/config';

// eslint-disable-next-line import/no-default-export
export default createTestConfig({
license: 'basic',
name: 'X-Pack APM API integration tests (basic)',
testFiles: [require.resolve('./tests')],
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

import expect from '@kbn/expect';
import { AgentConfigurationIntake } from '../../../../plugins/apm/common/agent_configuration/configuration_types';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../common/ftr_provider_context';

// eslint-disable-next-line import/no-default-export
export default function agentConfigurationTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const log = getService('log');
Expand Down
52 changes: 52 additions & 0 deletions x-pack/test/apm_api_integration/basic/tests/annotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 expect from '@kbn/expect';
import { JsonObject } from 'src/plugins/kibana_utils/common';
import { FtrProviderContext } from '../../common/ftr_provider_context';

// eslint-disable-next-line import/no-default-export
export default function annotationApiTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');

function request({ method, url, data }: { method: string; url: string; data?: JsonObject }) {
switch (method.toLowerCase()) {
case 'post':
return supertest
.post(url)
.send(data)
.set('kbn-xsrf', 'foo');

default:
throw new Error(`Unsupported methoed ${method}`);
}
}

describe('APM annotations with a basic license', () => {
describe('when creating an annotation', () => {
it('fails with a 403 forbidden', async () => {
const response = await request({
url: '/api/apm/services/opbeans-java/annotation',
method: 'POST',
data: {
'@timestamp': new Date().toISOString(),
message: 'New deployment',
tags: ['foo'],
service: {
version: '1.1',
environment: 'production',
},
},
});

expect(response.status).to.be(403);
expect(response.body.message).to.be(
'Annotations require at least a gold license or a trial license.'
);
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// import querystring from 'querystring';
// import {isEmpty} from 'lodash'
import URL from 'url';
import expect from '@kbn/expect';
import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../common/ftr_provider_context';

// eslint-disable-next-line import/no-default-export
export default function customLinksTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const log = getService('log');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
*/

import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../common/ftr_provider_context';

// eslint-disable-next-line import/no-default-export
export default function featureControlsTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FtrProviderContext } from '../../common/ftr_provider_context';

import { FtrProviderContext } from '../../ftr_provider_context';

// eslint-disable-next-line import/no-default-export
export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) {
describe('APM specs', () => {
describe('APM specs (basic)', function() {
this.tags('ciGroup1');

loadTestFile(require.resolve('./annotations'));
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./agent_configuration'));
Expand Down
Loading

0 comments on commit 31c680a

Please sign in to comment.