Skip to content

Commit

Permalink
Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
tsullivan committed Dec 14, 2023
1 parent b730c9f commit 89a92fd
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 46 deletions.
1 change: 0 additions & 1 deletion x-pack/packages/security/plugin_types_server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export type {
CreateAPIKeyResult,
CreateRestAPIKeyParams,
GrantAPIKeyResult,
HasApiKeysOptions,
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
CreateRestAPIKeyWithKibanaPrivilegesParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,6 @@ export interface APIKeys {
*/
areAPIKeysEnabled(): Promise<boolean>;

/**
* Determines if the currently logged in user has created API keys
* @param apiKeyPrams ValidateAPIKeyParams.
*/
hasApiKeys(request: KibanaRequest, options: HasApiKeysOptions): Promise<boolean>;

/**
* Determines if Cross-Cluster API Keys are enabled in Elasticsearch.
*/
Expand Down Expand Up @@ -122,14 +116,6 @@ export interface ValidateAPIKeyParams {
api_key: string;
}

/**
*
*/
export interface HasApiKeysOptions {
ownerOnly: boolean;
validOnly: boolean;
}

/**
* Represents the params for invalidating multiple API keys
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
export type {
CreateAPIKeyParams,
CreateAPIKeyResult,
HasApiKeysOptions,
InvalidateAPIKeyResult,
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export type {
CreateRestAPIKeyParams,
CreateRestAPIKeyWithKibanaPrivilegesParams,
CreateCrossClusterAPIKeyParams,
HasApiKeysOptions,
InvalidateAPIKeyResult,
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
Expand Down
27 changes: 0 additions & 27 deletions x-pack/plugins/security/server/authentication/api_keys/api_keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import type {
CreateRestAPIKeyParams,
CreateRestAPIKeyWithKibanaPrivilegesParams,
GrantAPIKeyResult,
HasApiKeysOptions,
InvalidateAPIKeyResult,
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
Expand Down Expand Up @@ -84,32 +83,6 @@ export class APIKeys implements APIKeysType {
this.kibanaFeatures = kibanaFeatures;
}

/**
* Determines if currently-logged-in user has created any API Keys.
* NOTE: The current user may not have privileges to call the requred Elasticsearch API.
*/
async hasApiKeys(request: KibanaRequest, options: HasApiKeysOptions): Promise<boolean> {
const { ownerOnly, validOnly } = options;

try {
const scopedClusterClient = this.clusterClient.asScoped(request);
const result = await scopedClusterClient.asCurrentUser.security.getApiKey({
owner: ownerOnly,
});
const { api_keys: apiKeys } = result;

let countedKeys = apiKeys;
if (validOnly) {
countedKeys = countedKeys.filter((key) => !key.invalidated);
}

return countedKeys.length > 0;
} catch (e) {
this.logger.error(`Failed to determine if user has created any API keys`);
return false;
}
}

/**
* Determines if API Keys are enabled in Elasticsearch.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ export interface InternalAuthenticationServiceStart extends AuthenticationServic
| 'areAPIKeysEnabled'
| 'areCrossClusterAPIKeysEnabled'
| 'create'
| 'hasApiKeys'
| 'update'
| 'invalidate'
| 'validate'
Expand Down Expand Up @@ -367,7 +366,6 @@ export class AuthenticationService {
create: apiKeys.create.bind(apiKeys),
update: apiKeys.update.bind(apiKeys),
grantAsInternalUser: apiKeys.grantAsInternalUser.bind(apiKeys),
hasApiKeys: apiKeys.hasApiKeys.bind(apiKeys),
invalidate: apiKeys.invalidate.bind(apiKeys),
validate: apiKeys.validate.bind(apiKeys),
invalidateAsInternalUser: apiKeys.invalidateAsInternalUser.bind(apiKeys),
Expand Down
125 changes: 125 additions & 0 deletions x-pack/plugins/security/server/routes/api_keys/has.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import Boom from '@hapi/boom';

import { kibanaResponseFactory } from '@kbn/core/server';
import type { RequestHandler } from '@kbn/core/server';
import type { CustomRequestHandlerMock, ScopedClusterClientMock } from '@kbn/core/server/mocks';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
import type { DeeplyMockedKeys } from '@kbn/utility-types-jest';

import { defineHasApiKeysRoutes } from './has';
import type { InternalAuthenticationServiceStart } from '../../authentication';
import { authenticationServiceMock } from '../../authentication/authentication_service.mock';
import { routeDefinitionParamsMock } from '../index.mock';

describe('Has API Keys route', () => {
let routeHandler: RequestHandler<any, any, any, any>;
let authc: DeeplyMockedKeys<InternalAuthenticationServiceStart>;
let esClientMock: ScopedClusterClientMock;
let mockContext: CustomRequestHandlerMock<unknown>;

beforeEach(async () => {
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
authc = authenticationServiceMock.createStart();
mockRouteDefinitionParams.getAuthenticationService.mockReturnValue(authc);
defineHasApiKeysRoutes(mockRouteDefinitionParams);
[[, routeHandler]] = mockRouteDefinitionParams.router.get.mock.calls;
mockContext = coreMock.createCustomRequestHandlerContext({
core: coreMock.createRequestHandlerContext(),
licensing: licensingMock.createRequestHandlerContext(),
});

esClientMock = (await mockContext.core).elasticsearch.client;

authc.apiKeys.areAPIKeysEnabled.mockResolvedValue(true);
authc.apiKeys.areCrossClusterAPIKeysEnabled.mockResolvedValue(true);

esClientMock.asCurrentUser.security.getApiKey.mockResponse({
api_keys: [
{ id: '123', invalidated: false },
{ id: '456', invalidated: true },
],
} as any);
});

it('should calculate when user has API keys', async () => {
const response = await routeHandler(
mockContext,
httpServerMock.createKibanaRequest(),
kibanaResponseFactory
);

expect(response.payload).toEqual(
expect.objectContaining({
hasApiKeys: true,
})
);
});

it('should calculate when user does not have API keys', async () => {
esClientMock.asCurrentUser.security.getApiKey.mockResponse({
api_keys: [],
});

const response = await routeHandler(
mockContext,
httpServerMock.createKibanaRequest(),
kibanaResponseFactory
);

expect(response.payload).toEqual(
expect.objectContaining({
hasApiKeys: false,
})
);
});

it('should filter out invalidated API keys', async () => {
const response = await routeHandler(
mockContext,
httpServerMock.createKibanaRequest(),
kibanaResponseFactory
);

expect(response.status).toBe(200);
expect(response.payload?.hasApiKeys).toBe(true);
});

it('should return `404` if API keys are disabled', async () => {
authc.apiKeys.areAPIKeysEnabled.mockResolvedValue(false);

const response = await routeHandler(
mockContext,
httpServerMock.createKibanaRequest(),
kibanaResponseFactory
);

expect(response.status).toBe(404);
expect(response.payload).toEqual({
message:
"API keys are disabled in Elasticsearch. To use API keys enable 'xpack.security.authc.api_key.enabled' setting.",
});
});

it('should forward error from Elasticsearch GET API keys endpoint', async () => {
const error = Boom.forbidden('test not acceptable message');
esClientMock.asCurrentUser.security.getApiKey.mockResponseImplementation(() => {
throw error;
});

const response = await routeHandler(
mockContext,
httpServerMock.createKibanaRequest(),
kibanaResponseFactory
);

expect(response.status).toBe(403);
expect(response.payload).toEqual(error);
});
});
65 changes: 65 additions & 0 deletions x-pack/plugins/security/server/routes/api_keys/has.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { RouteDefinitionParams } from '..';
import { wrapIntoCustomErrorResponse } from '../../errors';
import { createLicensedRouteHandler } from '../licensed_route_handler';

/**
* Response of Kibana Has API keys endpoint.
*/
export interface HasAPIKeysResult {
hasApiKeys: boolean;
}

export function defineHasApiKeysRoutes({
router,
getAuthenticationService,
}: RouteDefinitionParams) {
router.get(
{
path: '/internal/security/has_api_keys',
validate: false,
options: {
access: 'internal',
},
},
createLicensedRouteHandler(async (context, _request, response) => {
try {
// copied logic from get.ts
const esClient = (await context.core).elasticsearch.client;
const authenticationService = getAuthenticationService();

const areApiKeysEnabled = await authenticationService.apiKeys.areAPIKeysEnabled();

if (!areApiKeysEnabled) {
return response.notFound({
body: {
message:
"API keys are disabled in Elasticsearch. To use API keys enable 'xpack.security.authc.api_key.enabled' setting.",
},
});
}

const apiResponse = await esClient.asCurrentUser.security.getApiKey({
owner: true,
});

const validKeys = apiResponse.api_keys.filter(({ invalidated }) => !invalidated);

// simply return true if the result array is non-empty
return response.ok<HasAPIKeysResult>({
body: {
hasApiKeys: validKeys.length > 0,
},
});
} catch (error) {
return response.customError(wrapIntoCustomErrorResponse(error));
}
})
);
}
2 changes: 2 additions & 0 deletions x-pack/plugins/security/server/routes/api_keys/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { defineCreateApiKeyRoutes } from './create';
import { defineEnabledApiKeysRoutes } from './enabled';
import { defineGetApiKeysRoutes } from './get';
import { defineHasApiKeysRoutes } from './has';
import { defineInvalidateApiKeysRoutes } from './invalidate';
import { defineUpdateApiKeyRoutes } from './update';
import type { RouteDefinitionParams } from '..';
Expand All @@ -24,6 +25,7 @@ export type { GetAPIKeysResult } from './get';
export function defineApiKeysRoutes(params: RouteDefinitionParams) {
defineEnabledApiKeysRoutes(params);
defineGetApiKeysRoutes(params);
defineHasApiKeysRoutes(params);
defineCreateApiKeyRoutes(params);
defineUpdateApiKeyRoutes(params);
defineInvalidateApiKeysRoutes(params);
Expand Down
14 changes: 14 additions & 0 deletions x-pack/test/api_integration/apis/security/api_keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,19 @@ export default function ({ getService }: FtrProviderContext) {
});
});
});

