Skip to content

Commit

Permalink
[Cases] Deprecate endpoints (elastic#124773)
Browse files Browse the repository at this point in the history
Co-authored-by: Kibana Machine <[email protected]>
(cherry picked from commit eb7591b)

# Conflicts:
#	x-pack/plugins/cases/server/routes/api/stats/get_status.ts
  • Loading branch information
cnasikas committed Feb 11, 2022
1 parent a5d761b commit 3f49dac
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 13 deletions.
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ export interface PluginStartContract {

export class CasePlugin {
private readonly log: Logger;
private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
private clientFactory: CasesClientFactory;
private securityPluginSetup?: SecurityPluginSetup;
private lensEmbeddableFactory?: LensServerPluginSetup['lensEmbeddableFactory'];

constructor(private readonly initializerContext: PluginInitializerContext) {
this.kibanaVersion = initializerContext.env.packageInfo.version;
this.log = this.initializerContext.logger.get();
this.clientFactory = new CasesClientFactory(this.log);
}
Expand Down Expand Up @@ -101,6 +103,7 @@ export class CasePlugin {
initCaseApi({
logger: this.log,
router,
kibanaVersion: this.kibanaVersion,
});
}

Expand Down
23 changes: 21 additions & 2 deletions x-pack/plugins/cases/server/routes/api/cases/get_case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
import { schema } from '@kbn/config-schema';

import { RouteDeps } from '../types';
import { wrapError } from '../utils';
import { getWarningHeader, logDeprecatedEndpoint, wrapError } from '../utils';
import { CASE_DETAILS_URL } from '../../../../common/constants';

export function initGetCaseApi({ router, logger }: RouteDeps) {
export function initGetCaseApi({ router, logger, kibanaVersion }: RouteDeps) {
router.get(
{
path: CASE_DETAILS_URL,
Expand All @@ -20,16 +20,35 @@ export function initGetCaseApi({ router, logger }: RouteDeps) {
case_id: schema.string(),
}),
query: schema.object({
/**
* @deprecated since version 8.1.0
*/
includeComments: schema.boolean({ defaultValue: true }),
}),
},
},
async (context, request, response) => {
try {
const isIncludeCommentsParamProvidedByTheUser =
request.url.searchParams.has('includeComments');

if (isIncludeCommentsParamProvidedByTheUser) {
logDeprecatedEndpoint(
logger,
request.headers,
`The query parameter 'includeComments' of the get case API '${CASE_DETAILS_URL}' is deprecated`
);
}

const casesClient = await context.cases.getCasesClient();
const id = request.params.case_id;

return response.ok({
...(isIncludeCommentsParamProvidedByTheUser && {
headers: {
...getWarningHeader(kibanaVersion, 'Deprecated query parameter includeComments'),
},
}),
body: await casesClient.cases.get({
id,
includeComments: request.query.includeComments,
Expand Down
16 changes: 14 additions & 2 deletions x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
import { schema } from '@kbn/config-schema';

import { RouteDeps } from '../types';
import { wrapError } from '../utils';
import { wrapError, getWarningHeader, logDeprecatedEndpoint } from '../utils';
import { CASE_COMMENTS_URL } from '../../../../common/constants';

export function initGetAllCommentsApi({ router, logger }: RouteDeps) {
/**
* @deprecated since version 8.1.0
*/
export function initGetAllCommentsApi({ router, logger, kibanaVersion }: RouteDeps) {
router.get(
{
path: CASE_COMMENTS_URL,
Expand All @@ -23,9 +26,18 @@ export function initGetAllCommentsApi({ router, logger }: RouteDeps) {
},
async (context, request, response) => {
try {
logDeprecatedEndpoint(
logger,
request.headers,
`The get all cases comments API '${CASE_COMMENTS_URL}' is deprecated.`
);

const client = await context.cases.getCasesClient();

return response.ok({
headers: {
...getWarningHeader(kibanaVersion),
},
body: await client.attachments.getAll({
caseID: request.params.case_id,
}),
Expand Down
16 changes: 14 additions & 2 deletions x-pack/plugins/cases/server/routes/api/stats/get_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,33 @@
*/

import { RouteDeps } from '../types';
import { escapeHatch, wrapError } from '../utils';
import { escapeHatch, wrapError, getWarningHeader, logDeprecatedEndpoint } from '../utils';

import { CasesStatusRequest } from '../../../../common/api';
import { CASE_STATUS_URL } from '../../../../common/constants';

export function initGetCasesStatusApi({ router, logger }: RouteDeps) {
/**
* @deprecated since version 8.1.0
*/
export function initGetCasesStatusApi({ router, logger, kibanaVersion }: RouteDeps) {
router.get(
{
path: CASE_STATUS_URL,
validate: { query: escapeHatch },
},
async (context, request, response) => {
try {
logDeprecatedEndpoint(
logger,
request.headers,
`The get cases status API '${CASE_STATUS_URL}' is deprecated.`
);

const client = await context.cases.getCasesClient();
return response.ok({
headers: {
...getWarningHeader(kibanaVersion),
},
body: await client.stats.getStatusTotalsByType(request.query as CasesStatusRequest),
});
} catch (error) {
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/cases/server/routes/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
* 2.0.
*/

import type { Logger } from 'kibana/server';
import type { Logger, PluginInitializerContext } from 'kibana/server';

import type { CasesRouter } from '../../types';

export interface RouteDeps {
router: CasesRouter;
logger: Logger;
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
}

export interface TotalCommentByCase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
import { schema } from '@kbn/config-schema';

import { RouteDeps } from '../types';
import { wrapError } from '../utils';
import { getWarningHeader, logDeprecatedEndpoint, wrapError } from '../utils';
import { CASE_USER_ACTIONS_URL } from '../../../../common/constants';

export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) {
/**
* @deprecated since version 8.1.0
*/
export function initGetAllCaseUserActionsApi({ router, logger, kibanaVersion }: RouteDeps) {
router.get(
{
path: CASE_USER_ACTIONS_URL,
Expand All @@ -27,10 +30,19 @@ export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) {
return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' });
}

logDeprecatedEndpoint(
logger,
request.headers,
`The get all cases user actions API '${CASE_USER_ACTIONS_URL}' is deprecated.`
);

const casesClient = await context.cases.getCasesClient();
const caseId = request.params.case_id;

return response.ok({
headers: {
...getWarningHeader(kibanaVersion),
},
body: await casesClient.userActions.getAll({ caseId }),
});
} catch (error) {
Expand Down
22 changes: 21 additions & 1 deletion x-pack/plugins/cases/server/routes/api/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
*/

import { isBoom, boomify } from '@hapi/boom';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { HTTPError } from '../../common/error';
import { wrapError } from './utils';
import { logDeprecatedEndpoint, wrapError } from './utils';

describe('Utils', () => {
describe('wrapError', () => {
Expand Down Expand Up @@ -55,4 +56,23 @@ describe('Utils', () => {
expect(res.headers).toEqual({});
});
});

describe('logDeprecatedEndpoint', () => {
const logger = loggingSystemMock.createLogger();
const kibanaHeader = { 'kbn-version': '8.1.0', referer: 'test' };

beforeEach(() => {
jest.clearAllMocks();
});

it('does NOT log when the request is from the kibana client', () => {
logDeprecatedEndpoint(logger, kibanaHeader, 'test');
expect(logger.warn).not.toHaveBeenCalledWith('test');
});

it('does log when the request is NOT from the kibana client', () => {
logDeprecatedEndpoint(logger, {}, 'test');
expect(logger.warn).toHaveBeenCalledWith('test');
});
});
});
32 changes: 30 additions & 2 deletions x-pack/plugins/cases/server/routes/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
*/

import { Boom, boomify, isBoom } from '@hapi/boom';

import { schema } from '@kbn/config-schema';
import { CustomHttpResponseOptions, ResponseError } from 'kibana/server';
import type { CustomHttpResponseOptions, ResponseError, Headers, Logger } from 'kibana/server';
import { CaseError, isCaseError, HTTPError, isHTTPError } from '../../common/error';

/**
Expand All @@ -35,3 +34,32 @@ export function wrapError(
}

export const escapeHatch = schema.object({}, { unknowns: 'allow' });

/**
* Creates a warning header with a message formatted according to RFC7234.
* We follow the same formatting as Elasticsearch
* https://github.com/elastic/elasticsearch/blob/5baabff6670a8ed49297488ca8cac8ec12a2078d/server/src/main/java/org/elasticsearch/common/logging/HeaderWarning.java#L55
*/
export const getWarningHeader = (
kibanaVersion: string,
msg: string | undefined = 'Deprecated endpoint'
): { warning: string } => ({
warning: `299 Kibana-${kibanaVersion} "${msg}"`,
});

/**
* Taken from
* https://github.com/elastic/kibana/blob/ec30f2aeeb10fb64b507935e558832d3ef5abfaa/x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts#L113-L118
*/

const getIsKibanaRequest = (headers?: Headers) => {
// The presence of these two request headers gives us a good indication that this is a first-party request from the Kibana client.
// We can't be 100% certain, but this is a reasonable attempt.
return headers && headers['kbn-version'] && headers.referer;
};

export const logDeprecatedEndpoint = (logger: Logger, headers: Headers, msg: string) => {
if (!getIsKibanaRequest(headers)) {
logger.warn(msg);
}
};
12 changes: 12 additions & 0 deletions x-pack/test/cases_api_integration/common/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,3 +1131,15 @@ export const getServiceNowSimulationServer = async (): Promise<{

return { server, url };
};

/**
* Extracts the warning value a warning header that is formatted according to RFC 7234.
* For example for the string 299 Kibana-8.1.0 "Deprecation endpoint", the return value is Deprecation endpoint.
*
*/
export const extractWarningValueFromWarningHeader = (warningHeader: string) => {
const firstQuote = warningHeader.indexOf('"');
const lastQuote = warningHeader.length - 1;
const warningValue = warningHeader.substring(firstQuote + 1, lastQuote);
return warningValue;
};
14 changes: 14 additions & 0 deletions x-pack/test/cases_api_integration/common/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,17 @@ export function arraysToEqual(array1?: object[], array2?: object[]) {
const array1AsSet = new Set(array1);
return array2.every((item) => array1AsSet.has(item));
}

/**
* Regular expression to test if a string matches the RFC7234 specification (without warn-date) for warning headers. This pattern assumes that the warn code
* is always 299. Further, this pattern assumes that the warn agent represents a version of Kibana.
*
* Example: 299 Kibana-8.2.0 "Deprecated endpoint"
*/
const WARNING_HEADER_REGEX =
/299 Kibana-\d+.\d+.\d+(?:-(?:alpha|beta|rc)\\d+)?(?:-SNAPSHOT)? \".+\"/g;

export const assertWarningHeader = (warningHeader: string) => {
const res = warningHeader.match(WARNING_HEADER_REGEX);
expect(res).not.to.be(null);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';

import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api';
import { AttributesTypeUser, getCaseDetailsUrl } from '../../../../../../plugins/cases/common/api';
import { CASES_URL } from '../../../../../../plugins/cases/common/constants';
import {
defaultUser,
Expand All @@ -24,6 +24,7 @@ import {
createComment,
removeServerGeneratedPropertiesFromCase,
removeServerGeneratedPropertiesFromSavedObject,
extractWarningValueFromWarningHeader,
} from '../../../../common/lib/utils';
import {
secOnly,
Expand All @@ -37,6 +38,7 @@ import {
obsSec,
} from '../../../../common/lib/authentication/users';
import { getUserInfo } from '../../../../common/lib/authentication';
import { assertWarningHeader } from '../../../../common/lib/validation';

// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
Expand Down Expand Up @@ -191,5 +193,29 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
});

describe('deprecations', () => {
for (const paramValue of [true, false]) {
it(`should return a warning header if includeComments=${paramValue}`, async () => {
const theCase = await createCase(supertest, postCaseReq);
const res = await supertest
.get(`${getCaseDetailsUrl(theCase.id)}?includeComments=${paramValue}`)
.expect(200);
const warningHeader = res.header.warning;

assertWarningHeader(warningHeader);

const warningValue = extractWarningValueFromWarningHeader(warningHeader);
expect(warningValue).to.be('Deprecated query parameter includeComments');
});
}

it('should not return a warning header if includeComments is not provided', async () => {
const theCase = await createCase(supertest, postCaseReq);
const res = await supertest.get(getCaseDetailsUrl(theCase.id)).expect(200);
const warningHeader = res.header.warning;
expect(warningHeader).to.be(undefined);
});
});
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getAllCasesStatuses,
deleteAllCaseItems,
superUserSpace1Auth,
extractWarningValueFromWarningHeader,
} from '../../../../../common/lib/utils';
import {
globalRead,
Expand All @@ -26,6 +27,8 @@ import {
secOnlyRead,
superUser,
} from '../../../../../common/lib/authentication/users';
import { CASE_STATUS_URL } from '../../../../../../../plugins/cases/common/constants';
import { assertWarningHeader } from '../../../../../common/lib/validation';

// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
Expand Down Expand Up @@ -181,5 +184,18 @@ export default ({ getService }: FtrProviderContext): void => {
});
}
});

describe('deprecations', () => {
it('should return a warning header', async () => {
await createCase(supertest, postCaseReq);
const res = await supertest.get(CASE_STATUS_URL).expect(200);
const warningHeader = res.header.warning;

assertWarningHeader(warningHeader);

const warningValue = extractWarningValueFromWarningHeader(warningHeader);
expect(warningValue).to.be('Deprecated endpoint');
});
});
});
};
Loading

0 comments on commit 3f49dac

Please sign in to comment.