Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cases] Deprecate endpoints #124773

Merged
merged 18 commits into from
Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
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
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.metrics.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