Skip to content

Commit

Permalink
Add createApiKey support to security plugin (#42146) (#42381)
Browse files Browse the repository at this point in the history
* Add createApiKey support to security plugin

* Expiration is optional

* Start moving code to new platform

* Add unit tests

* Fix jest test

* Apply PR feedback

* Apply PR feedback

* Apply PR feedback pt2
  • Loading branch information
mikecote authored Jul 31, 2019
1 parent d423420 commit 38226a1
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 0 deletions.
19 changes: 19 additions & 0 deletions x-pack/legacy/server/lib/esjs_shield_plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,5 +497,24 @@
}
]
});

/**
* Creates an API key in Elasticsearch for the current user.
*
* @param {string} name A name for this API key
* @param {object} role_descriptors Role descriptors for this API key, if not
* provided then permissions of authenticated user are applied.
* @param {string} [expiration] Optional expiration for the API key being generated. If expiration
* is not provided then the API keys do not expire.
*
* @returns {{id: string, name: string, api_key: string, expiration?: number}}
*/
shield.createAPIKey = ca({
method: 'POST',
needBody: true,
url: {
fmt: '/_security/api_key',
},
});
};
}));
60 changes: 60 additions & 0 deletions x-pack/plugins/security/server/authentication/api_keys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 { createAPIKey } from './api_keys';
import { loggingServiceMock } from '../../../../../src/core/server/mocks';

const mockCallAsCurrentUser = jest.fn();

beforeAll(() => jest.resetAllMocks());

describe('createAPIKey()', () => {
it('returns null when security feature is disabled', async () => {
const result = await createAPIKey({
body: {
name: '',
role_descriptors: {},
},
loggers: loggingServiceMock.create(),
callAsCurrentUser: mockCallAsCurrentUser,
isSecurityFeatureDisabled: () => true,
});
expect(result).toBeNull();
expect(mockCallAsCurrentUser).not.toHaveBeenCalled();
});

it('calls callCluster with proper body arguments', async () => {
mockCallAsCurrentUser.mockResolvedValueOnce({
id: '123',
name: 'key-name',
expiration: '1d',
api_key: 'abc123',
});
const result = await createAPIKey({
body: {
name: 'key-name',
role_descriptors: { foo: true },
expiration: '1d',
},
loggers: loggingServiceMock.create(),
callAsCurrentUser: mockCallAsCurrentUser,
isSecurityFeatureDisabled: () => false,
});
expect(result).toEqual({
api_key: 'abc123',
expiration: '1d',
id: '123',
name: 'key-name',
});
expect(mockCallAsCurrentUser).toHaveBeenCalledWith('shield.createAPIKey', {
body: {
name: 'key-name',
role_descriptors: { foo: true },
expiration: '1d',
},
});
});
});
65 changes: 65 additions & 0 deletions x-pack/plugins/security/server/authentication/api_keys.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;
* you may not use this file except in compliance with the Elastic License.
*/

import { LoggerFactory, ScopedClusterClient } from '../../../../../src/core/server';

export interface CreateAPIKeyOptions {
loggers: LoggerFactory;
callAsCurrentUser: ScopedClusterClient['callAsCurrentUser'];
isSecurityFeatureDisabled: () => boolean;
body: {
name: string;
role_descriptors: Record<string, any>;
expiration?: string;
};
}

/**
* The return value when creating an API key in Elasticsearch. The API key returned by this API
* can then be used by sending a request with a Authorization header with a value having the
* prefix ApiKey `{token}` where token is id and api_key joined by a colon `{id}:{api_key}` and
* then encoded to base64.
*/
export interface CreateAPIKeyResult {
/**
* Unique id for this API key
*/
id: string;
/**
* Name for this API key
*/
name: string;
/**
* Optional expiration in milliseconds for this API key
*/
expiration?: number;
/**
* Generated API key
*/
api_key: string;
}

export async function createAPIKey({
body,
loggers,
callAsCurrentUser,
isSecurityFeatureDisabled,
}: CreateAPIKeyOptions): Promise<CreateAPIKeyResult | null> {
const logger = loggers.get('api-keys');

if (isSecurityFeatureDisabled()) {
return null;
}

logger.debug('Trying to create an API key');

// User needs `manage_api_key` privilege to use this API
const key = (await callAsCurrentUser('shield.createAPIKey', { body })) as CreateAPIKeyResult;

logger.debug('API key was created successfully');

return key;
}
31 changes: 31 additions & 0 deletions x-pack/plugins/security/server/authentication/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

