Skip to content

Commit 14f975c

Browse files
authored
[IngestManager] Expose agent authentication using access key (#69650)
* [IngestManager] Expose agent authentication using access key * Add unit tests to authenticateAgentWithAccessToken service
1 parent a104e5a commit 14f975c

File tree

9 files changed

+193
-10
lines changed

9 files changed

+193
-10
lines changed

x-pack/plugins/ingest_manager/server/plugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import {
5454
AgentService,
5555
datasourceService,
5656
} from './services';
57-
import { getAgentStatusById } from './services/agents';
57+
import { getAgentStatusById, authenticateAgentWithAccessToken } from './services/agents';
5858
import { CloudSetup } from '../../cloud/server';
5959
import { agentCheckinState } from './services/agents/checkin/state';
6060

@@ -256,6 +256,7 @@ export class IngestManagerPlugin
256256
esIndexPatternService: new ESIndexPatternSavedObjectService(),
257257
agentService: {
258258
getAgentStatusById,
259+
authenticateAgentWithAccessToken,
259260
},
260261
datasourceService,
261262
registerExternalCallback: (...args: ExternalCallback) => {

x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('test acks handlers', () => {
7777
id: 'action1',
7878
},
7979
]),
80-
getAgentByAccessAPIKeyId: jest.fn().mockReturnValueOnce({
80+
authenticateAgentWithAccessToken: jest.fn().mockReturnValueOnce({
8181
id: 'agent',
8282
}),
8383
getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient),

x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import { RequestHandler } from 'kibana/server';
1010
import { TypeOf } from '@kbn/config-schema';
1111
import { PostAgentAcksRequestSchema } from '../../types/rest_spec';
12-
import * as APIKeyService from '../../services/api_keys';
1312
import { AcksService } from '../../services/agents';
1413
import { AgentEvent } from '../../../common/types/models';
1514
import { PostAgentAcksResponse } from '../../../common/types/rest_spec';
@@ -24,8 +23,7 @@ export const postAgentAcksHandlerBuilder = function (
2423
return async (context, request, response) => {
2524
try {
2625
const soClient = ackService.getSavedObjectsClientContract(request);
27-
const res = APIKeyService.parseApiKeyFromHeaders(request.headers);
28-
const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string);
26+
const agent = await ackService.authenticateAgentWithAccessToken(soClient, request);
2927
const agentEvents = request.body.events as AgentEvent[];
3028

3129
// validate that all events are for the authorized agent obtained from the api key

x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,7 @@ export const postAgentCheckinHandler: RequestHandler<
171171
> = async (context, request, response) => {
172172
try {
173173
const soClient = appContextService.getInternalUserSOClient(request);
174-
const res = APIKeyService.parseApiKeyFromHeaders(request.headers);
175-
const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId);
174+
const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request);
176175
const abortController = new AbortController();
177176
request.events.aborted$.subscribe(() => {
178177
abortController.abort();

x-pack/plugins/ingest_manager/server/routes/agent/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export const registerRoutes = (router: IRouter) => {
109109
},
110110
postAgentAcksHandlerBuilder({
111111
acknowledgeAgentActions: AgentService.acknowledgeAgentActions,
112-
getAgentByAccessAPIKeyId: AgentService.getAgentByAccessAPIKeyId,
112+
authenticateAgentWithAccessToken: AgentService.authenticateAgentWithAccessToken,
113113
getSavedObjectsClientContract: appContextService.getInternalUserSOClient.bind(
114114
appContextService
115115
),

x-pack/plugins/ingest_manager/server/services/agents/acks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,9 @@ export interface AcksService {
140140
actionIds: AgentEvent[]
141141
) => Promise<AgentAction[]>;
142142

143-
getAgentByAccessAPIKeyId: (
143+
authenticateAgentWithAccessToken: (
144144
soClient: SavedObjectsClientContract,
145-
accessAPIKeyId: string
145+
request: KibanaRequest
146146
) => Promise<Agent>;
147147

148148
getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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 { KibanaRequest } from 'kibana/server';
8+
import { savedObjectsClientMock } from 'src/core/server/mocks';
9+
10+
import { authenticateAgentWithAccessToken } from './authenticate';
11+
12+
describe('test agent autenticate services', () => {
13+
it('should succeed with a valid API key and an active agent', async () => {
14+
const mockSavedObjectsClient = savedObjectsClientMock.create();
15+
mockSavedObjectsClient.find.mockReturnValue(
16+
Promise.resolve({
17+
page: 1,
18+
per_page: 100,
19+
total: 1,
20+
saved_objects: [
21+
{
22+
id: 'agent1',
23+
type: 'agent',
24+
references: [],
25+
score: 0,
26+
attributes: {
27+
active: true,
28+
access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
29+
},
30+
},
31+
],
32+
})
33+
);
34+
await authenticateAgentWithAccessToken(mockSavedObjectsClient, {
35+
auth: { isAuthenticated: true },
36+
headers: {
37+
authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
38+
},
39+
} as KibanaRequest);
40+
});
41+
42+
it('should throw if the request is not authenticated', async () => {
43+
const mockSavedObjectsClient = savedObjectsClientMock.create();
44+
mockSavedObjectsClient.find.mockReturnValue(
45+
Promise.resolve({
46+
page: 1,
47+
per_page: 100,
48+
total: 1,
49+
saved_objects: [
50+
{
51+
id: 'agent1',
52+
type: 'agent',
53+
references: [],
54+
score: 0,
55+
attributes: {
56+
active: true,
57+
access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
58+
},
59+
},
60+
],
61+
})
62+
);
63+
expect(
64+
authenticateAgentWithAccessToken(mockSavedObjectsClient, {
65+
auth: { isAuthenticated: false },
66+
headers: {
67+
authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
68+
},
69+
} as KibanaRequest)
70+
).rejects.toThrow(/Request not authenticated/);
71+
});
72+
73+
it('should throw if the ApiKey headers is malformed', async () => {
74+
const mockSavedObjectsClient = savedObjectsClientMock.create();
75+
mockSavedObjectsClient.find.mockReturnValue(
76+
Promise.resolve({
77+
page: 1,
78+
per_page: 100,
79+
total: 1,
80+
saved_objects: [
81+
{
82+
id: 'agent1',
83+
type: 'agent',
84+
references: [],
85+
score: 0,
86+
attributes: {
87+
active: false,
88+
access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
89+
},
90+
},
91+
],
92+
})
93+
);
94+
expect(
95+
authenticateAgentWithAccessToken(mockSavedObjectsClient, {
96+
auth: { isAuthenticated: true },
97+
headers: {
98+
authorization: 'aaaa',
99+
},
100+
} as KibanaRequest)
101+
).rejects.toThrow(/Authorization header is malformed/);
102+
});
103+
104+
it('should throw if the agent is not active', async () => {
105+
const mockSavedObjectsClient = savedObjectsClientMock.create();
106+
mockSavedObjectsClient.find.mockReturnValue(
107+
Promise.resolve({
108+
page: 1,
109+
per_page: 100,
110+
total: 1,
111+
saved_objects: [
112+
{
113+
id: 'agent1',
114+
type: 'agent',
115+
references: [],
116+
score: 0,
117+
attributes: {
118+
active: false,
119+
access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
120+
},
121+
},
122+
],
123+
})
124+
);
125+
expect(
126+
authenticateAgentWithAccessToken(mockSavedObjectsClient, {
127+
auth: { isAuthenticated: true },
128+
headers: {
129+
authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
130+
},
131+
} as KibanaRequest)
132+
).rejects.toThrow(/Agent inactive/);
133+
});
134+
135+
it('should throw if there is no agent matching the API key', async () => {
136+
const mockSavedObjectsClient = savedObjectsClientMock.create();
137+
mockSavedObjectsClient.find.mockReturnValue(
138+
Promise.resolve({
139+
page: 1,
140+
per_page: 100,
141+
total: 1,
142+
saved_objects: [],
143+
})
144+
);
145+
expect(
146+
authenticateAgentWithAccessToken(mockSavedObjectsClient, {
147+
auth: { isAuthenticated: true },
148+
headers: {
149+
authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
150+
},
151+
} as KibanaRequest)
152+
).rejects.toThrow(/Agent not found/);
153+
});
154+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 Boom from 'boom';
8+
import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server';
9+
import { Agent } from '../../types';
10+
import * as APIKeyService from '../api_keys';
11+
import { getAgentByAccessAPIKeyId } from './crud';
12+
13+
export async function authenticateAgentWithAccessToken(
14+
soClient: SavedObjectsClientContract,
15+
request: KibanaRequest
16+
): Promise<Agent> {
17+
if (!request.auth.isAuthenticated) {
18+
throw Boom.unauthorized('Request not authenticated');
19+
}
20+
let res: { apiKey: string; apiKeyId: string };
21+
try {
22+
res = APIKeyService.parseApiKeyFromHeaders(request.headers);
23+
} catch (err) {
24+
throw Boom.unauthorized(err.message);
25+
}
26+
27+
const agent = await getAgentByAccessAPIKeyId(soClient, res.apiKeyId);
28+
29+
return agent;
30+
}

x-pack/plugins/ingest_manager/server/services/agents/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export * from './crud';
1414
export * from './update';
1515
export * from './actions';
1616
export * from './reassign';
17+
export * from './authenticate';

0 commit comments

Comments
 (0)