Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add createApiKey support to security plugin #42146

Merged
merged 10 commits into from
Jul 31, 2019
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()', () => {
test('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();
});

test('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?: string;
/**
* 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;
}
28 changes: 28 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 @@ -336,4 +337,31 @@ describe('setupAuthentication()', () => {
);
});
});

describe('createAPIKey()', () => {
let createAPIKey: jest.Mock;
beforeEach(async () => {
createAPIKey = (await setupAuthentication(mockSetupAuthenticationParams))
.createAPIKey as jest.Mock;
});

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: expect.anything(),
callAsCurrentUser: expect.anything(),
isSecurityFeatureDisabled: expect.anything(),
});
});
});
});
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 @@ -134,6 +135,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 @@ -37,6 +38,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