describe('GET /internal/security/has_api_keys', () => {
it('should return false by default', async () => {
await supertest
.get('/internal/security/has_api_keys')
.set('kbn-xsrf', 'xxx')
.send()
.expect(200)
.then((response: Record<string, any>) => {
const payload = response.body;
expect(payload).to.eql({ hasApiKeys: false });
});
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,35 @@ export default function ({ getService }: FtrProviderContext) {
expect(status).toBe(200);
});

it('has_api_keys', async () => {
let body: unknown;
let status: number;

({ body, status } = await supertest
.get('/internal/security/has_api_keys')
.set(svlCommonApi.getCommonRequestHeader()));
// expect a rejection because we're not using the internal header
expect(body).toEqual({
statusCode: 400,
error: 'Bad Request',
message: expect.stringContaining(
'method [get] exists but is not available with the current configuration'
),
});
expect(status).toBe(400);

({ body, status } = await supertest
.get('/internal/security/has_api_keys')
.set(svlCommonApi.getInternalRequestHeader()));
// expect success because we're using the internal header
expect(body).toEqual(
expect.objectContaining({
apiKeys: expect.arrayContaining([expect.objectContaining({ id: roleMapping.id })]),
})
);
expect(status).toBe(200);
});

it('invalidate', async () => {
let body: unknown;
let status: number;
Expand Down

0 comments on commit 89a92fd

Please sign in to comment.