Skip to content

Commit 13ec56d

Browse files
Alex Kahanelasticmachine
andauthored
Limit concurrent access to download API + Replace with LRU cache (#72503)
* Limit concurrent access to download API * Replacing cache with LRU Cache * Configure the LRU cache Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent eb71e59 commit 13ec56d

File tree

11 files changed

+100
-108
lines changed

11 files changed

+100
-108
lines changed

x-pack/plugins/security_solution/common/endpoint/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ export const alertsIndexPattern = 'logs-endpoint.alerts-*';
99
export const metadataIndexPattern = 'metrics-endpoint.metadata-*';
1010
export const policyIndexPattern = 'metrics-endpoint.policy-*';
1111
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
12+
export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency';
13+
export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100;

x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts

Lines changed: 0 additions & 48 deletions
This file was deleted.

x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

x-pack/plugins/security_solution/server/endpoint/lib/artifacts/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
export * from './cache';
87
export * from './common';
98
export * from './lists';
109
export * from './manifest';

x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66
import { deflateSync, inflateSync } from 'zlib';
7+
import LRU from 'lru-cache';
78
import {
89
ILegacyClusterClient,
910
IRouter,
@@ -22,7 +23,6 @@ import {
2223
httpServerMock,
2324
loggingSystemMock,
2425
} from 'src/core/server/mocks';
25-
import { ExceptionsCache } from '../../lib/artifacts/cache';
2626
import { ArtifactConstants } from '../../lib/artifacts';
2727
import { registerDownloadExceptionListRoute } from './download_exception_list';
2828
import { EndpointAppContextService } from '../../endpoint_app_context_services';
@@ -97,7 +97,7 @@ describe('test alerts route', () => {
9797
let routeConfig: RouteConfig<unknown, unknown, unknown, never>;
9898
let routeHandler: RequestHandler<unknown, unknown, unknown>;
9999
let endpointAppContextService: EndpointAppContextService;
100-
let cache: ExceptionsCache;
100+
let cache: LRU<string, Buffer>;
101101
let ingestSavedObjectClient: jest.Mocked<SavedObjectsClientContract>;
102102

103103
beforeEach(() => {
@@ -108,7 +108,7 @@ describe('test alerts route', () => {
108108
mockClusterClient.asScoped.mockReturnValue(mockScopedClient);
109109
routerMock = httpServiceMock.createRouter();
110110
endpointAppContextService = new EndpointAppContextService();
111-
cache = new ExceptionsCache(5);
111+
cache = new LRU<string, Buffer>({ max: 10, maxAge: 1000 * 60 * 60 });
112112
const startContract = createMockEndpointAppContextServiceStartContract();
113113

114114
// The authentication with the Fleet Plugin needs a separate scoped SO Client
@@ -164,7 +164,7 @@ describe('test alerts route', () => {
164164
path.startsWith('/api/endpoint/artifacts/download')
165165
)!;
166166

167-
expect(routeConfig.options).toEqual(undefined);
167+
expect(routeConfig.options).toEqual({ tags: ['endpoint:limited-concurrency'] });
168168

169169
await routeHandler(
170170
({

x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import {
1111
IKibanaResponse,
1212
SavedObject,
1313
} from 'src/core/server';
14+
import LRU from 'lru-cache';
1415
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
1516
import { authenticateAgentWithAccessToken } from '../../../../../ingest_manager/server/services/agents/authenticate';
1617
import { validate } from '../../../../common/validate';
18+
import { LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG } from '../../../../common/endpoint/constants';
1719
import { buildRouteValidation } from '../../../utils/build_validation/route_validation';
18-
import { ArtifactConstants, ExceptionsCache } from '../../lib/artifacts';
20+
import { ArtifactConstants } from '../../lib/artifacts';
1921
import {
2022
DownloadArtifactRequestParamsSchema,
2123
downloadArtifactRequestParamsSchema,
@@ -32,7 +34,7 @@ const allowlistBaseRoute: string = '/api/endpoint/artifacts';
3234
export function registerDownloadExceptionListRoute(
3335
router: IRouter,
3436
endpointContext: EndpointAppContext,
35-
cache: ExceptionsCache
37+
cache: LRU<string, Buffer>
3638
) {
3739
router.get(
3840
{
@@ -43,6 +45,7 @@ export function registerDownloadExceptionListRoute(
4345
DownloadArtifactRequestParamsSchema
4446
>(downloadArtifactRequestParamsSchema),
4547
},
48+
options: { tags: [LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG] },
4649
},
4750
async (context, req, res) => {
4851
let scopedSOClient: SavedObjectsClientContract;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import {
8+
CoreSetup,
9+
KibanaRequest,
10+
LifecycleResponseFactory,
11+
OnPreAuthToolkit,
12+
} from 'kibana/server';
13+
import {
14+
LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG,
15+
LIMITED_CONCURRENCY_ENDPOINT_COUNT,
16+
} from '../../../common/endpoint/constants';
17+
18+
class MaxCounter {
19+
constructor(private readonly max: number = 1) {}
20+
private counter = 0;
21+
valueOf() {
22+
return this.counter;
23+
}
24+
increase() {
25+
if (this.counter < this.max) {
26+
this.counter += 1;
27+
}
28+
}
29+
decrease() {
30+
if (this.counter > 0) {
31+
this.counter -= 1;
32+
}
33+
}
34+
lessThanMax() {
35+
return this.counter < this.max;
36+
}
37+
}
38+
39+
function shouldHandleRequest(request: KibanaRequest) {
40+
const tags = request.route.options.tags;
41+
return tags.includes(LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG);
42+
}
43+
44+
export function registerLimitedConcurrencyRoutes(core: CoreSetup) {
45+
const counter = new MaxCounter(LIMITED_CONCURRENCY_ENDPOINT_COUNT);
46+
core.http.registerOnPreAuth(function preAuthHandler(
47+
request: KibanaRequest,
48+
response: LifecycleResponseFactory,
49+
toolkit: OnPreAuthToolkit
50+
) {
51+
if (!shouldHandleRequest(request)) {
52+
return toolkit.next();
53+
}
54+
55+
if (!counter.lessThanMax()) {
56+
return response.customError({
57+
body: 'Too Many Requests',
58+
statusCode: 429,
59+
});
60+
}
61+
62+
counter.increase();
63+
64+
// requests.events.aborted$ has a bug (but has test which explicitly verifies) where it's fired even when the request completes
65+
// https://github.com/elastic/kibana/pull/70495#issuecomment-656288766
66+
request.events.aborted$.toPromise().then(() => {
67+
counter.decrease();
68+
});
69+
70+
return toolkit.next();
71+
});
72+
}

x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Logger } from 'src/core/server';
99
import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server';
1010
import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks';
1111
import { listMock } from '../../../../../../lists/server/mocks';
12-
import { ExceptionsCache } from '../../../lib/artifacts';
12+
import LRU from 'lru-cache';
1313
import { getArtifactClientMock } from '../artifact_client.mock';
1414
import { getManifestClientMock } from '../manifest_client.mock';
1515
import { ManifestManager } from './manifest_manager';
@@ -28,11 +28,11 @@ export enum ManifestManagerMockType {
2828

2929
export const getManifestManagerMock = (opts?: {
3030
mockType?: ManifestManagerMockType;
31-
cache?: ExceptionsCache;
31+
cache?: LRU<string, Buffer>;
3232
packageConfigService?: jest.Mocked<PackageConfigServiceInterface>;
3333
savedObjectsClient?: ReturnType<typeof savedObjectsClientMock.create>;
3434
}): ManifestManager => {
35-
let cache = new ExceptionsCache(5);
35+
let cache = new LRU<string, Buffer>({ max: 10, maxAge: 1000 * 60 * 60 });
3636
if (opts?.cache !== undefined) {
3737
cache = opts.cache;
3838
}

x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@
77
import { inflateSync } from 'zlib';
88
import { savedObjectsClientMock } from 'src/core/server/mocks';
99
import { createPackageConfigServiceMock } from '../../../../../../ingest_manager/server/mocks';
10-
import {
11-
ArtifactConstants,
12-
ManifestConstants,
13-
ExceptionsCache,
14-
isCompleteArtifact,
15-
} from '../../../lib/artifacts';
10+
import { ArtifactConstants, ManifestConstants, isCompleteArtifact } from '../../../lib/artifacts';
11+
1612
import { getManifestManagerMock } from './manifest_manager.mock';
13+
import LRU from 'lru-cache';
1714

1815
describe('manifest_manager', () => {
1916
describe('ManifestManager sanity checks', () => {
@@ -41,7 +38,7 @@ describe('manifest_manager', () => {
4138
});
4239

4340
test('ManifestManager populates cache properly', async () => {
44-
const cache = new ExceptionsCache(5);
41+
const cache = new LRU<string, Buffer>({ max: 10, maxAge: 1000 * 60 * 60 });
4542
const manifestManager = getManifestManagerMock({ cache });
4643
const oldManifest = await manifestManager.getLastComputedManifest(
4744
ManifestConstants.SCHEMA_VERSION

x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { Logger, SavedObjectsClientContract } from 'src/core/server';
8+
import LRU from 'lru-cache';
89
import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server';
910
import { ExceptionListClient } from '../../../../../../lists/server';
1011
import { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common';
@@ -16,7 +17,6 @@ import {
1617
Manifest,
1718
buildArtifact,
1819
getFullEndpointExceptionList,
19-
ExceptionsCache,
2020
ManifestDiff,
2121
getArtifactId,
2222
} from '../../../lib/artifacts';
@@ -33,7 +33,7 @@ export interface ManifestManagerContext {
3333
exceptionListClient: ExceptionListClient;
3434
packageConfigService: PackageConfigServiceInterface;
3535
logger: Logger;
36-
cache: ExceptionsCache;
36+
cache: LRU<string, Buffer>;
3737
}
3838

3939
export interface ManifestSnapshotOpts {
@@ -51,7 +51,7 @@ export class ManifestManager {
5151
protected packageConfigService: PackageConfigServiceInterface;
5252
protected savedObjectsClient: SavedObjectsClientContract;
5353
protected logger: Logger;
54-
protected cache: ExceptionsCache;
54+
protected cache: LRU<string, Buffer>;
5555

5656
constructor(context: ManifestManagerContext) {
5757
this.artifactClient = context.artifactClient;

0 commit comments

Comments
 (0)