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
5 changes: 4 additions & 1 deletion x-pack/legacy/plugins/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ export const security = (kibana) => new kibana.Plugin({
// to re-compute the license check results for this plugin
xpackInfoFeature.registerLicenseCheckResultsGenerator(checkLicense);

server.expose({ getUser: request => securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)) });
server.expose({
getUser: request => securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)),
createApiKey: (request, body) => securityPlugin.authc.createApiKey(KibanaRequest.from(request), body),
});

const { savedObjects } = server;

Expand Down
11 changes: 11 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,16 @@
}
]
});

/**
* Perform a [shield.createApiKey](Creates an API Key for the current user) request
*/
shield.createApiKey = ca({
method: 'POST',
needBody: true,
url: {
fmt: '/_security/api_key',
},
});
};
}));
58 changes: 58 additions & 0 deletions x-pack/plugins/security/server/authentication/api_key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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_key';

const mockCallCluster = jest.fn();

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

describe('createApiKey()', () => {
test('returns null when security feature is disabled', async () => {
const result = await createApiKey({
body: {
name: '',
role_descriptors: {},
},
callCluster: mockCallCluster,
isSecurityFeatureDisabled: () => true,
});
expect(result).toBeNull();
});

test('calls callCluster with proper body arguments', async () => {
mockCallCluster.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',
},
callCluster: mockCallCluster,
isSecurityFeatureDisabled: () => false,
});
expect(result).toMatchInlineSnapshot(`
Object {
"api_key": "abc123",
"expiration": "1d",
"id": "123",
"name": "key-name",
}
`);
expect(mockCallCluster).toHaveBeenCalledWith('shield.createApiKey', {
body: {
name: 'key-name',
role_descriptors: { foo: true },
expiration: '1d',
},
});
});
});
33 changes: 33 additions & 0 deletions x-pack/plugins/security/server/authentication/api_key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.
*/

export interface CreateApiKeyOptions {
callCluster: any;
isSecurityFeatureDisabled: () => boolean;
body: {
name: string;
role_descriptors: Record<string, any>;
expiration?: string;
};
}

export interface CreateApiKeyResult {
id: string;
name: string;
expiration?: string;
api_key: string;
}

export async function createApiKey({
callCluster,
body,
isSecurityFeatureDisabled,
}: CreateApiKeyOptions): Promise<CreateApiKeyResult | null> {
if (isSecurityFeatureDisabled()) {
return null;
}
return await callCluster('shield.createApiKey', { body });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it'd be great if we can at least debug-log here whenever new API key is requested. Later, it feels, we'll need to issue an audit log event here as well.... (ping @kobelb).

All in all it seems we'll end up with a APIKeys.create/invalidate/retrieve/* class here that can accept logger/auditLogger/clusterClient/isSecurityFeatureDisabled at the initialization (constructor) stage :) Having said that I'm totally fine to keep it as is right now if you're not up to this kind of generalization at this stage.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the time being, I think we can ignore the audit logging here... We only have to write entries to the Kibana audit log when we're performing authentication/authorization ourselves in Kibana and can't defer to the Elasticsearch audit log. At least, that's been the current thinking, which is entirely up for debate once we start to focus more on audit logging.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only have to write entries to the Kibana audit log when we're performing authentication/authorization ourselves in Kibana and can't defer to the Elasticsearch audit log.

Right, good point, thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the comment stay for now or be removed?

}
54 changes: 54 additions & 0 deletions x-pack/plugins/security/server/authentication/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { getErrorStatusCode } from '../errors';
import { LegacyAPI } from '../plugin';
import { AuthenticationResult } from './authentication_result';
import { setupAuthentication } from '.';
import { CreateApiKeyResult, CreateApiKeyOptions } from './api_key';

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

describe('createApiKey()', () => {
let createApiKey: (
r: KibanaRequest,
body: CreateApiKeyOptions['body']
) => Promise<CreateApiKeyResult | null>;
beforeEach(async () => {
createApiKey = (await setupAuthentication(mockSetupAuthenticationParams)).createApiKey;
});

it('returns `null` if Security is disabled', async () => {
mockXpackInfo.feature.mockReturnValue(mockXPackFeature({ isEnabled: false }));

await expect(
createApiKey(httpServerMock.createKibanaRequest(), {
name: 'my-key',
role_descriptors: {},
expiration: '1d',
})
).resolves.toBe(null);
});

it('fails if `authenticate` call fails', async () => {
const failureReason = new Error('Something went wrong');
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason);

await expect(
createApiKey(httpServerMock.createKibanaRequest(), {
name: 'my-key',
role_descriptors: {},
expiration: '1d',
})
).rejects.toBe(failureReason);
});

it('returns result of `createApiKey` call.', async () => {
const mockKey = {
id: '123',
name: 'my-key',
expiration: '1d',
api_key: '123abc',
};
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(mockKey);

await expect(
createApiKey(httpServerMock.createKibanaRequest(), {
name: 'my-key',
role_descriptors: {},
expiration: '1d',
})
).resolves.toBe(mockKey);
});
});
});
7 changes: 7 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_key';

export { canRedirectRequest } from './can_redirect_request';
export { Authenticator, ProviderLoginAttempt } from './authenticator';
Expand Down Expand Up @@ -134,6 +135,12 @@ export async function setupAuthentication({
login: authenticator.login.bind(authenticator),
logout: authenticator.logout.bind(authenticator),
getCurrentUser,
createApiKey: (request: KibanaRequest, body: CreateApiKeyOptions['body']) =>
createApiKey({
body,
isSecurityFeatureDisabled,
callCluster: 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