-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Security/APIKey Service] Internal API endpoint do determine if user …
…has API keys (#172884) ## Summary This PR enhances the Security server plugin APIKey service with a new method to determine if the user has API keys. The service is integrated into the "No Data Page" server plugin as a new HTTP route. This route is called when needed to direct users effectively through their getting started experience. Pulled from #172225 [Context](#172225 (comment)) > ...we [sh]ould introduce whatever functionality we required into the security plugin's API Key Service. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Aleh Zasypkin <[email protected]>
- Loading branch information
Showing
5 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
113 changes: 113 additions & 0 deletions
113
x-pack/plugins/security/server/routes/api_keys/has_active.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
/* | ||
* 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 { 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_active'; | ||
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, | ||
}) | ||
); | ||
expect(esClientMock.asCurrentUser.security.getApiKey).toHaveBeenCalledTimes(1); | ||
expect(esClientMock.asCurrentUser.security.getApiKey).toHaveBeenCalledWith({ | ||
owner: true, | ||
active_only: 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.", | ||
}); | ||
}); | ||
}); |
59 changes: 59 additions & 0 deletions
59
x-pack/plugins/security/server/routes/api_keys/has_active.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/* | ||
* 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 { 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/api_key/_has_active', | ||
validate: false, | ||
options: { | ||
access: 'internal', | ||
}, | ||
}, | ||
createLicensedRouteHandler(async (context, _request, response) => { | ||
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 { api_keys: apiKeys } = await esClient.asCurrentUser.security.getApiKey({ | ||
owner: true, | ||
// @ts-expect-error @elastic/elasticsearch SecurityGetApiKeyRequest.active_only: boolean | undefined | ||
active_only: true, | ||
}); | ||
|
||
// simply return true if the result array is non-empty | ||
return response.ok<HasAPIKeysResult>({ | ||
body: { | ||
hasApiKeys: apiKeys.length > 0, | ||
}, | ||
}); | ||
}) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
59 changes: 59 additions & 0 deletions
59
x-pack/test/security_api_integration/tests/api_keys/has_active_key.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/* | ||
* 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 expect from '@kbn/expect'; | ||
import type { ApiKey } from '@kbn/security-plugin/common/model'; | ||
import type { FtrProviderContext } from '../../ftr_provider_context'; | ||
|
||
export default function ({ getService }: FtrProviderContext) { | ||
const supertest = getService('supertest'); | ||
|
||
const createKey = async () => { | ||
const { body: apiKey } = await supertest | ||
.post('/api_keys/_grant') | ||
.set('kbn-xsrf', 'xxx') | ||
.send({ name: 'an-actual-api-key' }) | ||
.expect(200); | ||
expect(apiKey.name).to.eql('an-actual-api-key'); | ||
return apiKey; | ||
}; | ||
|
||
const cleanup = async () => { | ||
// get existing keys which would affect test results | ||
const { body: getResponseBody } = await supertest.get('/internal/security/api_key').expect(200); | ||
const apiKeys: ApiKey[] = getResponseBody.apiKeys; | ||
const existing = apiKeys.map(({ id, name }) => ({ id, name })); | ||
|
||
// invalidate the keys | ||
await supertest | ||
.post(`/internal/security/api_key/invalidate`) | ||
.set('kbn-xsrf', 'xxx') | ||
.send({ apiKeys: existing, isAdmin: false }) | ||
.expect(200, { itemsInvalidated: existing, errors: [] }); | ||
}; | ||
|
||
describe('Has Active API Keys: _has_active', () => { | ||
before(cleanup); | ||
after(cleanup); | ||
|
||
it('detects when user has no API Keys', async () => { | ||
await supertest | ||
.get('/internal/security/api_key/_has_active') | ||
.set('kbn-xsrf', 'xxx') | ||
.expect(200, { hasApiKeys: false }); | ||
}); | ||
|
||
it('detects when user has some API Keys', async () => { | ||
await createKey(); | ||
|
||
await supertest | ||
.get('/internal/security/api_key/_has_active') | ||
.set('kbn-xsrf', 'xxx') | ||
.expect(200, { hasApiKeys: true }); | ||
}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters