[IngestManager] Expose agent authentication using access key (#69650)
* [IngestManager] Expose agent authentication using access key * Add unit tests to authenticateAgentWithAccessToken service
This commit is contained in:
parent
a104e5ab0e
commit
14f975c899
|
@ -54,7 +54,7 @@ import {
|
|||
AgentService,
|
||||
datasourceService,
|
||||
} from './services';
|
||||
import { getAgentStatusById } from './services/agents';
|
||||
import { getAgentStatusById, authenticateAgentWithAccessToken } from './services/agents';
|
||||
import { CloudSetup } from '../../cloud/server';
|
||||
import { agentCheckinState } from './services/agents/checkin/state';
|
||||
|
||||
|
@ -256,6 +256,7 @@ export class IngestManagerPlugin
|
|||
esIndexPatternService: new ESIndexPatternSavedObjectService(),
|
||||
agentService: {
|
||||
getAgentStatusById,
|
||||
authenticateAgentWithAccessToken,
|
||||
},
|
||||
datasourceService,
|
||||
registerExternalCallback: (...args: ExternalCallback) => {
|
||||
|
|
|
@ -77,7 +77,7 @@ describe('test acks handlers', () => {
|
|||
id: 'action1',
|
||||
},
|
||||
]),
|
||||
getAgentByAccessAPIKeyId: jest.fn().mockReturnValueOnce({
|
||||
authenticateAgentWithAccessToken: jest.fn().mockReturnValueOnce({
|
||||
id: 'agent',
|
||||
}),
|
||||
getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient),
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import { RequestHandler } from 'kibana/server';
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { PostAgentAcksRequestSchema } from '../../types/rest_spec';
|
||||
import * as APIKeyService from '../../services/api_keys';
|
||||
import { AcksService } from '../../services/agents';
|
||||
import { AgentEvent } from '../../../common/types/models';
|
||||
import { PostAgentAcksResponse } from '../../../common/types/rest_spec';
|
||||
|
@ -24,8 +23,7 @@ export const postAgentAcksHandlerBuilder = function (
|
|||
return async (context, request, response) => {
|
||||
try {
|
||||
const soClient = ackService.getSavedObjectsClientContract(request);
|
||||
const res = APIKeyService.parseApiKeyFromHeaders(request.headers);
|
||||
const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string);
|
||||
const agent = await ackService.authenticateAgentWithAccessToken(soClient, request);
|
||||
const agentEvents = request.body.events as AgentEvent[];
|
||||
|
||||
// validate that all events are for the authorized agent obtained from the api key
|
||||
|
|
|
@ -171,8 +171,7 @@ export const postAgentCheckinHandler: RequestHandler<
|
|||
> = async (context, request, response) => {
|
||||
try {
|
||||
const soClient = appContextService.getInternalUserSOClient(request);
|
||||
const res = APIKeyService.parseApiKeyFromHeaders(request.headers);
|
||||
const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId);
|
||||
const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request);
|
||||
const abortController = new AbortController();
|
||||
request.events.aborted$.subscribe(() => {
|
||||
abortController.abort();
|
||||
|
|
|
@ -109,7 +109,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
},
|
||||
postAgentAcksHandlerBuilder({
|
||||
acknowledgeAgentActions: AgentService.acknowledgeAgentActions,
|
||||
getAgentByAccessAPIKeyId: AgentService.getAgentByAccessAPIKeyId,
|
||||
authenticateAgentWithAccessToken: AgentService.authenticateAgentWithAccessToken,
|
||||
getSavedObjectsClientContract: appContextService.getInternalUserSOClient.bind(
|
||||
appContextService
|
||||
),
|
||||
|
|
|
@ -140,9 +140,9 @@ export interface AcksService {
|
|||
actionIds: AgentEvent[]
|
||||
) => Promise<AgentAction[]>;
|
||||
|
||||
getAgentByAccessAPIKeyId: (
|
||||
authenticateAgentWithAccessToken: (
|
||||
soClient: SavedObjectsClientContract,
|
||||
accessAPIKeyId: string
|
||||
request: KibanaRequest
|
||||
) => Promise<Agent>;
|
||||
|
||||
getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract;
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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 { KibanaRequest } from 'kibana/server';
|
||||
import { savedObjectsClientMock } from 'src/core/server/mocks';
|
||||
|
||||
import { authenticateAgentWithAccessToken } from './authenticate';
|
||||
|
||||
describe('test agent autenticate services', () => {
|
||||
it('should succeed with a valid API key and an active agent', async () => {
|
||||
const mockSavedObjectsClient = savedObjectsClientMock.create();
|
||||
mockSavedObjectsClient.find.mockReturnValue(
|
||||
Promise.resolve({
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
references: [],
|
||||
score: 0,
|
||||
attributes: {
|
||||
active: true,
|
||||
access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
await authenticateAgentWithAccessToken(mockSavedObjectsClient, {
|
||||
auth: { isAuthenticated: true },
|
||||
headers: {
|
||||
authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
|
||||
},
|
||||
} as KibanaRequest);
|
||||
});
|
||||
|
||||
it('should throw if the request is not authenticated', async () => {
|
||||
const mockSavedObjectsClient = savedObjectsClientMock.create();
|
||||
mockSavedObjectsClient.find.mockReturnValue(
|
||||
Promise.resolve({
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
references: [],
|
||||
score: 0,
|
||||
attributes: {
|
||||
active: true,
|
||||
access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(
|
||||
authenticateAgentWithAccessToken(mockSavedObjectsClient, {
|
||||
auth: { isAuthenticated: false },
|
||||
headers: {
|
||||
authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
|
||||
},
|
||||
} as KibanaRequest)
|
||||
).rejects.toThrow(/Request not authenticated/);
|
||||
});
|
||||
|
||||
it('should throw if the ApiKey headers is malformed', async () => {
|
||||
const mockSavedObjectsClient = savedObjectsClientMock.create();
|
||||
mockSavedObjectsClient.find.mockReturnValue(
|
||||
Promise.resolve({
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
references: [],
|
||||
score: 0,
|
||||
attributes: {
|
||||
active: false,
|
||||
access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(
|
||||
authenticateAgentWithAccessToken(mockSavedObjectsClient, {
|
||||
auth: { isAuthenticated: true },
|
||||
headers: {
|
||||
authorization: 'aaaa',
|
||||
},
|
||||
} as KibanaRequest)
|
||||
).rejects.toThrow(/Authorization header is malformed/);
|
||||
});
|
||||
|
||||
it('should throw if the agent is not active', async () => {
|
||||
const mockSavedObjectsClient = savedObjectsClientMock.create();
|
||||
mockSavedObjectsClient.find.mockReturnValue(
|
||||
Promise.resolve({
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
references: [],
|
||||
score: 0,
|
||||
attributes: {
|
||||
active: false,
|
||||
access_api_key_id: 'pedTuHIBTEDt93wW0Fhr',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(
|
||||
authenticateAgentWithAccessToken(mockSavedObjectsClient, {
|
||||
auth: { isAuthenticated: true },
|
||||
headers: {
|
||||
authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
|
||||
},
|
||||
} as KibanaRequest)
|
||||
).rejects.toThrow(/Agent inactive/);
|
||||
});
|
||||
|
||||
it('should throw if there is no agent matching the API key', async () => {
|
||||
const mockSavedObjectsClient = savedObjectsClientMock.create();
|
||||
mockSavedObjectsClient.find.mockReturnValue(
|
||||
Promise.resolve({
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
total: 1,
|
||||
saved_objects: [],
|
||||
})
|
||||
);
|
||||
expect(
|
||||
authenticateAgentWithAccessToken(mockSavedObjectsClient, {
|
||||
auth: { isAuthenticated: true },
|
||||
headers: {
|
||||
authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==',
|
||||
},
|
||||
} as KibanaRequest)
|
||||
).rejects.toThrow(/Agent not found/);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server';
|
||||
import { Agent } from '../../types';
|
||||
import * as APIKeyService from '../api_keys';
|
||||
import { getAgentByAccessAPIKeyId } from './crud';
|
||||
|
||||
export async function authenticateAgentWithAccessToken(
|
||||
soClient: SavedObjectsClientContract,
|
||||
request: KibanaRequest
|
||||
): Promise<Agent> {
|
||||
if (!request.auth.isAuthenticated) {
|
||||
throw Boom.unauthorized('Request not authenticated');
|
||||
}
|
||||
let res: { apiKey: string; apiKeyId: string };
|
||||
try {
|
||||
res = APIKeyService.parseApiKeyFromHeaders(request.headers);
|
||||
} catch (err) {
|
||||
throw Boom.unauthorized(err.message);
|
||||
}
|
||||
|
||||
const agent = await getAgentByAccessAPIKeyId(soClient, res.apiKeyId);
|
||||
|
||||
return agent;
|
||||
}
|
|
@ -14,3 +14,4 @@ export * from './crud';
|
|||
export * from './update';
|
||||
export * from './actions';
|
||||
export * from './reassign';
|
||||
export * from './authenticate';
|
||||
|
|
Loading…
Reference in a new issue