Add createApiKey support to security plugin (#42146)
* 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
This commit is contained in:
parent
33fea2f391
commit
011c04f9ca
|
@ -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',
|
||||
},
|
||||
});
|
||||
};
|
||||
}));
|
||||
|
|
|
@ -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
x-pack/plugins/security/server/authentication/api_keys.ts
Normal file
65
x-pack/plugins/security/server/authentication/api_keys.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
jest.mock('./authenticator');
|
||||
jest.mock('./api_keys', () => ({ createAPIKey: jest.fn() }));
|
||||
|
||||
import Boom from 'boom';
|
||||
import { errors } from 'elasticsearch';
|
||||
|
@ -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 {
|
||||
|
@ -336,4 +338,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),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
@ -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);
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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
|
||||
|
@ -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<{
|
||||
|
|
Loading…
Reference in a new issue