Introduce PKI authentication provider. (#42606)
This commit is contained in:
parent
3cc4653c96
commit
17106e8a78
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
import { Request } from 'hapi';
|
||||
import { merge } from 'lodash';
|
||||
import { Socket } from 'net';
|
||||
|
||||
import querystring from 'querystring';
|
||||
|
||||
|
@ -37,6 +38,7 @@ interface RequestFixtureOptions {
|
|||
query?: Record<string, any>;
|
||||
path?: string;
|
||||
method?: RouteMethod;
|
||||
socket?: Socket;
|
||||
}
|
||||
|
||||
function createKibanaRequestMock({
|
||||
|
@ -46,6 +48,7 @@ function createKibanaRequestMock({
|
|||
body = {},
|
||||
query = {},
|
||||
method = 'get',
|
||||
socket = new Socket(),
|
||||
}: RequestFixtureOptions = {}) {
|
||||
const queryString = querystring.stringify(query);
|
||||
return KibanaRequest.from(
|
||||
|
@ -63,7 +66,7 @@ function createKibanaRequestMock({
|
|||
},
|
||||
route: { settings: {} },
|
||||
raw: {
|
||||
req: {},
|
||||
req: { socket },
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
|
|
|
@ -536,5 +536,21 @@
|
|||
fmt: '/_security/api_key',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets an access token in exchange to the certificate chain for the target subject distinguished name.
|
||||
*
|
||||
* @param {string[]} x509_certificate_chain An ordered array of base64-encoded (Section 4 of RFC4648 - not
|
||||
* base64url-encoded) DER PKIX certificate values.
|
||||
*
|
||||
* @returns {{access_token: string, type: string, expires_in: number}}
|
||||
*/
|
||||
shield.delegatePKI = ca({
|
||||
method: 'POST',
|
||||
needBody: true,
|
||||
url: {
|
||||
fmt: '/_security/delegate_pki',
|
||||
},
|
||||
});
|
||||
};
|
||||
}));
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
SAMLAuthenticationProvider,
|
||||
TokenAuthenticationProvider,
|
||||
OIDCAuthenticationProvider,
|
||||
PKIAuthenticationProvider,
|
||||
isSAMLRequestQuery,
|
||||
} from './providers';
|
||||
import { AuthenticationResult } from './authentication_result';
|
||||
|
@ -98,6 +99,7 @@ const providerMap = new Map<
|
|||
['saml', SAMLAuthenticationProvider],
|
||||
['token', TokenAuthenticationProvider],
|
||||
['oidc', OIDCAuthenticationProvider],
|
||||
['pki', PKIAuthenticationProvider],
|
||||
]);
|
||||
|
||||
function assertRequest(request: KibanaRequest) {
|
||||
|
|
|
@ -7,12 +7,20 @@
|
|||
import sinon from 'sinon';
|
||||
import { ScopedClusterClient } from '../../../../../../src/core/server';
|
||||
import { Tokens } from '../tokens';
|
||||
import { loggingServiceMock, httpServiceMock } from '../../../../../../src/core/server/mocks';
|
||||
import {
|
||||
loggingServiceMock,
|
||||
httpServiceMock,
|
||||
elasticsearchServiceMock,
|
||||
} from '../../../../../../src/core/server/mocks';
|
||||
|
||||
export type MockAuthenticationProviderOptions = ReturnType<
|
||||
typeof mockAuthenticationProviderOptions
|
||||
>;
|
||||
|
||||
export type MockAuthenticationProviderOptionsWithJest = ReturnType<
|
||||
typeof mockAuthenticationProviderOptionsWithJest
|
||||
>;
|
||||
|
||||
export function mockScopedClusterClient(
|
||||
client: MockAuthenticationProviderOptions['client'],
|
||||
requestMatcher: sinon.SinonMatcher = sinon.match.any
|
||||
|
@ -34,3 +42,16 @@ export function mockAuthenticationProviderOptions() {
|
|||
tokens: sinon.createStubInstance(Tokens),
|
||||
};
|
||||
}
|
||||
|
||||
// Will be renamed to mockAuthenticationProviderOptions as soon as we migrate all providers tests to Jest.
|
||||
export function mockAuthenticationProviderOptionsWithJest() {
|
||||
const basePath = httpServiceMock.createSetupContract().basePath;
|
||||
basePath.get.mockReturnValue('/base-path');
|
||||
|
||||
return {
|
||||
client: elasticsearchServiceMock.createClusterClient(),
|
||||
logger: loggingServiceMock.create().get(),
|
||||
basePath,
|
||||
tokens: { refresh: jest.fn(), invalidate: jest.fn() },
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,3 +14,4 @@ export { KerberosAuthenticationProvider } from './kerberos';
|
|||
export { SAMLAuthenticationProvider, isSAMLRequestQuery } from './saml';
|
||||
export { TokenAuthenticationProvider } from './token';
|
||||
export { OIDCAuthenticationProvider, OIDCAuthenticationFlow } from './oidc';
|
||||
export { PKIAuthenticationProvider } from './pki';
|
||||
|
|
|
@ -0,0 +1,589 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('net');
|
||||
jest.mock('tls');
|
||||
|
||||
import { PeerCertificate, TLSSocket } from 'tls';
|
||||
import { errors } from 'elasticsearch';
|
||||
|
||||
import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks';
|
||||
import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock';
|
||||
import {
|
||||
MockAuthenticationProviderOptionsWithJest,
|
||||
mockAuthenticationProviderOptionsWithJest,
|
||||
} from './base.mock';
|
||||
|
||||
import { PKIAuthenticationProvider } from './pki';
|
||||
import {
|
||||
ElasticsearchErrorHelpers,
|
||||
ScopedClusterClient,
|
||||
} from '../../../../../../src/core/server/elasticsearch';
|
||||
import { Socket } from 'net';
|
||||
import { getErrorStatusCode } from '../../errors';
|
||||
|
||||
interface MockPeerCertificate extends Partial<PeerCertificate> {
|
||||
issuerCertificate: MockPeerCertificate;
|
||||
fingerprint256: string;
|
||||
}
|
||||
|
||||
function getMockPeerCertificate(chain: string[] | string) {
|
||||
const mockPeerCertificate = {} as MockPeerCertificate;
|
||||
|
||||
(Array.isArray(chain) ? chain : [chain]).reduce(
|
||||
(certificate, fingerprint, index, fingerprintChain) => {
|
||||
certificate.fingerprint256 = fingerprint;
|
||||
certificate.raw = { toString: (enc: string) => `fingerprint:${fingerprint}:${enc}` };
|
||||
|
||||
// Imitate self-signed certificate that is issuer for itself.
|
||||
certificate.issuerCertificate = index === fingerprintChain.length - 1 ? certificate : {};
|
||||
|
||||
return certificate.issuerCertificate;
|
||||
},
|
||||
mockPeerCertificate as Record<string, any>
|
||||
);
|
||||
|
||||
return mockPeerCertificate;
|
||||
}
|
||||
|
||||
function getMockSocket({
|
||||
authorized = false,
|
||||
peerCertificate = null,
|
||||
}: {
|
||||
authorized?: boolean;
|
||||
peerCertificate?: MockPeerCertificate | null;
|
||||
} = {}) {
|
||||
const socket = new TLSSocket(new Socket());
|
||||
socket.authorized = authorized;
|
||||
socket.getPeerCertificate = jest.fn().mockReturnValue(peerCertificate);
|
||||
return socket;
|
||||
}
|
||||
|
||||
describe('PKIAuthenticationProvider', () => {
|
||||
let provider: PKIAuthenticationProvider;
|
||||
let mockOptions: MockAuthenticationProviderOptionsWithJest;
|
||||
beforeEach(() => {
|
||||
mockOptions = mockAuthenticationProviderOptionsWithJest();
|
||||
provider = new PKIAuthenticationProvider(mockOptions);
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('`authenticate` method', () => {
|
||||
it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: { authorization: 'Basic some:credentials' },
|
||||
});
|
||||
const state = {
|
||||
accessToken: 'some-valid-token',
|
||||
peerCertificateFingerprint256: '2A:7A:C2:DD',
|
||||
};
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, state);
|
||||
|
||||
expect(mockOptions.client.asScoped).not.toHaveBeenCalled();
|
||||
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
|
||||
expect(request.headers.authorization).toBe('Basic some:credentials');
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not handle requests without certificate.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
socket: getMockSocket({ authorized: true }),
|
||||
});
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, null);
|
||||
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
expect(mockOptions.client.asScoped).not.toHaveBeenCalled();
|
||||
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not handle unauthorized requests.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
socket: getMockSocket({ peerCertificate: getMockPeerCertificate('2A:7A:C2:DD') }),
|
||||
});
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, null);
|
||||
|
||||
expect(authenticationResult.notHandled()).toBe(true);
|
||||
expect(mockOptions.client.asScoped).not.toHaveBeenCalled();
|
||||
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fails with non-401 error if state is available, peer is authorized, but certificate is not available.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
socket: getMockSocket({ authorized: true }),
|
||||
});
|
||||
|
||||
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, state);
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toMatchInlineSnapshot(
|
||||
`[Error: Peer certificate is not available]`
|
||||
);
|
||||
expect(authenticationResult.authResponseHeaders).toBeUndefined();
|
||||
expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('invalidates token and fails with 401 if state is present, but peer certificate is not.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({ socket: getMockSocket() });
|
||||
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, state);
|
||||
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(getErrorStatusCode(authenticationResult.error)).toBe(401);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
|
||||
accessToken: state.accessToken,
|
||||
});
|
||||
});
|
||||
|
||||
it('invalidates token and fails with 401 if new certificate is present, but not authorized.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
socket: getMockSocket({ peerCertificate: getMockPeerCertificate('2A:7A:C2:DD') }),
|
||||
});
|
||||
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, state);
|
||||
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(getErrorStatusCode(authenticationResult.error)).toBe(401);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
|
||||
accessToken: state.accessToken,
|
||||
});
|
||||
});
|
||||
|
||||
it('gets an access token in exchange to peer certificate chain and stores it in the state.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: {},
|
||||
socket: getMockSocket({
|
||||
authorized: true,
|
||||
peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']),
|
||||
}),
|
||||
});
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
|
||||
body: {
|
||||
x509_certificate_chain: [
|
||||
'fingerprint:2A:7A:C2:DD:base64',
|
||||
'fingerprint:3B:8B:D3:EE:base64',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.asScoped).toHaveBeenCalledWith({
|
||||
headers: { authorization: `Bearer access-token` },
|
||||
});
|
||||
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate');
|
||||
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toBe(user);
|
||||
expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' });
|
||||
expect(authenticationResult.authResponseHeaders).toBeUndefined();
|
||||
expect(authenticationResult.state).toEqual({
|
||||
accessToken: 'access-token',
|
||||
peerCertificateFingerprint256: '2A:7A:C2:DD',
|
||||
});
|
||||
});
|
||||
|
||||
it('gets an access token in exchange to a self-signed certificate and stores it in the state.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: {},
|
||||
socket: getMockSocket({
|
||||
authorized: true,
|
||||
peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'),
|
||||
}),
|
||||
});
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
|
||||
body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] },
|
||||
});
|
||||
|
||||
expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.asScoped).toHaveBeenCalledWith({
|
||||
headers: { authorization: `Bearer access-token` },
|
||||
});
|
||||
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate');
|
||||
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toBe(user);
|
||||
expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' });
|
||||
expect(authenticationResult.authResponseHeaders).toBeUndefined();
|
||||
expect(authenticationResult.state).toEqual({
|
||||
accessToken: 'access-token',
|
||||
peerCertificateFingerprint256: '2A:7A:C2:DD',
|
||||
});
|
||||
});
|
||||
|
||||
it('invalidates existing token and gets a new one if fingerprints do not match.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
socket: getMockSocket({
|
||||
authorized: true,
|
||||
peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']),
|
||||
}),
|
||||
});
|
||||
const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '3A:9A:C5:DD' };
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, state);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
|
||||
accessToken: state.accessToken,
|
||||
});
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
|
||||
body: {
|
||||
x509_certificate_chain: [
|
||||
'fingerprint:2A:7A:C2:DD:base64',
|
||||
'fingerprint:3B:8B:D3:EE:base64',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toBe(user);
|
||||
expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' });
|
||||
expect(authenticationResult.authResponseHeaders).toBeUndefined();
|
||||
expect(authenticationResult.state).toEqual({
|
||||
accessToken: 'access-token',
|
||||
peerCertificateFingerprint256: '2A:7A:C2:DD',
|
||||
});
|
||||
});
|
||||
|
||||
it('gets a new access token even if existing token is expired.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
socket: getMockSocket({
|
||||
authorized: true,
|
||||
peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']),
|
||||
}),
|
||||
});
|
||||
const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser
|
||||
// In response to call with an expired token.
|
||||
.mockRejectedValueOnce(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()))
|
||||
// In response to a call with a new token.
|
||||
.mockResolvedValueOnce(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, state);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
|
||||
body: {
|
||||
x509_certificate_chain: [
|
||||
'fingerprint:2A:7A:C2:DD:base64',
|
||||
'fingerprint:3B:8B:D3:EE:base64',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toBe(user);
|
||||
expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' });
|
||||
expect(authenticationResult.authResponseHeaders).toBeUndefined();
|
||||
expect(authenticationResult.state).toEqual({
|
||||
accessToken: 'access-token',
|
||||
peerCertificateFingerprint256: '2A:7A:C2:DD',
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with 401 if existing token is expired, but certificate is not present.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({ socket: getMockSocket() });
|
||||
const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(
|
||||
ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())
|
||||
);
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, state);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
|
||||
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(getErrorStatusCode(authenticationResult.error)).toBe(401);
|
||||
expect(authenticationResult.authResponseHeaders).toBeUndefined();
|
||||
});
|
||||
|
||||
it('fails if could not retrieve an access token in exchange to peer certificate chain.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
socket: getMockSocket({
|
||||
authorized: true,
|
||||
peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'),
|
||||
}),
|
||||
});
|
||||
|
||||
const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error());
|
||||
mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
|
||||
body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] },
|
||||
});
|
||||
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toBe(failureReason);
|
||||
expect(authenticationResult.authResponseHeaders).toBeUndefined();
|
||||
});
|
||||
|
||||
it('fails if could not retrieve user using the new access token.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: {},
|
||||
socket: getMockSocket({
|
||||
authorized: true,
|
||||
peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'),
|
||||
}),
|
||||
});
|
||||
|
||||
const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error());
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason);
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
|
||||
body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] },
|
||||
});
|
||||
|
||||
expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.client.asScoped).toHaveBeenCalledWith({
|
||||
headers: { authorization: `Bearer access-token` },
|
||||
});
|
||||
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
|
||||
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate');
|
||||
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toBe(failureReason);
|
||||
expect(authenticationResult.authResponseHeaders).toBeUndefined();
|
||||
});
|
||||
|
||||
it('succeeds if state contains a valid token.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
socket: getMockSocket({
|
||||
authorized: true,
|
||||
peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256),
|
||||
}),
|
||||
});
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, state);
|
||||
|
||||
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
|
||||
|
||||
expect(request.headers).not.toHaveProperty('authorization');
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.authHeaders).toEqual({
|
||||
authorization: `Bearer ${state.accessToken}`,
|
||||
});
|
||||
expect(authenticationResult.user).toBe(user);
|
||||
expect(authenticationResult.state).toBeUndefined();
|
||||
});
|
||||
|
||||
it('fails if token from the state is rejected because of unknown reason.', async () => {
|
||||
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
socket: getMockSocket({
|
||||
authorized: true,
|
||||
peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256),
|
||||
}),
|
||||
});
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(new errors.ServiceUnavailable());
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, state);
|
||||
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toHaveProperty('status', 503);
|
||||
expect(authenticationResult.authResponseHeaders).toBeUndefined();
|
||||
});
|
||||
|
||||
it('succeeds if `authorization` contains a valid token.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: { authorization: 'Bearer some-valid-token' },
|
||||
});
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
expect(request.headers.authorization).toBe('Bearer some-valid-token');
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.authHeaders).toBeUndefined();
|
||||
expect(authenticationResult.user).toBe(user);
|
||||
expect(authenticationResult.state).toBeUndefined();
|
||||
});
|
||||
|
||||
it('fails if token from `authorization` header is rejected.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: { authorization: 'Bearer some-invalid-token' },
|
||||
});
|
||||
|
||||
const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error());
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason);
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request);
|
||||
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toBe(failureReason);
|
||||
});
|
||||
|
||||
it('fails if token from `authorization` header is rejected even if state contains a valid one.', async () => {
|
||||
const user = mockAuthenticatedUser();
|
||||
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: { authorization: 'Bearer some-invalid-token' },
|
||||
socket: getMockSocket({
|
||||
authorized: true,
|
||||
peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256),
|
||||
}),
|
||||
});
|
||||
|
||||
const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error());
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.callAsCurrentUser
|
||||
// In response to call with a token from header.
|
||||
.mockRejectedValueOnce(failureReason)
|
||||
// In response to a call with a token from session (not expected to be called).
|
||||
.mockResolvedValueOnce(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(
|
||||
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
|
||||
);
|
||||
|
||||
const authenticationResult = await provider.authenticate(request, state);
|
||||
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toBe(failureReason);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`logout` method', () => {
|
||||
it('returns `notHandled` if state is not presented.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
|
||||
let deauthenticateResult = await provider.logout(request);
|
||||
expect(deauthenticateResult.notHandled()).toBe(true);
|
||||
|
||||
deauthenticateResult = await provider.logout(request, null);
|
||||
expect(deauthenticateResult.notHandled()).toBe(true);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fails if `tokens.invalidate` fails', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const state = { accessToken: 'foo', peerCertificateFingerprint256: '2A:7A:C2:DD' };
|
||||
|
||||
const failureReason = new Error('failed to delete token');
|
||||
mockOptions.tokens.invalidate.mockRejectedValue(failureReason);
|
||||
|
||||
const authenticationResult = await provider.logout(request, state);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' });
|
||||
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toBe(failureReason);
|
||||
});
|
||||
|
||||
it('redirects to `/logged_out` page if access token is invalidated successfully.', async () => {
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const state = { accessToken: 'foo', peerCertificateFingerprint256: '2A:7A:C2:DD' };
|
||||
|
||||
mockOptions.tokens.invalidate.mockResolvedValue(undefined);
|
||||
|
||||
const authenticationResult = await provider.logout(request, state);
|
||||
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' });
|
||||
|
||||
expect(authenticationResult.redirected()).toBe(true);
|
||||
expect(authenticationResult.redirectURL).toBe('/logged_out');
|
||||
});
|
||||
});
|
||||
});
|
277
x-pack/plugins/security/server/authentication/providers/pki.ts
Normal file
277
x-pack/plugins/security/server/authentication/providers/pki.ts
Normal file
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
* 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 Boom from 'boom';
|
||||
import { DetailedPeerCertificate } from 'tls';
|
||||
import { KibanaRequest } from '../../../../../../src/core/server';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
import { BaseAuthenticationProvider } from './base';
|
||||
import { Tokens } from '../tokens';
|
||||
|
||||
/**
|
||||
* The state supported by the provider.
|
||||
*/
|
||||
interface ProviderState {
|
||||
/**
|
||||
* Access token we got in exchange to peer certificate chain.
|
||||
*/
|
||||
accessToken: string;
|
||||
|
||||
/**
|
||||
* The SHA-256 digest of the DER encoded peer leaf certificate. It is a `:` separated hexadecimal string.
|
||||
*/
|
||||
peerCertificateFingerprint256: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses request's `Authorization` HTTP header if present and extracts authentication scheme.
|
||||
* @param request Request instance to extract authentication scheme for.
|
||||
*/
|
||||
function getRequestAuthenticationScheme(request: KibanaRequest) {
|
||||
const authorization = request.headers.authorization;
|
||||
if (!authorization || typeof authorization !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return authorization.split(/\s+/)[0].toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider that supports PKI request authentication.
|
||||
*/
|
||||
export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
|
||||
/**
|
||||
* Performs PKI request authentication.
|
||||
* @param request Request instance.
|
||||
* @param [state] Optional state object associated with the provider.
|
||||
*/
|
||||
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
|
||||
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
|
||||
|
||||
const authenticationScheme = getRequestAuthenticationScheme(request);
|
||||
if (authenticationScheme && authenticationScheme !== 'bearer') {
|
||||
this.logger.debug(`Unsupported authentication scheme: ${authenticationScheme}`);
|
||||
return AuthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
let authenticationResult = AuthenticationResult.notHandled();
|
||||
if (authenticationScheme) {
|
||||
// We should get rid of `Bearer` scheme support as soon as Reporting doesn't need it anymore.
|
||||
authenticationResult = await this.authenticateWithBearerScheme(request);
|
||||
}
|
||||
|
||||
if (state && authenticationResult.notHandled()) {
|
||||
authenticationResult = await this.authenticateViaState(request, state);
|
||||
|
||||
// If access token expired or doesn't match to the certificate fingerprint we should try to get
|
||||
// a new one in exchange to peer certificate chain.
|
||||
if (
|
||||
authenticationResult.notHandled() ||
|
||||
(authenticationResult.failed() &&
|
||||
Tokens.isAccessTokenExpiredError(authenticationResult.error))
|
||||
) {
|
||||
authenticationResult = await this.authenticateViaPeerCertificate(request);
|
||||
// If we have an active session that we couldn't use to authenticate user and at the same time
|
||||
// we couldn't use peer's certificate to establish a new one, then we should respond with 401
|
||||
// and force authenticator to clear the session.
|
||||
if (authenticationResult.notHandled()) {
|
||||
return AuthenticationResult.failed(Boom.unauthorized());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't authenticate by means of all methods above, let's try to check if we can authenticate
|
||||
// request using its peer certificate chain, otherwise just return authentication result we have.
|
||||
return authenticationResult.notHandled()
|
||||
? await this.authenticateViaPeerCertificate(request)
|
||||
: authenticationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates access token retrieved in exchange for peer certificate chain if it exists.
|
||||
* @param request Request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
public async logout(request: KibanaRequest, state?: ProviderState | null) {
|
||||
this.logger.debug(`Trying to log user out via ${request.url.path}.`);
|
||||
|
||||
if (!state) {
|
||||
this.logger.debug('There is no access token to invalidate.');
|
||||
return DeauthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.options.tokens.invalidate({ accessToken: state.accessToken });
|
||||
} catch (err) {
|
||||
this.logger.debug(`Failed invalidating access token: ${err.message}`);
|
||||
return DeauthenticationResult.failed(err);
|
||||
}
|
||||
|
||||
return DeauthenticationResult.redirectTo('/logged_out');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to authenticate request with `Bearer ***` Authorization header by passing it to the Elasticsearch backend.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
private async authenticateWithBearerScheme(request: KibanaRequest) {
|
||||
this.logger.debug('Trying to authenticate request using "Bearer" authentication scheme.');
|
||||
|
||||
try {
|
||||
const user = await this.getUser(request);
|
||||
|
||||
this.logger.debug('Request has been authenticated using "Bearer" authentication scheme.');
|
||||
return AuthenticationResult.succeeded(user);
|
||||
} catch (err) {
|
||||
this.logger.debug(
|
||||
`Failed to authenticate request using "Bearer" authentication scheme: ${err.message}`
|
||||
);
|
||||
return AuthenticationResult.failed(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to extract access token from state and adds it to the request before it's
|
||||
* forwarded to Elasticsearch backend.
|
||||
* @param request Request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
private async authenticateViaState(
|
||||
request: KibanaRequest,
|
||||
{ accessToken, peerCertificateFingerprint256 }: ProviderState
|
||||
) {
|
||||
this.logger.debug('Trying to authenticate via state.');
|
||||
|
||||
// If peer is authorized, but its certificate isn't available, that likely means the connection
|
||||
// with the peer is closed already. We shouldn't invalidate peer's access token in this case
|
||||
// since we cannot guarantee that there is a mismatch in access token and peer certificate.
|
||||
const peerCertificate = request.socket.getPeerCertificate(true);
|
||||
if (peerCertificate === null && request.socket.authorized) {
|
||||
this.logger.debug(
|
||||
'Cannot validate state access token with the peer certificate since it is not available.'
|
||||
);
|
||||
return AuthenticationResult.failed(new Error('Peer certificate is not available'));
|
||||
}
|
||||
|
||||
if (
|
||||
!request.socket.authorized ||
|
||||
peerCertificate === null ||
|
||||
(peerCertificate as any).fingerprint256 !== peerCertificateFingerprint256
|
||||
) {
|
||||
this.logger.debug(
|
||||
'Peer certificate is not present or its fingerprint does not match to the one associated with the access token. Invalidating access token...'
|
||||
);
|
||||
|
||||
try {
|
||||
await this.options.tokens.invalidate({ accessToken });
|
||||
} catch (err) {
|
||||
this.logger.debug(`Failed to invalidate access token: ${err.message}`);
|
||||
return AuthenticationResult.failed(err);
|
||||
}
|
||||
|
||||
// Return "Not Handled" result to allow provider to try to exchange new peer certificate chain
|
||||
// to the new access token down the line.
|
||||
return AuthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
try {
|
||||
const authHeaders = { authorization: `Bearer ${accessToken}` };
|
||||
const user = await this.getUser(request, authHeaders);
|
||||
|
||||
this.logger.debug('Request has been authenticated via state.');
|
||||
return AuthenticationResult.succeeded(user, { authHeaders });
|
||||
} catch (err) {
|
||||
this.logger.debug(`Failed to authenticate request via state: ${err.message}`);
|
||||
return AuthenticationResult.failed(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to exchange peer certificate chain to access/refresh token pair.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
private async authenticateViaPeerCertificate(request: KibanaRequest) {
|
||||
this.logger.debug('Trying to authenticate request via peer certificate chain.');
|
||||
|
||||
if (!request.socket.authorized) {
|
||||
this.logger.debug(
|
||||
`Authentication is not possible since peer certificate was not authorized: ${request.socket.authorizationError}.`
|
||||
);
|
||||
return AuthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
const peerCertificate = request.socket.getPeerCertificate(true);
|
||||
if (peerCertificate === null) {
|
||||
this.logger.debug('Authentication is not possible due to missing peer certificate chain.');
|
||||
return AuthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
// We should collect entire certificate chain as an ordered array of certificates encoded as base64 strings.
|
||||
const certificateChain = this.getCertificateChain(peerCertificate);
|
||||
let accessToken: string;
|
||||
try {
|
||||
accessToken = (await this.options.client.callAsInternalUser('shield.delegatePKI', {
|
||||
body: { x509_certificate_chain: certificateChain },
|
||||
})).access_token;
|
||||
} catch (err) {
|
||||
this.logger.debug(
|
||||
`Failed to exchange peer certificate chain to an access token: ${err.message}`
|
||||
);
|
||||
return AuthenticationResult.failed(err);
|
||||
}
|
||||
|
||||
this.logger.debug('Successfully retrieved access token in exchange to peer certificate chain.');
|
||||
|
||||
try {
|
||||
// Then attempt to query for the user details using the new token
|
||||
const authHeaders = { authorization: `Bearer ${accessToken}` };
|
||||
const user = await this.getUser(request, authHeaders);
|
||||
|
||||
this.logger.debug('User has been authenticated with new access token');
|
||||
return AuthenticationResult.succeeded(user, {
|
||||
authHeaders,
|
||||
state: {
|
||||
accessToken,
|
||||
// NodeJS typings don't include `fingerprint256` yet.
|
||||
peerCertificateFingerprint256: (peerCertificate as any).fingerprint256,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.debug(`Failed to authenticate request via access token: ${err.message}`);
|
||||
return AuthenticationResult.failed(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts from the leaf peer certificate and iterates up to the top-most available certificate
|
||||
* authority using `issuerCertificate` certificate property. THe iteration is stopped only when
|
||||
* we detect circular reference (root/self-signed certificate) or when `issuerCertificate` isn't
|
||||
* available (null or empty object).
|
||||
* @param peerCertificate Peer leaf certificate instance.
|
||||
*/
|
||||
private getCertificateChain(peerCertificate: DetailedPeerCertificate | null) {
|
||||
const certificateChain = [];
|
||||
let certificate: DetailedPeerCertificate | null = peerCertificate;
|
||||
while (certificate !== null && Object.keys(certificate).length > 0) {
|
||||
certificateChain.push(certificate.raw.toString('base64'));
|
||||
|
||||
// For self-signed certificates, `issuerCertificate` may be a circular reference.
|
||||
if (certificate === certificate.issuerCertificate) {
|
||||
this.logger.debug('Self-signed certificate is detected in certificate chain');
|
||||
certificate = null;
|
||||
} else {
|
||||
certificate = certificate.issuerCertificate;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Peer certificate chain consists of ${certificateChain.length} certificates.`
|
||||
);
|
||||
|
||||
return certificateChain;
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ require('@kbn/test').runTestsCli([
|
|||
require.resolve('../test/token_api_integration/config.js'),
|
||||
require.resolve('../test/oidc_api_integration/config.ts'),
|
||||
require.resolve('../test/oidc_api_integration/implicit_flow.config.ts'),
|
||||
// require.resolve('../test/pki_api_integration/config.ts'),
|
||||
require.resolve('../test/spaces_api_integration/spaces_only/config'),
|
||||
require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial'),
|
||||
require.resolve('../test/spaces_api_integration/security_and_spaces/config_basic'),
|
||||
|
|
14
x-pack/test/pki_api_integration/apis/index.ts
Normal file
14
x-pack/test/pki_api_integration/apis/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export default function({ loadTestFile }: FtrProviderContext) {
|
||||
describe('apis PKI', function() {
|
||||
this.tags('ciGroup6');
|
||||
loadTestFile(require.resolve('./security'));
|
||||
});
|
||||
}
|
13
x-pack/test/pki_api_integration/apis/security/index.ts
Normal file
13
x-pack/test/pki_api_integration/apis/security/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function({ loadTestFile }: FtrProviderContext) {
|
||||
describe('security', () => {
|
||||
loadTestFile(require.resolve('./pki_auth'));
|
||||
});
|
||||
}
|
370
x-pack/test/pki_api_integration/apis/security/pki_auth.ts
Normal file
370
x-pack/test/pki_api_integration/apis/security/pki_auth.ts
Normal file
|
@ -0,0 +1,370 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import request, { Cookie } from 'request';
|
||||
import { delay } from 'bluebird';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
// @ts-ignore
|
||||
import { CA_CERT_PATH } from '@kbn/dev-utils';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
const CA_CERT = readFileSync(CA_CERT_PATH);
|
||||
const FIRST_CLIENT_CERT = readFileSync(resolve(__dirname, '../../fixtures/first_client.p12'));
|
||||
const SECOND_CLIENT_CERT = readFileSync(resolve(__dirname, '../../fixtures/second_client.p12'));
|
||||
const UNTRUSTED_CLIENT_CERT = readFileSync(
|
||||
resolve(__dirname, '../../fixtures/untrusted_client.p12')
|
||||
);
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
const config = getService('config');
|
||||
|
||||
function checkCookieIsSet(cookie: Cookie) {
|
||||
expect(cookie.value).to.not.be.empty();
|
||||
|
||||
expect(cookie.key).to.be('sid');
|
||||
expect(cookie.path).to.be('/');
|
||||
expect(cookie.httpOnly).to.be(true);
|
||||
expect(cookie.maxAge).to.be(null);
|
||||
}
|
||||
|
||||
function checkCookieIsCleared(cookie: Cookie) {
|
||||
expect(cookie.value).to.be.empty();
|
||||
|
||||
expect(cookie.key).to.be('sid');
|
||||
expect(cookie.path).to.be('/');
|
||||
expect(cookie.httpOnly).to.be(true);
|
||||
expect(cookie.maxAge).to.be(0);
|
||||
}
|
||||
|
||||
describe('PKI authentication', () => {
|
||||
before(async () => {
|
||||
await getService('esSupertest')
|
||||
.post('/_security/role_mapping/first_client_pki')
|
||||
.ca(CA_CERT)
|
||||
.send({
|
||||
roles: ['kibana_user'],
|
||||
enabled: true,
|
||||
rules: { field: { dn: 'CN=first_client' } },
|
||||
})
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should reject API requests that use untrusted certificate', async () => {
|
||||
await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(UNTRUSTED_CLIENT_CERT)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('does not prevent basic login', async () => {
|
||||
const [username, password] = config.get('servers.elasticsearch.auth').split(':');
|
||||
const response = await supertest
|
||||
.post('/api/security/v1/login')
|
||||
.ca(CA_CERT)
|
||||
.pfx(UNTRUSTED_CLIENT_CERT)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({ username, password })
|
||||
.expect(204);
|
||||
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).to.have.length(1);
|
||||
|
||||
const cookie = request.cookie(cookies[0])!;
|
||||
checkCookieIsSet(cookie);
|
||||
|
||||
const { body: user } = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(UNTRUSTED_CLIENT_CERT)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Cookie', cookie.cookieString())
|
||||
.expect(200);
|
||||
|
||||
expect(user.username).to.eql(username);
|
||||
expect(user.authentication_realm).to.eql({ name: 'reserved', type: 'reserved' });
|
||||
});
|
||||
|
||||
it('should properly set cookie and authenticate user', async () => {
|
||||
const response = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.expect(200);
|
||||
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).to.have.length(1);
|
||||
|
||||
const sessionCookie = request.cookie(cookies[0])!;
|
||||
checkCookieIsSet(sessionCookie);
|
||||
|
||||
expect(response.body).to.eql({
|
||||
username: 'first_client',
|
||||
roles: ['kibana_user'],
|
||||
full_name: null,
|
||||
email: null,
|
||||
enabled: true,
|
||||
metadata: {
|
||||
pki_delegated_by_realm: 'reserved',
|
||||
pki_delegated_by_user: 'elastic',
|
||||
pki_dn: 'CN=first_client',
|
||||
},
|
||||
authentication_realm: { name: 'pki1', type: 'pki' },
|
||||
lookup_realm: { name: 'pki1', type: 'pki' },
|
||||
});
|
||||
|
||||
// Cookie should be accepted.
|
||||
await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should update session if new certificate is provided', async () => {
|
||||
let response = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.expect(200);
|
||||
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).to.have.length(1);
|
||||
|
||||
const sessionCookie = request.cookie(cookies[0])!;
|
||||
checkCookieIsSet(sessionCookie);
|
||||
|
||||
response = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(SECOND_CLIENT_CERT)
|
||||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(200, {
|
||||
username: 'second_client',
|
||||
roles: [],
|
||||
full_name: null,
|
||||
email: null,
|
||||
enabled: true,
|
||||
metadata: {
|
||||
pki_delegated_by_realm: 'reserved',
|
||||
pki_delegated_by_user: 'elastic',
|
||||
pki_dn: 'CN=second_client',
|
||||
},
|
||||
authentication_realm: { name: 'pki1', type: 'pki' },
|
||||
lookup_realm: { name: 'pki1', type: 'pki' },
|
||||
});
|
||||
|
||||
checkCookieIsSet(request.cookie(response.headers['set-cookie'][0])!);
|
||||
});
|
||||
|
||||
it('should reject valid cookie if used with untrusted certificate', async () => {
|
||||
const response = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.expect(200);
|
||||
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).to.have.length(1);
|
||||
|
||||
const sessionCookie = request.cookie(cookies[0])!;
|
||||
checkCookieIsSet(sessionCookie);
|
||||
|
||||
await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(UNTRUSTED_CLIENT_CERT)
|
||||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
describe('API access with active session', () => {
|
||||
let sessionCookie: Cookie;
|
||||
|
||||
beforeEach(async () => {
|
||||
const response = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.expect(200);
|
||||
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).to.have.length(1);
|
||||
|
||||
sessionCookie = request.cookie(cookies[0])!;
|
||||
checkCookieIsSet(sessionCookie);
|
||||
});
|
||||
|
||||
it('should extend cookie on every successful non-system API call', async () => {
|
||||
const apiResponseOne = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(200);
|
||||
|
||||
expect(apiResponseOne.headers['set-cookie']).to.not.be(undefined);
|
||||
const sessionCookieOne = request.cookie(apiResponseOne.headers['set-cookie'][0])!;
|
||||
|
||||
checkCookieIsSet(sessionCookieOne);
|
||||
expect(sessionCookieOne.value).to.not.equal(sessionCookie.value);
|
||||
|
||||
const apiResponseTwo = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(200);
|
||||
|
||||
expect(apiResponseTwo.headers['set-cookie']).to.not.be(undefined);
|
||||
const sessionCookieTwo = request.cookie(apiResponseTwo.headers['set-cookie'][0])!;
|
||||
|
||||
checkCookieIsSet(sessionCookieTwo);
|
||||
expect(sessionCookieTwo.value).to.not.equal(sessionCookieOne.value);
|
||||
});
|
||||
|
||||
it('should not extend cookie for system API calls', async () => {
|
||||
const systemAPIResponse = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('kbn-system-api', 'true')
|
||||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(200);
|
||||
|
||||
expect(systemAPIResponse.headers['set-cookie']).to.be(undefined);
|
||||
});
|
||||
|
||||
it('should fail and preserve session cookie if unsupported authentication schema is used', async () => {
|
||||
const apiResponse = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Authorization', 'Basic a3JiNTprcmI1')
|
||||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(401);
|
||||
|
||||
expect(apiResponse.headers['set-cookie']).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logging out', () => {
|
||||
it('should redirect to `logged_out` page after successful logout', async () => {
|
||||
// First authenticate user to retrieve session cookie.
|
||||
const response = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.expect(200);
|
||||
|
||||
let cookies = response.headers['set-cookie'];
|
||||
expect(cookies).to.have.length(1);
|
||||
|
||||
const sessionCookie = request.cookie(cookies[0])!;
|
||||
checkCookieIsSet(sessionCookie);
|
||||
|
||||
// And then log user out.
|
||||
const logoutResponse = await supertest
|
||||
.get('/api/security/v1/logout')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(302);
|
||||
|
||||
cookies = logoutResponse.headers['set-cookie'];
|
||||
expect(cookies).to.have.length(1);
|
||||
checkCookieIsCleared(request.cookie(cookies[0])!);
|
||||
|
||||
expect(logoutResponse.headers.location).to.be('/logged_out');
|
||||
});
|
||||
|
||||
it('should redirect to home page if session cookie is not provided', async () => {
|
||||
const logoutResponse = await supertest
|
||||
.get('/api/security/v1/logout')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.expect(302);
|
||||
|
||||
expect(logoutResponse.headers['set-cookie']).to.be(undefined);
|
||||
expect(logoutResponse.headers.location).to.be('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('API access with expired access token.', () => {
|
||||
let sessionCookie: Cookie;
|
||||
|
||||
beforeEach(async () => {
|
||||
const response = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.expect(200);
|
||||
|
||||
const cookies = response.headers['set-cookie'];
|
||||
expect(cookies).to.have.length(1);
|
||||
|
||||
sessionCookie = request.cookie(cookies[0])!;
|
||||
checkCookieIsSet(sessionCookie);
|
||||
});
|
||||
|
||||
it('AJAX call should re-acquire token and update existing cookie', async function() {
|
||||
this.timeout(40000);
|
||||
|
||||
// Access token expiration is set to 15s for API integration tests.
|
||||
// Let's wait for 20s to make sure token expires.
|
||||
await delay(20000);
|
||||
|
||||
// This api call should succeed and automatically refresh token. Returned cookie will contain
|
||||
// the new access token.
|
||||
const apiResponse = await supertest
|
||||
.get('/api/security/v1/me')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(200);
|
||||
|
||||
const cookies = apiResponse.headers['set-cookie'];
|
||||
expect(cookies).to.have.length(1);
|
||||
|
||||
const refreshedCookie = request.cookie(cookies[0])!;
|
||||
checkCookieIsSet(refreshedCookie);
|
||||
});
|
||||
|
||||
it('non-AJAX call should re-acquire token and update existing cookie', async function() {
|
||||
this.timeout(40000);
|
||||
|
||||
// Access token expiration is set to 15s for API integration tests.
|
||||
// Let's wait for 20s to make sure token expires.
|
||||
await delay(20000);
|
||||
|
||||
// This request should succeed and automatically refresh token. Returned cookie will contain
|
||||
// the new access and refresh token pair.
|
||||
const nonAjaxResponse = await supertest
|
||||
.get('/app/kibana')
|
||||
.ca(CA_CERT)
|
||||
.pfx(FIRST_CLIENT_CERT)
|
||||
.set('Cookie', sessionCookie.cookieString())
|
||||
.expect(200);
|
||||
|
||||
const cookies = nonAjaxResponse.headers['set-cookie'];
|
||||
expect(cookies).to.have.length(1);
|
||||
|
||||
const refreshedCookie = request.cookie(cookies[0])!;
|
||||
checkCookieIsSet(refreshedCookie);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
70
x-pack/test/pki_api_integration/config.ts
Normal file
70
x-pack/test/pki_api_integration/config.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { resolve } from 'path';
|
||||
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
|
||||
// @ts-ignore
|
||||
import { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from '@kbn/dev-utils';
|
||||
import { services } from './services';
|
||||
|
||||
export default async function({ readConfigFile }: FtrConfigProviderContext) {
|
||||
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
|
||||
|
||||
const servers = {
|
||||
...xPackAPITestsConfig.get('servers'),
|
||||
elasticsearch: {
|
||||
...xPackAPITestsConfig.get('servers.elasticsearch'),
|
||||
protocol: 'https',
|
||||
},
|
||||
kibana: {
|
||||
...xPackAPITestsConfig.get('servers.kibana'),
|
||||
protocol: 'https',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
testFiles: [require.resolve('./apis')],
|
||||
servers,
|
||||
services,
|
||||
junit: {
|
||||
reportName: 'X-Pack PKI API Integration Tests',
|
||||
},
|
||||
|
||||
esTestCluster: {
|
||||
...xPackAPITestsConfig.get('esTestCluster'),
|
||||
ssl: true,
|
||||
serverArgs: [
|
||||
...xPackAPITestsConfig.get('esTestCluster.serverArgs'),
|
||||
'xpack.security.authc.token.enabled=true',
|
||||
'xpack.security.authc.token.timeout=15s',
|
||||
'xpack.security.http.ssl.client_authentication=optional',
|
||||
'xpack.security.http.ssl.verification_mode=certificate',
|
||||
'xpack.security.authc.realms.native.native1.order=0',
|
||||
'xpack.security.authc.realms.pki.pki1.order=1',
|
||||
'xpack.security.authc.realms.pki.pki1.delegation.enabled=true',
|
||||
`xpack.security.authc.realms.pki.pki1.certificate_authorities=${CA_CERT_PATH}`,
|
||||
],
|
||||
},
|
||||
|
||||
kbnTestServer: {
|
||||
...xPackAPITestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--server.ssl.enabled=true',
|
||||
`--server.ssl.key=${ES_KEY_PATH}`,
|
||||
`--server.ssl.certificate=${ES_CERT_PATH}`,
|
||||
`--server.ssl.certificateAuthorities=${JSON.stringify([
|
||||
CA_CERT_PATH,
|
||||
resolve(__dirname, './fixtures/kibana_ca.crt'),
|
||||
])}`,
|
||||
`--server.ssl.clientAuthentication=required`,
|
||||
`--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,
|
||||
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
|
||||
`--xpack.security.authc.providers=${JSON.stringify(['pki', 'basic'])}`,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
7
x-pack/test/pki_api_integration/fixtures/README.md
Normal file
7
x-pack/test/pki_api_integration/fixtures/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# PKI Fixtures
|
||||
|
||||
* `es_ca.key` - the CA key used to sign certificates from @kbn/dev-utils that are used and trusted by test Elasticsearch server.
|
||||
* `first_client.p12` and `second_client.p12` - the client certificate bundles signed by `es_ca.key` and hence trusted by
|
||||
both test Kibana and Elasticsearch servers.
|
||||
* `untrusted_client.p12` - the client certificate bundle trusted by test Kibana server, but not test Elasticsearch test server.
|
||||
* `kibana_ca.crt` and `kibana_ca.key` - the CA certificate and key trusted by test Kibana server only.
|
27
x-pack/test/pki_api_integration/fixtures/es_ca.key
Normal file
27
x-pack/test/pki_api_integration/fixtures/es_ca.key
Normal file
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAjSJiqfwPZfvgHO1OZbxzgPn2EW/KewIHXygTAdL926Pm6R45
|
||||
G5H972B46NcSUoOZbOhDyvg6OKMJAICiXa85yOf3nyTo4APspR+K4AH60SEJohRF
|
||||
mZwL/OryfiKvN5n5DxC2+Hb1wouwBUJM6DP62C24ve8YWuWwNkhJqWKe1YQUzPc1
|
||||
svqvU5uaHTzvLtp++RqSDNkcIqWl5S9Ip5PtOv6MHkCaIr2g4KQzplFwhT5qVd1Q
|
||||
nYVBsQ0D8htLqUJBfjW0KHouEZpbjxJlc+EuyExS1o1+y3mVT+t2yZHAoIquh5ve
|
||||
5A7a/RGJTyoR5u1DFs4Tcx2378kjA86gCQtClwIDAQABAoIBAFTOGKMzxrztQJmh
|
||||
Lr6LIoyZpnaLygtoCK3xEprCAbB9KD9j3cTnUMMKIR0oPuY+FW8Pkczgo3ts2/fl
|
||||
U6sfo4VJfc2vDA+vy/7cmUJJbkFDrNorfDb1QW7UbqnEhazPZIzc6lUahkpETZyb
|
||||
XkMZGN3Ve3EFvojAA8ZaYYjarb52HRddLPZJ7c8ZiHfJ1jHNIvx6dIQ6CJVuovBJ
|
||||
OGbbSAK8MjUtOI2XzWNHgUqGHcjVDFysuAac3ckK14TaN4KVNRl+usAMkZwqSM5u
|
||||
j/ATFL9hx7nkzh3KWPsuOLMoLX7JN81z0YtT52wTxJoSiZKk/u91JHZ3NcrsOSPS
|
||||
oLvVkyECgYEA16qtXvtmboAbqeuXf0nF+7QD0b+MdaRFIacqTG0LpEgY9Tjgs9Pn
|
||||
6z44tHABWPVkRLNQZiky99MAq4Ci354Bk9dmylCw9ADH78VGmKWklbQEr1rw4dqm
|
||||
DHTj9NQ79SyTdiasQjnnxCilWkrO6ZUqD8og4DT5MhzfxO/ZND8arGsCgYEAp4df
|
||||
oI5lwlc1n9X/G9RQAKwNM5un8RmReleUVemjkcvWwvZVEjV0Gcc1WtjB+77Y5B9B
|
||||
CM3laURDGrAgX5VS/I2jb0xqBNUr8XccSkDQAP9UuVPZgxpS+8d0R3fxVzniHWwR
|
||||
WC2dW/Is40i/6+7AkFXhkiFiqxkvSg4pWHPazYUCgYB/gP7C+urSRZcVXJ3SuXD9
|
||||
oK3pYc/O9XGRtd0CFi4d0CpBQIFIj+27XKv1sYp6Z4oCO+k6nPzvG6Z3vrOMdUQF
|
||||
fgHddttHRvbtwLo+ISAvCaEDc0aaoMQu9SSYaKmSB+qenbqV5NorVMR9n2C5JGEb
|
||||
uKq7I1Z41C1Pp2XIx84jRQKBgQCjKvfZsjesZDJnfg9dtJlDPlARXt7gte16gkiI
|
||||
sOnOfAGtnCzZclSlMuBlnk65enVXIpW+FIQH1iOhn7+4OQE92FpBceSk1ldZdJCK
|
||||
RbwR7J5Bb0igJ4iBkA9R+KGIOmlgDLyL7MmiHyrXKCk9iynkqrDsGjY2vW3QrCBa
|
||||
9WQ73QKBgQDAYZzplO4TPoPK9AnxoW/HpSwGEO7Fb8fLyPg94CvHn4QBCFJUKuTn
|
||||
hBp/TJgF6CjQWQMr2FKVFF33Ow7+Qa96YGvmYlEjR/71D4Rlprj5JJpuO154DI3I
|
||||
YIMNTjvwEQEI+YamMarKsz0Kq+I1EYSAf6bQ4H2PgxDxwTXaLkl0RA==
|
||||
-----END RSA PRIVATE KEY-----
|
BIN
x-pack/test/pki_api_integration/fixtures/first_client.p12
Normal file
BIN
x-pack/test/pki_api_integration/fixtures/first_client.p12
Normal file
Binary file not shown.
19
x-pack/test/pki_api_integration/fixtures/kibana_ca.crt
Normal file
19
x-pack/test/pki_api_integration/fixtures/kibana_ca.crt
Normal file
|
@ -0,0 +1,19 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDCjCCAfKgAwIBAgIVAK8/CDsQxdAItvVPu2P72xXx4pbAMA0GCSqGSIb3DQEB
|
||||
CwUAMBQxEjAQBgNVBAMTCUtpYmFuYSBDQTAeFw0xOTA4MTQxNTAwNDFaFw0yMjA4
|
||||
MTMxNTAwNDFaMBQxEjAQBgNVBAMTCUtpYmFuYSBDQTCCASIwDQYJKoZIhvcNAQEB
|
||||
BQADggEPADCCAQoCggEBANDBPAHZvBBtOZ/9aBHVmBFA3QS35wemnT2VwFE6LSUw
|
||||
35Tj3/Vj/1NQAqAqKOUTCE0zQAyDBOGWAa1MadhYC2Fvxt/VUoOJWczeMuO3ktua
|
||||
ybk3xzJJcOSoPjbPBUfQuRQ7GnBJsjyHKgPXIsP6wshQosYZnHPJcZSF1+6N9aGJ
|
||||
psV/ukdLD8oJFq3pv7D9KY/gbAFeVkwWwdx9dqtfT0STGXOOZnLAz8ZmWH2WIt+f
|
||||
t7+9EIv1pIUM6KOqANmhxxyitvka7XdN/ZEnwV/+Is9y/6N0NGaC9BWWoCNAgvuX
|
||||
Ep0R+5qvNtCkL8okLaCc0a/B843e3k7eWuI8ES3Dhg0CAwEAAaNTMFEwHQYDVR0O
|
||||
BBYEFBEp58Oz7rIAbT5O/yOGnSQcasG7MB8GA1UdIwQYMBaAFBEp58Oz7rIAbT5O
|
||||
/yOGnSQcasG7MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKZ/
|
||||
ZblM7pEOP77DLePM3NpjJQu73a7vjou2n0ifEq0HYsSMuKverZhhrc4L2PjRM34A
|
||||
NVtcSsjnc2OkhtG6baV8q/GyDtvUXwnfCnI2MxNiVtmX7fWzHZVwkd4GCXnvOd3S
|
||||
IBxzh4OYLV2rTFjo7oUWdDV+nFGVzQhhdlQ/fZ8by6g0qZvKKfe70Z3prmkRRRxz
|
||||
QslJYQwB+cK3rdyAVJDYGbMGcJjM50PR3iM/PqQFAwOcyW9th1CpiHOOmbcQRmCS
|
||||
W7h8A2TDzqvFWOz0QRoldt93vCXkP6PF3UXo2wpSPt8tzd6e0z0+HIyhYGUPstiE
|
||||
zO36/AJiPQicgQK60gI=
|
||||
-----END CERTIFICATE-----
|
27
x-pack/test/pki_api_integration/fixtures/kibana_ca.key
Normal file
27
x-pack/test/pki_api_integration/fixtures/kibana_ca.key
Normal file
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA0ME8Adm8EG05n/1oEdWYEUDdBLfnB6adPZXAUTotJTDflOPf
|
||||
9WP/U1ACoCoo5RMITTNADIME4ZYBrUxp2FgLYW/G39VSg4lZzN4y47eS25rJuTfH
|
||||
Mklw5Kg+Ns8FR9C5FDsacEmyPIcqA9ciw/rCyFCixhmcc8lxlIXX7o31oYmmxX+6
|
||||
R0sPygkWrem/sP0pj+BsAV5WTBbB3H12q19PRJMZc45mcsDPxmZYfZYi35+3v70Q
|
||||
i/WkhQzoo6oA2aHHHKK2+Rrtd039kSfBX/4iz3L/o3Q0ZoL0FZagI0CC+5cSnRH7
|
||||
mq820KQvyiQtoJzRr8Hzjd7eTt5a4jwRLcOGDQIDAQABAoIBACZm5bsRat86uJcN
|
||||
7s8ZE9hYrk/n5MArjlF98tr+cL+etgKVyOVDd/zDgzgjiVJapfRNsUKb95HoHnba
|
||||
z73UtINAJL2YaI15/uMJHSN26bUsTF+eOy6tA++MY6WBf98uLl3iYYK2i+tGkhwS
|
||||
v3p97scazlbS70z9ib9gv9BKnR0R+DTDBACwNtfurOGQh9PDU/e3orsrVBR/kj7o
|
||||
nfjlXZzsuuVdGRHmO2yGoALCx1N0dMpO/ALWDi+phP5Jz6SBo0AGAKfC2tXZJrVz
|
||||
qwHfCPnklIphHHmFkArmrAYZDOHBNtLRFQL8SmfcZOz3HO9er87ct/zAHN5im91s
|
||||
vVZnYQECgYEA90u/q4Ux9A2iaJ4qfDqjM8i+DLezj0ogLLe9OZuBgKfy41bt7ilX
|
||||
4iQ6cmmzq/9x0dM2ydRXTVJ0Ek/0EDgfVxxcWTRSHHrwy+WOnaB0hWkMzwgDak58
|
||||
fhRi5RAhXhCJlUyHsU7FBTYcwQE6I/C971X0AuCH06eEeaHG8HOERzkCgYEA2Bo2
|
||||
cYvRmfL7uh4STWTJkq08ppNDgo1aJ8brc+YtK3v11OZEbnSSmq0rdnDqkvFBmSrJ
|
||||
wHprhs+RGBKBJzI3BMZ9uxOzhufk/xPZrpZie77JFvpzMSbC0WMTpb06QsGk1bu7
|
||||
jqtbNx8OYYF7PVxiuUsZY3sOIZ5b3t9yWeiFwXUCgYB8tcyRGPiaFQ4kKC9Qutl2
|
||||
0fNVwoZg6obTRk288XkbgpbwovQWOO9C8fYvoLKlOIsTv6pPmi/0pHI4ke2JCGR1
|
||||
r626prIJ/s3UZY3IXBSm+tUkyuu9/pq1kl5VGg9ZuolHq3J6rjiZajKR+qZxXYTL
|
||||
X9NQaB7XVBFwrW7/76FzsQKBgCOOVIzkI22AFDjwP7SqM5xFkqgZrM7rMP1AdncQ
|
||||
VThFYhJQfMvrtD9s5KzNMVtSBKgN6ToZKl35AveB++wWEAViH0fLmwtEVmI9wuA9
|
||||
8CBKKM32EUPyC7Xl5lKrys03DUb5Z4e23AA6xOP4KO3UqI2yNJAwrAeOBbGq9Cak
|
||||
4nUNAoGBAKBhG767ePf/5fMrKJnsdxK2+fpP1UglHHTKOxwtLU9xtkUw96g7yzxj
|
||||
5ma66yv1QuGIvAmbLVLCI6MmFvXYQmomoRt2KnYEgxjg8jjXXDdoOsNL87t7l0HA
|
||||
CviL/UR7ZZV28rp5TmexRworu7hC4qer9NZxqV0a7bOLq0+uVda9
|
||||
-----END RSA PRIVATE KEY-----
|
BIN
x-pack/test/pki_api_integration/fixtures/second_client.p12
Normal file
BIN
x-pack/test/pki_api_integration/fixtures/second_client.p12
Normal file
Binary file not shown.
BIN
x-pack/test/pki_api_integration/fixtures/untrusted_client.p12
Normal file
BIN
x-pack/test/pki_api_integration/fixtures/untrusted_client.p12
Normal file
Binary file not shown.
11
x-pack/test/pki_api_integration/ftr_provider_context.d.ts
vendored
Normal file
11
x-pack/test/pki_api_integration/ftr_provider_context.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr';
|
||||
|
||||
import { services } from './services';
|
||||
|
||||
export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>;
|
12
x-pack/test/pki_api_integration/services.ts
Normal file
12
x-pack/test/pki_api_integration/services.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { services as apiIntegrationServices } from '../api_integration/services';
|
||||
|
||||
export const services = {
|
||||
esSupertest: apiIntegrationServices.esSupertest,
|
||||
supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth,
|
||||
};
|
Loading…
Reference in a new issue