jest.mock('./authenticator');
jest.mock('./api_keys', () => ({ createAPIKey: jest.fn() }));

import Boom from 'boom';
import { errors } from 'elasticsearch';
Expand Down Expand Up @@ -35,6 +36,7 @@ import { getErrorStatusCode } from '../errors';
import { LegacyAPI } from '../plugin';
import { AuthenticationResult } from './authentication_result';
import { setupAuthentication } from '.';
import { CreateAPIKeyResult, CreateAPIKeyOptions } from './api_keys';

function mockXPackFeature({ isEnabled = true }: Partial<{ isEnabled: boolean }> = {}) {
return {
Expand Down Expand Up @@ -379,4 +381,33 @@ describe('setupAuthentication()', () => {
);
});
});

describe('createAPIKey()', () => {
let createAPIKey: (
request: KibanaRequest,
body: CreateAPIKeyOptions['body']
) => Promise<CreateAPIKeyResult | null>;
beforeEach(async () => {
createAPIKey = (await setupAuthentication(mockSetupAuthenticationParams)).createAPIKey;
});

it('calls createAPIKey with given arguments', async () => {
const { createAPIKey: createAPIKeyMock } = jest.requireMock('./api_keys');
const options = {
name: 'my-key',
role_descriptors: {},
expiration: '1d',
};
createAPIKeyMock.mockResolvedValueOnce({ success: true });
await expect(createAPIKey(httpServerMock.createKibanaRequest(), options)).resolves.toEqual({
success: true,
});
expect(createAPIKeyMock).toHaveBeenCalledWith({
body: options,
loggers: mockSetupAuthenticationParams.loggers,
callAsCurrentUser: mockScopedClusterClient.callAsCurrentUser,
isSecurityFeatureDisabled: expect.any(Function),
});
});
});
});
8 changes: 8 additions & 0 deletions x-pack/plugins/security/server/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ConfigType } from '../config';
import { getErrorStatusCode, wrapError } from '../errors';
import { Authenticator, ProviderSession } from './authenticator';
import { LegacyAPI } from '../plugin';
import { createAPIKey, CreateAPIKeyOptions } from './api_keys';

export { canRedirectRequest } from './can_redirect_request';
export { Authenticator, ProviderLoginAttempt } from './authenticator';
Expand Down Expand Up @@ -148,6 +149,13 @@ export async function setupAuthentication({
login: authenticator.login.bind(authenticator),
logout: authenticator.logout.bind(authenticator),
getCurrentUser,
createAPIKey: (request: KibanaRequest, body: CreateAPIKeyOptions['body']) =>
createAPIKey({
body,
loggers,
isSecurityFeatureDisabled,
callAsCurrentUser: clusterClient.asScoped(request).callAsCurrentUser,
}),
isAuthenticated: async (request: KibanaRequest) => {
try {
await getCurrentUser(request);
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('Security Plugin', () => {
await expect(plugin.setup(mockCoreSetup)).resolves.toMatchInlineSnapshot(`
Object {
"authc": Object {
"createAPIKey": [Function],
"getCurrentUser": [Function],
"isAuthenticated": [Function],
"login": [Function],
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/security/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { XPackInfo } from '../../../legacy/plugins/xpack_main/server/lib/xpack_i
import { AuthenticatedUser } from '../common/model';
import { Authenticator, setupAuthentication } from './authentication';
import { createConfig$ } from './config';
import { CreateAPIKeyOptions, CreateAPIKeyResult } from './authentication/api_keys';

/**
* Describes a set of APIs that is available in the legacy platform only and required by this plugin
Expand All @@ -38,6 +39,10 @@ export interface PluginSetupContract {
logout: Authenticator['logout'];
getCurrentUser: (request: KibanaRequest) => Promise<AuthenticatedUser | null>;
isAuthenticated: (request: KibanaRequest) => Promise<boolean>;
createAPIKey: (
request: KibanaRequest,
body: CreateAPIKeyOptions['body']
) => Promise<CreateAPIKeyResult | null>;
};

config: RecursiveReadonly<{
Expand Down

0 comments on commit 38226a1

Please sign in to comment.