mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Typesciptify Authenticator and move its tests to Jest. (#35750)
This commit is contained in:
parent
9d2be1b730
commit
6871157963
27 changed files with 709 additions and 700 deletions
21
x-pack/plugins/security/index.d.ts
vendored
Normal file
21
x-pack/plugins/security/index.d.ts
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { Legacy } from 'kibana';
|
||||
import { AuthenticatedUser } from './common/model';
|
||||
import { AuthenticationResult, DeauthenticationResult } from './server/lib/authentication';
|
||||
import { AuthorizationService } from './server/lib/authorization/service';
|
||||
|
||||
/**
|
||||
* Public interface of the security plugin.
|
||||
*/
|
||||
export interface SecurityPlugin {
|
||||
authorization: Readonly<AuthorizationService>;
|
||||
authenticate: (request: Legacy.Request) => Promise<AuthenticationResult>;
|
||||
deauthenticate: (request: Legacy.Request) => Promise<DeauthenticationResult>;
|
||||
getUser: (request: Legacy.Request) => Promise<AuthenticatedUser>;
|
||||
isAuthenticated: (request: Legacy.Request) => Promise<boolean>;
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
* 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 sinon from 'sinon';
|
||||
|
||||
import { AuthScopeService } from '../auth_scope_service';
|
||||
|
||||
async function assertReject(fn, check) {
|
||||
try {
|
||||
await fn();
|
||||
throw new Error('expected function to reject the promise');
|
||||
} catch (error) {
|
||||
expect(() => { throw error; }).to.throwError(check);
|
||||
}
|
||||
}
|
||||
|
||||
describe('getCredentialsScope()', function () {
|
||||
describe('basics', () => {
|
||||
it('does not take any arguments', () => {
|
||||
const authScope = new AuthScopeService();
|
||||
expect(authScope).to.be.a(AuthScopeService);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerGetter()', () => {
|
||||
it('throws when not passed a function', () => {
|
||||
const authScope = new AuthScopeService();
|
||||
expect(() => authScope.registerGetter()).to.throwError('function');
|
||||
expect(() => authScope.registerGetter(null)).to.throwError('function');
|
||||
expect(() => authScope.registerGetter(0)).to.throwError('function');
|
||||
expect(() => authScope.registerGetter('')).to.throwError('function');
|
||||
expect(() => authScope.registerGetter([])).to.throwError('function');
|
||||
expect(() => authScope.registerGetter({})).to.throwError('function');
|
||||
});
|
||||
|
||||
it('only calls the passed function when #getForRequestAndUser() is called', async () => {
|
||||
const authScope = new AuthScopeService();
|
||||
const stub = sinon.stub();
|
||||
authScope.registerGetter(stub);
|
||||
sinon.assert.notCalled(stub);
|
||||
const request = {};
|
||||
const user = {};
|
||||
await authScope.getForRequestAndUser(request, user);
|
||||
sinon.assert.calledOnce(stub);
|
||||
expect(stub.firstCall.args[0]).to.be(request);
|
||||
expect(stub.firstCall.args[1]).to.be(user);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getForRequestAndUser()', () => {
|
||||
it('throws when request and user are not objects', async () => {
|
||||
const authScope = new AuthScopeService();
|
||||
await assertReject(() => authScope.getForRequestAndUser(null, {}), 'request object');
|
||||
await assertReject(() => authScope.getForRequestAndUser(1, {}), 'request object');
|
||||
await assertReject(() => authScope.getForRequestAndUser('abc', {}), 'request object');
|
||||
|
||||
await assertReject(() => authScope.getForRequestAndUser({}, null), 'user object');
|
||||
await assertReject(() => authScope.getForRequestAndUser({}, 1), 'user object');
|
||||
await assertReject(() => authScope.getForRequestAndUser({}, 'abc'), 'user object');
|
||||
});
|
||||
|
||||
it('returns a promise for an empty array by default', async () => {
|
||||
const authScope = new AuthScopeService();
|
||||
const scope = await authScope.getForRequestAndUser({}, {});
|
||||
expect(scope).to.eql([]);
|
||||
});
|
||||
|
||||
it('calls each registered getter once each call', async () => {
|
||||
const authScope = new AuthScopeService();
|
||||
const getter1 = sinon.stub();
|
||||
const getter2 = sinon.stub();
|
||||
const getter3 = sinon.stub();
|
||||
|
||||
authScope.registerGetter(getter1);
|
||||
authScope.registerGetter(getter2);
|
||||
authScope.registerGetter(getter3);
|
||||
|
||||
await authScope.getForRequestAndUser({}, {});
|
||||
sinon.assert.calledOnce(getter1);
|
||||
sinon.assert.calledOnce(getter2);
|
||||
sinon.assert.calledOnce(getter3);
|
||||
|
||||
await authScope.getForRequestAndUser({}, {});
|
||||
sinon.assert.calledTwice(getter1);
|
||||
sinon.assert.calledTwice(getter2);
|
||||
sinon.assert.calledTwice(getter3);
|
||||
|
||||
await authScope.getForRequestAndUser({}, {});
|
||||
sinon.assert.calledThrice(getter1);
|
||||
sinon.assert.calledThrice(getter2);
|
||||
sinon.assert.calledThrice(getter3);
|
||||
});
|
||||
|
||||
it('casts the return value of the getters to an produce a flat, unique array', async () => {
|
||||
const authScope = new AuthScopeService();
|
||||
authScope.registerGetter(() => undefined);
|
||||
authScope.registerGetter(() => null);
|
||||
authScope.registerGetter(() => ['foo', undefined, 'bar']);
|
||||
authScope.registerGetter(() => ['foo', 'foo1', 'foo2']);
|
||||
authScope.registerGetter(() => 'bar2');
|
||||
|
||||
expect(await authScope.getForRequestAndUser({}, {})).to.eql(['foo', 'bar', 'foo1', 'foo2', 'bar2']);
|
||||
});
|
||||
});
|
||||
});
|
135
x-pack/plugins/security/server/lib/auth_scope_service.test.ts
Normal file
135
x-pack/plugins/security/server/lib/auth_scope_service.test.ts
Normal file
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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 sinon from 'sinon';
|
||||
import { AuthenticatedUser } from '../../common/model';
|
||||
import { requestFixture } from './__tests__/__fixtures__/request';
|
||||
|
||||
import { AuthScopeService } from './auth_scope_service';
|
||||
|
||||
describe('getCredentialsScope()', function() {
|
||||
describe('basics', () => {
|
||||
it('does not take any arguments', () => {
|
||||
const authScope = new AuthScopeService();
|
||||
expect(authScope).toBeInstanceOf(AuthScopeService);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerGetter()', () => {
|
||||
it('throws when not passed a function', () => {
|
||||
const authScope = new AuthScopeService();
|
||||
expect(() => authScope.registerGetter(undefined as any)).toThrowError(
|
||||
'Expected `getterFunction` to be a function'
|
||||
);
|
||||
expect(() => authScope.registerGetter(null as any)).toThrowError(
|
||||
'Expected `getterFunction` to be a function'
|
||||
);
|
||||
expect(() => authScope.registerGetter(0 as any)).toThrowError(
|
||||
'Expected `getterFunction` to be a function'
|
||||
);
|
||||
expect(() => authScope.registerGetter('' as any)).toThrowError(
|
||||
'Expected `getterFunction` to be a function'
|
||||
);
|
||||
expect(() => authScope.registerGetter([] as any)).toThrowError(
|
||||
'Expected `getterFunction` to be a function'
|
||||
);
|
||||
expect(() => authScope.registerGetter({} as any)).toThrowError(
|
||||
'Expected `getterFunction` to be a function'
|
||||
);
|
||||
});
|
||||
|
||||
it('only calls the passed function when #getForRequestAndUser() is called', async () => {
|
||||
const authScope = new AuthScopeService();
|
||||
|
||||
const stub = sinon.stub();
|
||||
authScope.registerGetter(stub);
|
||||
sinon.assert.notCalled(stub);
|
||||
|
||||
const request = requestFixture();
|
||||
const user = {} as AuthenticatedUser;
|
||||
await authScope.getForRequestAndUser(request, user);
|
||||
sinon.assert.calledOnce(stub);
|
||||
sinon.assert.calledWithExactly(stub, request, user);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getForRequestAndUser()', () => {
|
||||
it('throws when request and user are not objects', async () => {
|
||||
const authScope = new AuthScopeService();
|
||||
|
||||
await expect(authScope.getForRequestAndUser(null as any, {} as any)).rejects.toThrowError(
|
||||
'getCredentialsScope() requires a request object'
|
||||
);
|
||||
await expect(authScope.getForRequestAndUser(1 as any, {} as any)).rejects.toThrowError(
|
||||
'getCredentialsScope() requires a request object'
|
||||
);
|
||||
await expect(authScope.getForRequestAndUser('abc' as any, {} as any)).rejects.toThrowError(
|
||||
'getCredentialsScope() requires a request object'
|
||||
);
|
||||
|
||||
await expect(authScope.getForRequestAndUser({} as any, null as any)).rejects.toThrowError(
|
||||
'getCredentialsScope() requires a user object'
|
||||
);
|
||||
await expect(authScope.getForRequestAndUser({} as any, 1 as any)).rejects.toThrowError(
|
||||
'getCredentialsScope() requires a user object'
|
||||
);
|
||||
await expect(authScope.getForRequestAndUser({} as any, 'abc' as any)).rejects.toThrowError(
|
||||
'getCredentialsScope() requires a user object'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a promise for an empty array by default', async () => {
|
||||
const authScope = new AuthScopeService();
|
||||
const scope = await authScope.getForRequestAndUser(requestFixture(), {} as any);
|
||||
expect(scope).toEqual([]);
|
||||
});
|
||||
|
||||
it('calls each registered getter once each call', async () => {
|
||||
const authScope = new AuthScopeService();
|
||||
const user = {} as any;
|
||||
const request = requestFixture();
|
||||
const getter1 = sinon.stub();
|
||||
const getter2 = sinon.stub();
|
||||
const getter3 = sinon.stub();
|
||||
|
||||
authScope.registerGetter(getter1);
|
||||
authScope.registerGetter(getter2);
|
||||
authScope.registerGetter(getter3);
|
||||
|
||||
await authScope.getForRequestAndUser(request, user);
|
||||
sinon.assert.calledOnce(getter1);
|
||||
sinon.assert.calledOnce(getter2);
|
||||
sinon.assert.calledOnce(getter3);
|
||||
|
||||
await authScope.getForRequestAndUser(request, user);
|
||||
sinon.assert.calledTwice(getter1);
|
||||
sinon.assert.calledTwice(getter2);
|
||||
sinon.assert.calledTwice(getter3);
|
||||
|
||||
await authScope.getForRequestAndUser(request, user);
|
||||
sinon.assert.calledThrice(getter1);
|
||||
sinon.assert.calledThrice(getter2);
|
||||
sinon.assert.calledThrice(getter3);
|
||||
});
|
||||
|
||||
it('casts the return value of the getters to an produce a flat, unique array', async () => {
|
||||
const authScope = new AuthScopeService();
|
||||
authScope.registerGetter(() => undefined as any);
|
||||
authScope.registerGetter(() => null as any);
|
||||
authScope.registerGetter(() => ['foo', undefined, 'bar'] as any);
|
||||
authScope.registerGetter(() => ['foo', 'foo1', 'foo2']);
|
||||
authScope.registerGetter(() => 'bar2');
|
||||
|
||||
expect(await authScope.getForRequestAndUser(requestFixture(), {} as any)).toEqual([
|
||||
'foo',
|
||||
'bar',
|
||||
'foo1',
|
||||
'foo2',
|
||||
'bar2',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,7 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Legacy } from 'kibana';
|
||||
import { uniq, flattenDeep } from 'lodash';
|
||||
import { AuthenticatedUser } from '../../common/model';
|
||||
|
||||
export type ScopesGetter = (request: Legacy.Request, user: AuthenticatedUser) => string[] | string;
|
||||
|
||||
/**
|
||||
* Manages the creation of the scopes attached to the credentials which result
|
||||
|
@ -17,11 +21,9 @@ import { uniq, flattenDeep } from 'lodash';
|
|||
* This service's primary reason for existing it to track the list of functions
|
||||
* that have been provided by plugins to which should be used to create the scopes
|
||||
* for requests.
|
||||
*
|
||||
* @type {AuthScopeService}
|
||||
*/
|
||||
export class AuthScopeService {
|
||||
_getterFunctions = [];
|
||||
private readonly getterFunctions: ScopesGetter[] = [];
|
||||
|
||||
/**
|
||||
* Add a function that will be used to determine the list of scopes for a
|
||||
|
@ -33,28 +35,27 @@ export class AuthScopeService {
|
|||
* The function should return either an array of tags (strings) or a
|
||||
* promise that resolves to an array of tags.
|
||||
*
|
||||
* @param {Function} getterFunction
|
||||
* @param getterFunction Auth scope getter function.
|
||||
*/
|
||||
registerGetter(getterFunction) {
|
||||
registerGetter(getterFunction: ScopesGetter) {
|
||||
if (typeof getterFunction !== 'function') {
|
||||
throw new TypeError('Expected `getterFunction` to be a function');
|
||||
}
|
||||
|
||||
this._getterFunctions.push(getterFunction);
|
||||
this.getterFunctions.push(getterFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the scope for a specific user/request. Hapi credentials (the
|
||||
* result of a hapi auth scheme) can have a `scope` property that lists scope
|
||||
* "tags" which can then be required by routes using the
|
||||
* `route.config.auth.access.scope` property, or accessed in pre-functions,
|
||||
* extensions, or route handlers as `request.auth.credentials.scope`.
|
||||
*
|
||||
* @param {Hapi.Request} request
|
||||
* @param {Object} user user object from the security API
|
||||
* @return {Array<string>} a list of scope tags
|
||||
*/
|
||||
async getForRequestAndUser(request, user) {
|
||||
* Determine the scope for a specific user/request. Hapi credentials (the
|
||||
* result of a hapi auth scheme) can have a `scope` property that lists scope
|
||||
* "tags" which can then be required by routes using the
|
||||
* `route.config.auth.access.scope` property, or accessed in pre-functions,
|
||||
* extensions, or route handlers as `request.auth.credentials.scope`.
|
||||
*
|
||||
* @param request Request instance.
|
||||
* @param user User object from the security API
|
||||
*/
|
||||
async getForRequestAndUser(request: Legacy.Request, user: AuthenticatedUser): Promise<string[]> {
|
||||
if (!request || typeof request !== 'object') {
|
||||
throw new TypeError('getCredentialsScope() requires a request object');
|
||||
}
|
||||
|
@ -63,10 +64,8 @@ export class AuthScopeService {
|
|||
throw new TypeError('getCredentialsScope() requires a user object');
|
||||
}
|
||||
|
||||
const getterResults = await Promise.all(
|
||||
this._getterFunctions.map(fn => fn(request, user))
|
||||
);
|
||||
const getterResults = await Promise.all(this.getterFunctions.map(fn => fn(request, user)));
|
||||
|
||||
return uniq(flattenDeep(getterResults).filter(Boolean));
|
||||
return uniq(flattenDeep<string>(getterResults).filter(Boolean));
|
||||
}
|
||||
}
|
|
@ -4,30 +4,35 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import sinon from 'sinon';
|
||||
import Boom from 'boom';
|
||||
import { Legacy } from 'kibana';
|
||||
|
||||
import { serverFixture } from '../../__tests__/__fixtures__/server';
|
||||
import { requestFixture } from '../../__tests__/__fixtures__/request';
|
||||
import { Session } from '../session';
|
||||
import { AuthScopeService } from '../../auth_scope_service';
|
||||
import { LoginAttempt } from '../login_attempt';
|
||||
import { initAuthenticator } from '../authenticator';
|
||||
import * as ClientShield from '../../../../../../server/lib/get_client_shield';
|
||||
import { serverFixture } from '../__tests__/__fixtures__/server';
|
||||
import { requestFixture } from '../__tests__/__fixtures__/request';
|
||||
import { AuthenticationResult } from './authentication_result';
|
||||
import { DeauthenticationResult } from './deauthentication_result';
|
||||
import { Session } from './session';
|
||||
import { AuthScopeService } from '../auth_scope_service';
|
||||
import { LoginAttempt } from './login_attempt';
|
||||
import { initAuthenticator } from './authenticator';
|
||||
import * as ClientShield from '../../../../../server/lib/get_client_shield';
|
||||
|
||||
describe('Authenticator', () => {
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
let config;
|
||||
let server;
|
||||
let session;
|
||||
let cluster;
|
||||
let config: sinon.SinonStubbedInstance<Legacy.KibanaConfig>;
|
||||
let server: ReturnType<typeof serverFixture>;
|
||||
let session: sinon.SinonStubbedInstance<Session>;
|
||||
let cluster: sinon.SinonStubbedInstance<{
|
||||
callWithRequest: (request: ReturnType<typeof requestFixture>, ...args: any[]) => any;
|
||||
callWithInternalUser: (...args: any[]) => any;
|
||||
}>;
|
||||
beforeEach(() => {
|
||||
server = serverFixture();
|
||||
session = sinon.createStubInstance(Session);
|
||||
|
||||
config = { get: sinon.stub() };
|
||||
config = { get: sinon.stub(), has: sinon.stub() };
|
||||
|
||||
// Cluster is returned by `getClient` function that is wrapped into `once` making cluster
|
||||
// a static singleton, so we should use sandbox to set/reset its behavior between tests.
|
||||
|
@ -37,92 +42,74 @@ describe('Authenticator', () => {
|
|||
server.config.returns(config);
|
||||
server.register.yields();
|
||||
|
||||
sandbox.stub(Session, 'create').withArgs(server).returns(Promise.resolve(session));
|
||||
sandbox.stub(AuthScopeService.prototype, 'getForRequestAndUser')
|
||||
.returns(Promise.resolve([]));
|
||||
sandbox
|
||||
.stub(Session, 'create')
|
||||
.withArgs(server as any)
|
||||
.resolves(session as any);
|
||||
sandbox.stub(AuthScopeService.prototype, 'getForRequestAndUser').resolves([]);
|
||||
|
||||
sandbox.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
describe('initialization', () => {
|
||||
it('fails if authentication providers are not configured.', async () => {
|
||||
config.get.withArgs('xpack.security.authProviders').returns([]);
|
||||
|
||||
try {
|
||||
await initAuthenticator(server);
|
||||
expect().fail('`initAuthenticator` should fail.');
|
||||
} catch(err) {
|
||||
expect(err).to.be.a(Error);
|
||||
expect(err.message).to.be(
|
||||
'No authentication provider is configured. Verify `xpack.security.authProviders` config value.'
|
||||
);
|
||||
}
|
||||
await expect(initAuthenticator(server as any)).rejects.toThrowError(
|
||||
'No authentication provider is configured. Verify `xpack.security.authProviders` config value.'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails if configured authentication provider is not known.', async () => {
|
||||
config.get.withArgs('xpack.security.authProviders').returns(['super-basic']);
|
||||
|
||||
try {
|
||||
await initAuthenticator(server);
|
||||
expect().fail('`initAuthenticator` should fail.');
|
||||
} catch(err) {
|
||||
expect(err).to.be.a(Error);
|
||||
expect(err.message).to.be('Unsupported authentication provider name: super-basic.');
|
||||
}
|
||||
await expect(initAuthenticator(server as any)).rejects.toThrowError(
|
||||
'Unsupported authentication provider name: super-basic.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`authenticate` method', () => {
|
||||
let authenticate;
|
||||
let authenticate: (request: ReturnType<typeof requestFixture>) => Promise<AuthenticationResult>;
|
||||
beforeEach(async () => {
|
||||
config.get.withArgs('xpack.security.authProviders').returns(['basic']);
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest.returns(true);
|
||||
session.clear.throws(new Error('`Session.clear` is not supposed to be called!'));
|
||||
|
||||
await initAuthenticator(server);
|
||||
await initAuthenticator(server as any);
|
||||
|
||||
// Second argument will be a method we'd like to test.
|
||||
authenticate = server.expose.withArgs('authenticate').firstCall.args[1];
|
||||
});
|
||||
|
||||
it('fails if request is not provided.', async () => {
|
||||
try {
|
||||
await authenticate();
|
||||
expect().fail('`authenticate` should fail.');
|
||||
} catch(err) {
|
||||
expect(err).to.be.a(Error);
|
||||
expect(err.message).to.be('Request should be a valid object, was [undefined].');
|
||||
}
|
||||
await expect(authenticate(undefined as any)).rejects.toThrowError(
|
||||
'Request should be a valid object, was [undefined].'
|
||||
);
|
||||
});
|
||||
|
||||
it('fails if any authentication providers fail.', async () => {
|
||||
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
|
||||
session.get.withArgs(request).returns(Promise.resolve(null));
|
||||
session.get.withArgs(request).resolves(null);
|
||||
|
||||
const failureReason = new Error('Not Authorized');
|
||||
cluster.callWithRequest.withArgs(request).returns(Promise.reject(failureReason));
|
||||
cluster.callWithRequest.withArgs(request).rejects(failureReason);
|
||||
|
||||
const authenticationResult = await authenticate(request);
|
||||
|
||||
expect(authenticationResult.failed()).to.be(true);
|
||||
expect(authenticationResult.error).to.be(failureReason);
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
expect(authenticationResult.error).toBe(failureReason);
|
||||
});
|
||||
|
||||
it('returns user that authentication provider returns.', async () => {
|
||||
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
|
||||
const user = { username: 'user' };
|
||||
cluster.callWithRequest.withArgs(request).returns(Promise.resolve(user));
|
||||
cluster.callWithRequest.withArgs(request).resolves(user);
|
||||
|
||||
const authenticationResult = await authenticate(request);
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql({
|
||||
...user,
|
||||
scope: []
|
||||
});
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toEqual({ ...user, scope: [] });
|
||||
});
|
||||
|
||||
it('creates session whenever authentication provider returns state for system API requests', async () => {
|
||||
|
@ -131,24 +118,19 @@ describe('Authenticator', () => {
|
|||
const loginAttempt = new LoginAttempt();
|
||||
const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`;
|
||||
loginAttempt.setCredentials('foo', 'bar');
|
||||
request.loginAttempt.returns(loginAttempt);
|
||||
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(request).returns(true);
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest.withArgs(request).returns(true);
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(request).returns(Promise.resolve(user));
|
||||
cluster.callWithRequest.withArgs(request).resolves(user);
|
||||
|
||||
const systemAPIAuthenticationResult = await authenticate(request);
|
||||
expect(systemAPIAuthenticationResult.succeeded()).to.be(true);
|
||||
expect(systemAPIAuthenticationResult.user).to.be.eql({
|
||||
...user,
|
||||
scope: []
|
||||
});
|
||||
expect(systemAPIAuthenticationResult.succeeded()).toBe(true);
|
||||
expect(systemAPIAuthenticationResult.user).toEqual({ ...user, scope: [] });
|
||||
sinon.assert.calledOnce(session.set);
|
||||
sinon.assert.calledWithExactly(session.set, request, {
|
||||
state: { authorization },
|
||||
provider: 'basic'
|
||||
provider: 'basic',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -158,24 +140,19 @@ describe('Authenticator', () => {
|
|||
const loginAttempt = new LoginAttempt();
|
||||
const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`;
|
||||
loginAttempt.setCredentials('foo', 'bar');
|
||||
request.loginAttempt.returns(loginAttempt);
|
||||
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(request).returns(false);
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest.withArgs(request).returns(false);
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(request).returns(Promise.resolve(user));
|
||||
cluster.callWithRequest.withArgs(request).resolves(user);
|
||||
|
||||
const notSystemAPIAuthenticationResult = await authenticate(request);
|
||||
expect(notSystemAPIAuthenticationResult.succeeded()).to.be(true);
|
||||
expect(notSystemAPIAuthenticationResult.user).to.be.eql({
|
||||
...user,
|
||||
scope: []
|
||||
});
|
||||
expect(notSystemAPIAuthenticationResult.succeeded()).toBe(true);
|
||||
expect(notSystemAPIAuthenticationResult.user).toEqual({ ...user, scope: [] });
|
||||
sinon.assert.calledOnce(session.set);
|
||||
sinon.assert.calledWithExactly(session.set, request, {
|
||||
state: { authorization },
|
||||
provider: 'basic'
|
||||
provider: 'basic',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -184,42 +161,40 @@ describe('Authenticator', () => {
|
|||
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
|
||||
const notSystemAPIRequest = requestFixture({ headers: { xCustomHeader: 'yyy' } });
|
||||
|
||||
session.get.withArgs(systemAPIRequest).returns(Promise.resolve({
|
||||
session.get.withArgs(systemAPIRequest).resolves({
|
||||
state: { authorization: 'Basic xxx' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
session.get.withArgs(notSystemAPIRequest).returns(Promise.resolve({
|
||||
session.get.withArgs(notSystemAPIRequest).resolves({
|
||||
state: { authorization: 'Basic yyy' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(systemAPIRequest).returns(true)
|
||||
.withArgs(notSystemAPIRequest).returns(false);
|
||||
.withArgs(systemAPIRequest)
|
||||
.returns(true)
|
||||
.withArgs(notSystemAPIRequest)
|
||||
.returns(false);
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(systemAPIRequest).returns(Promise.resolve(user))
|
||||
.withArgs(notSystemAPIRequest).returns(Promise.resolve(user));
|
||||
.withArgs(systemAPIRequest)
|
||||
.resolves(user)
|
||||
.withArgs(notSystemAPIRequest)
|
||||
.resolves(user);
|
||||
|
||||
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
|
||||
expect(systemAPIAuthenticationResult.succeeded()).to.be(true);
|
||||
expect(systemAPIAuthenticationResult.user).to.be.eql({
|
||||
...user,
|
||||
scope: []
|
||||
});
|
||||
expect(systemAPIAuthenticationResult.succeeded()).toBe(true);
|
||||
expect(systemAPIAuthenticationResult.user).toEqual({ ...user, scope: [] });
|
||||
sinon.assert.notCalled(session.set);
|
||||
|
||||
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
|
||||
expect(notSystemAPIAuthenticationResult.succeeded()).to.be(true);
|
||||
expect(notSystemAPIAuthenticationResult.user).to.be.eql({
|
||||
...user,
|
||||
scope: []
|
||||
});
|
||||
expect(notSystemAPIAuthenticationResult.succeeded()).toBe(true);
|
||||
expect(notSystemAPIAuthenticationResult.user).toEqual({ ...user, scope: [] });
|
||||
sinon.assert.calledOnce(session.set);
|
||||
sinon.assert.calledWithExactly(session.set, notSystemAPIRequest, {
|
||||
state: { authorization: 'Basic yyy' },
|
||||
provider: 'basic'
|
||||
provider: 'basic',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -227,29 +202,33 @@ describe('Authenticator', () => {
|
|||
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
|
||||
const notSystemAPIRequest = requestFixture({ headers: { xCustomHeader: 'yyy' } });
|
||||
|
||||
session.get.withArgs(systemAPIRequest).returns(Promise.resolve({
|
||||
session.get.withArgs(systemAPIRequest).resolves({
|
||||
state: { authorization: 'Basic xxx' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
session.get.withArgs(notSystemAPIRequest).returns(Promise.resolve({
|
||||
session.get.withArgs(notSystemAPIRequest).resolves({
|
||||
state: { authorization: 'Basic yyy' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(systemAPIRequest).returns(true)
|
||||
.withArgs(notSystemAPIRequest).returns(false);
|
||||
.withArgs(systemAPIRequest)
|
||||
.returns(true)
|
||||
.withArgs(notSystemAPIRequest)
|
||||
.returns(false);
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(systemAPIRequest).returns(Promise.reject(new Error('some error')))
|
||||
.withArgs(notSystemAPIRequest).returns(Promise.reject(new Error('some error')));
|
||||
.withArgs(systemAPIRequest)
|
||||
.rejects(new Error('some error'))
|
||||
.withArgs(notSystemAPIRequest)
|
||||
.rejects(new Error('some error'));
|
||||
|
||||
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
|
||||
expect(systemAPIAuthenticationResult.failed()).to.be(true);
|
||||
expect(systemAPIAuthenticationResult.failed()).toBe(true);
|
||||
|
||||
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
|
||||
expect(notSystemAPIAuthenticationResult.failed()).to.be(true);
|
||||
expect(notSystemAPIAuthenticationResult.failed()).toBe(true);
|
||||
|
||||
sinon.assert.notCalled(session.clear);
|
||||
sinon.assert.notCalled(session.set);
|
||||
|
@ -261,29 +240,24 @@ describe('Authenticator', () => {
|
|||
const request = requestFixture();
|
||||
const loginAttempt = new LoginAttempt();
|
||||
loginAttempt.setCredentials('foo', 'bar');
|
||||
request.loginAttempt.returns(loginAttempt);
|
||||
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
|
||||
|
||||
session.get.withArgs(request).returns(Promise.resolve({
|
||||
session.get.withArgs(request).resolves({
|
||||
state: { authorization: 'Basic some-old-token' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(request).returns(true);
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest.withArgs(request).returns(true);
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(request).returns(Promise.resolve(user));
|
||||
cluster.callWithRequest.withArgs(request).resolves(user);
|
||||
|
||||
const authenticationResult = await authenticate(request);
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql({
|
||||
...user,
|
||||
scope: []
|
||||
});
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toEqual({ ...user, scope: [] });
|
||||
sinon.assert.calledOnce(session.set);
|
||||
sinon.assert.calledWithExactly(session.set, request, {
|
||||
state: { authorization },
|
||||
provider: 'basic'
|
||||
provider: 'basic',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -293,29 +267,24 @@ describe('Authenticator', () => {
|
|||
const request = requestFixture();
|
||||
const loginAttempt = new LoginAttempt();
|
||||
loginAttempt.setCredentials('foo', 'bar');
|
||||
request.loginAttempt.returns(loginAttempt);
|
||||
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
|
||||
|
||||
session.get.withArgs(request).returns(Promise.resolve({
|
||||
session.get.withArgs(request).resolves({
|
||||
state: { authorization: 'Basic some-old-token' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(request).returns(false);
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest.withArgs(request).returns(false);
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(request).returns(Promise.resolve(user));
|
||||
cluster.callWithRequest.withArgs(request).resolves(user);
|
||||
|
||||
const authenticationResult = await authenticate(request);
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql({
|
||||
...user,
|
||||
scope: []
|
||||
});
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toEqual({ ...user, scope: [] });
|
||||
sinon.assert.calledOnce(session.set);
|
||||
sinon.assert.calledWithExactly(session.set, request, {
|
||||
state: { authorization },
|
||||
provider: 'basic'
|
||||
provider: 'basic',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -323,34 +292,38 @@ describe('Authenticator', () => {
|
|||
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
|
||||
const notSystemAPIRequest = requestFixture({ headers: { xCustomHeader: 'yyy' } });
|
||||
|
||||
session.get.withArgs(systemAPIRequest).returns(Promise.resolve({
|
||||
session.get.withArgs(systemAPIRequest).resolves({
|
||||
state: { authorization: 'Basic xxx' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
session.get.withArgs(notSystemAPIRequest).returns(Promise.resolve({
|
||||
session.get.withArgs(notSystemAPIRequest).resolves({
|
||||
state: { authorization: 'Basic yyy' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
session.clear.returns(Promise.resolve());
|
||||
session.clear.resolves();
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(systemAPIRequest).returns(true)
|
||||
.withArgs(notSystemAPIRequest).returns(false);
|
||||
.withArgs(systemAPIRequest)
|
||||
.returns(true)
|
||||
.withArgs(notSystemAPIRequest)
|
||||
.returns(false);
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(systemAPIRequest).returns(Promise.reject(Boom.unauthorized('token expired')))
|
||||
.withArgs(notSystemAPIRequest).returns(Promise.reject(Boom.unauthorized('invalid token')));
|
||||
.withArgs(systemAPIRequest)
|
||||
.rejects(Boom.unauthorized('token expired'))
|
||||
.withArgs(notSystemAPIRequest)
|
||||
.rejects(Boom.unauthorized('invalid token'));
|
||||
|
||||
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
|
||||
expect(systemAPIAuthenticationResult.failed()).to.be(true);
|
||||
expect(systemAPIAuthenticationResult.failed()).toBe(true);
|
||||
|
||||
sinon.assert.calledOnce(session.clear);
|
||||
sinon.assert.calledWithExactly(session.clear, systemAPIRequest);
|
||||
|
||||
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
|
||||
expect(notSystemAPIAuthenticationResult.failed()).to.be(true);
|
||||
expect(notSystemAPIAuthenticationResult.failed()).toBe(true);
|
||||
|
||||
sinon.assert.calledTwice(session.clear);
|
||||
sinon.assert.calledWithExactly(session.clear, notSystemAPIRequest);
|
||||
|
@ -359,97 +332,101 @@ describe('Authenticator', () => {
|
|||
it('clears session if provider requested it via setting state to `null`.', async () => {
|
||||
// Use `token` provider for this test as it's the only one that does what we want.
|
||||
config.get.withArgs('xpack.security.authProviders').returns(['token']);
|
||||
await initAuthenticator(server);
|
||||
await initAuthenticator(server as any);
|
||||
authenticate = server.expose.withArgs('authenticate').lastCall.args[1];
|
||||
|
||||
const request = requestFixture({ headers: { xCustomHeader: 'xxx' } });
|
||||
|
||||
session.get.withArgs(request).resolves({
|
||||
state: { accessToken: 'access-xxx', refreshToken: 'refresh-xxx' },
|
||||
provider: 'token'
|
||||
provider: 'token',
|
||||
});
|
||||
|
||||
session.clear.resolves();
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(request).rejects({ statusCode: 401 });
|
||||
cluster.callWithRequest.withArgs(request).rejects({ statusCode: 401 });
|
||||
|
||||
cluster.callWithInternalUser.withArgs('shield.getAccessToken').rejects(
|
||||
Boom.badRequest('refresh token expired')
|
||||
);
|
||||
cluster.callWithInternalUser
|
||||
.withArgs('shield.getAccessToken')
|
||||
.rejects(Boom.badRequest('refresh token expired'));
|
||||
|
||||
const authenticationResult = await authenticate(request);
|
||||
expect(authenticationResult.redirected()).to.be(true);
|
||||
expect(authenticationResult.redirected()).toBe(true);
|
||||
|
||||
sinon.assert.calledOnce(session.clear);
|
||||
sinon.assert.calledWithExactly(session.clear, request);
|
||||
});
|
||||
|
||||
it('does not clear session if provider failed to authenticate request with non-401 reason with active session.',
|
||||
async () => {
|
||||
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
|
||||
const notSystemAPIRequest = requestFixture({ headers: { xCustomHeader: 'yyy' } });
|
||||
it('does not clear session if provider failed to authenticate request with non-401 reason with active session.', async () => {
|
||||
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
|
||||
const notSystemAPIRequest = requestFixture({ headers: { xCustomHeader: 'yyy' } });
|
||||
|
||||
session.get.withArgs(systemAPIRequest).returns(Promise.resolve({
|
||||
state: { authorization: 'Basic xxx' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
|
||||
session.get.withArgs(notSystemAPIRequest).returns(Promise.resolve({
|
||||
state: { authorization: 'Basic yyy' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
|
||||
session.clear.returns(Promise.resolve());
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(systemAPIRequest).returns(true)
|
||||
.withArgs(notSystemAPIRequest).returns(false);
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(systemAPIRequest).returns(Promise.reject(Boom.badRequest('something went wrong')))
|
||||
.withArgs(notSystemAPIRequest).returns(Promise.reject(new Error('Non boom error')));
|
||||
|
||||
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
|
||||
expect(systemAPIAuthenticationResult.failed()).to.be(true);
|
||||
|
||||
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
|
||||
expect(notSystemAPIAuthenticationResult.failed()).to.be(true);
|
||||
|
||||
sinon.assert.notCalled(session.clear);
|
||||
session.get.withArgs(systemAPIRequest).resolves({
|
||||
state: { authorization: 'Basic xxx' },
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
session.get.withArgs(notSystemAPIRequest).resolves({
|
||||
state: { authorization: 'Basic yyy' },
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
session.clear.resolves();
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(systemAPIRequest)
|
||||
.returns(true)
|
||||
.withArgs(notSystemAPIRequest)
|
||||
.returns(false);
|
||||
|
||||
cluster.callWithRequest
|
||||
.withArgs(systemAPIRequest)
|
||||
.rejects(Boom.badRequest('something went wrong'))
|
||||
.withArgs(notSystemAPIRequest)
|
||||
.rejects(new Error('Non boom error'));
|
||||
|
||||
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
|
||||
expect(systemAPIAuthenticationResult.failed()).toBe(true);
|
||||
|
||||
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
|
||||
expect(notSystemAPIAuthenticationResult.failed()).toBe(true);
|
||||
|
||||
sinon.assert.notCalled(session.clear);
|
||||
});
|
||||
|
||||
it('does not clear session if provider can not handle request authentication with active session.', async () => {
|
||||
// Add `kbn-xsrf` header to the raw part of the request to make `can_redirect_request`
|
||||
// think that it's AJAX request and redirect logic shouldn't be triggered.
|
||||
const systemAPIRequest = requestFixture({
|
||||
headers: { xCustomHeader: 'xxx', 'kbn-xsrf': 'xsrf' }
|
||||
headers: { xCustomHeader: 'xxx', 'kbn-xsrf': 'xsrf' },
|
||||
});
|
||||
const notSystemAPIRequest = requestFixture({
|
||||
headers: { xCustomHeader: 'yyy', 'kbn-xsrf': 'xsrf' }
|
||||
headers: { xCustomHeader: 'yyy', 'kbn-xsrf': 'xsrf' },
|
||||
});
|
||||
|
||||
session.get.withArgs(systemAPIRequest).returns(Promise.resolve({
|
||||
session.get.withArgs(systemAPIRequest).resolves({
|
||||
state: { authorization: 'Some weird authentication schema...' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
session.get.withArgs(notSystemAPIRequest).returns(Promise.resolve({
|
||||
session.get.withArgs(notSystemAPIRequest).resolves({
|
||||
state: { authorization: 'Some weird authentication schema...' },
|
||||
provider: 'basic'
|
||||
}));
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
session.clear.returns(Promise.resolve());
|
||||
session.clear.resolves();
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(systemAPIRequest).returns(true)
|
||||
.withArgs(notSystemAPIRequest).returns(false);
|
||||
.withArgs(systemAPIRequest)
|
||||
.returns(true)
|
||||
.withArgs(notSystemAPIRequest)
|
||||
.returns(false);
|
||||
|
||||
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
|
||||
expect(systemAPIAuthenticationResult.failed()).to.be(true);
|
||||
expect(systemAPIAuthenticationResult.failed()).toBe(true);
|
||||
|
||||
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
|
||||
expect(notSystemAPIAuthenticationResult.failed()).to.be(true);
|
||||
expect(notSystemAPIAuthenticationResult.failed()).toBe(true);
|
||||
|
||||
sinon.assert.notCalled(session.clear);
|
||||
});
|
||||
|
@ -458,34 +435,36 @@ describe('Authenticator', () => {
|
|||
// Add `kbn-xsrf` header to the raw part of the request to make `can_redirect_request`
|
||||
// think that it's AJAX request and redirect logic shouldn't be triggered.
|
||||
const systemAPIRequest = requestFixture({
|
||||
headers: { xCustomHeader: 'xxx', 'kbn-xsrf': 'xsrf' }
|
||||
headers: { xCustomHeader: 'xxx', 'kbn-xsrf': 'xsrf' },
|
||||
});
|
||||
const notSystemAPIRequest = requestFixture({
|
||||
headers: { xCustomHeader: 'yyy', 'kbn-xsrf': 'xsrf' }
|
||||
headers: { xCustomHeader: 'yyy', 'kbn-xsrf': 'xsrf' },
|
||||
});
|
||||
|
||||
session.get.withArgs(systemAPIRequest).resolves({
|
||||
state: { accessToken: 'some old token' },
|
||||
provider: 'token'
|
||||
provider: 'token',
|
||||
});
|
||||
|
||||
session.get.withArgs(notSystemAPIRequest).resolves({
|
||||
state: { accessToken: 'some old token' },
|
||||
provider: 'token'
|
||||
provider: 'token',
|
||||
});
|
||||
|
||||
session.clear.resolves();
|
||||
|
||||
server.plugins.kibana.systemApi.isSystemApiRequest
|
||||
.withArgs(systemAPIRequest).returns(true)
|
||||
.withArgs(notSystemAPIRequest).returns(false);
|
||||
.withArgs(systemAPIRequest)
|
||||
.returns(true)
|
||||
.withArgs(notSystemAPIRequest)
|
||||
.returns(false);
|
||||
|
||||
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
|
||||
expect(systemAPIAuthenticationResult.notHandled()).to.be(true);
|
||||
expect(systemAPIAuthenticationResult.notHandled()).toBe(true);
|
||||
sinon.assert.calledOnce(session.clear);
|
||||
|
||||
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
|
||||
expect(notSystemAPIAuthenticationResult.notHandled()).to.be(true);
|
||||
expect(notSystemAPIAuthenticationResult.notHandled()).toBe(true);
|
||||
sinon.assert.calledTwice(session.clear);
|
||||
});
|
||||
|
||||
|
@ -493,88 +472,85 @@ describe('Authenticator', () => {
|
|||
const user = { username: 'user' };
|
||||
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
|
||||
|
||||
cluster.callWithRequest.withArgs(request)
|
||||
.returns(Promise.resolve(user));
|
||||
AuthScopeService.prototype.getForRequestAndUser.withArgs(request, user)
|
||||
.returns(Promise.resolve(['foo', 'bar']));
|
||||
cluster.callWithRequest.withArgs(request).resolves(user);
|
||||
(AuthScopeService.prototype.getForRequestAndUser as sinon.SinonStub)
|
||||
.withArgs(request, user)
|
||||
.resolves(['foo', 'bar']);
|
||||
|
||||
const authenticationResult = await authenticate(request);
|
||||
expect(authenticationResult.succeeded()).to.be(true);
|
||||
expect(authenticationResult.user).to.be.eql({
|
||||
...user,
|
||||
scope: ['foo', 'bar']
|
||||
});
|
||||
expect(authenticationResult.succeeded()).toBe(true);
|
||||
expect(authenticationResult.user).toEqual({ ...user, scope: ['foo', 'bar'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('`deauthenticate` method', () => {
|
||||
let deauthenticate;
|
||||
let deauthenticate: (
|
||||
request: ReturnType<typeof requestFixture>
|
||||
) => Promise<DeauthenticationResult>;
|
||||
beforeEach(async () => {
|
||||
config.get.withArgs('xpack.security.authProviders').returns(['basic']);
|
||||
config.get.withArgs('server.basePath').returns('/base-path');
|
||||
|
||||
await initAuthenticator(server);
|
||||
await initAuthenticator(server as any);
|
||||
|
||||
// Second argument will be a method we'd like to test.
|
||||
deauthenticate = server.expose.withArgs('deauthenticate').firstCall.args[1];
|
||||
});
|
||||
|
||||
it('fails if request is not provided.', async () => {
|
||||
try {
|
||||
await deauthenticate();
|
||||
expect().fail('`deauthenticate` should fail.');
|
||||
} catch(err) {
|
||||
expect(err).to.be.a(Error);
|
||||
expect(err.message).to.be('Request should be a valid object, was [undefined].');
|
||||
}
|
||||
await expect(deauthenticate(undefined as any)).rejects.toThrowError(
|
||||
'Request should be a valid object, was [undefined].'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns `notHandled` if session does not exist.', async () => {
|
||||
const request = requestFixture();
|
||||
session.get.withArgs(request).returns(null);
|
||||
session.get.withArgs(request).resolves(null);
|
||||
|
||||
const deauthenticationResult = await deauthenticate(request);
|
||||
|
||||
expect(deauthenticationResult.notHandled()).to.be(true);
|
||||
expect(deauthenticationResult.notHandled()).toBe(true);
|
||||
sinon.assert.notCalled(session.clear);
|
||||
});
|
||||
|
||||
it('clears session and returns whatever authentication provider returns.', async () => {
|
||||
const request = requestFixture({ search: '?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' });
|
||||
session.get.withArgs(request).returns(Promise.resolve({
|
||||
state: {},
|
||||
provider: 'basic'
|
||||
}));
|
||||
|
||||
const deauthenticationResult = await deauthenticate(request);
|
||||
|
||||
sinon.assert.calledOnce(session.clear);
|
||||
sinon.assert.calledWithExactly(session.clear, request);
|
||||
expect(deauthenticationResult.redirected()).to.be(true);
|
||||
expect(deauthenticationResult.redirectURL).to.be('/base-path/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED');
|
||||
});
|
||||
|
||||
it('only clears session if it belongs to not configured provider.', async () => {
|
||||
const request = requestFixture({ search: '?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' });
|
||||
session.get.withArgs(request).resolves({
|
||||
state: {},
|
||||
provider: 'token'
|
||||
provider: 'basic',
|
||||
});
|
||||
|
||||
const deauthenticationResult = await deauthenticate(request);
|
||||
|
||||
sinon.assert.calledOnce(session.clear);
|
||||
sinon.assert.calledWithExactly(session.clear, request);
|
||||
expect(deauthenticationResult.notHandled()).to.be(true);
|
||||
expect(deauthenticationResult.redirected()).toBe(true);
|
||||
expect(deauthenticationResult.redirectURL).toBe(
|
||||
'/base-path/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED'
|
||||
);
|
||||
});
|
||||
|
||||
it('only clears session if it belongs to not configured provider.', async () => {
|
||||
const request = requestFixture({ search: '?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' });
|
||||
session.get.withArgs(request).resolves({
|
||||
state: {},
|
||||
provider: 'token',
|
||||
});
|
||||
|
||||
const deauthenticationResult = await deauthenticate(request);
|
||||
|
||||
sinon.assert.calledOnce(session.clear);
|
||||
sinon.assert.calledWithExactly(session.clear, request);
|
||||
expect(deauthenticationResult.notHandled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`isAuthenticated` method', () => {
|
||||
let isAuthenticated;
|
||||
let isAuthenticated: (request: ReturnType<typeof requestFixture>) => Promise<boolean>;
|
||||
beforeEach(async () => {
|
||||
config.get.withArgs('xpack.security.authProviders').returns(['basic']);
|
||||
|
||||
await initAuthenticator(server);
|
||||
await initAuthenticator(server as any);
|
||||
|
||||
// Second argument will be a method we'd like to test.
|
||||
isAuthenticated = server.expose.withArgs('isAuthenticated').firstCall.args[1];
|
||||
|
@ -582,50 +558,32 @@ describe('Authenticator', () => {
|
|||
|
||||
it('returns `true` if `getUser` succeeds.', async () => {
|
||||
const request = requestFixture();
|
||||
server.plugins.security.getUser
|
||||
.withArgs(request)
|
||||
.returns(Promise.resolve({}));
|
||||
server.plugins.security.getUser.withArgs(request).resolves({});
|
||||
|
||||
expect(await isAuthenticated(request)).to.be(true);
|
||||
await expect(isAuthenticated(request)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('returns `false` when `getUser` throws a 401 boom error.', async () => {
|
||||
const request = requestFixture();
|
||||
server.plugins.security.getUser
|
||||
.withArgs(request)
|
||||
.returns(Promise.reject(Boom.unauthorized()));
|
||||
server.plugins.security.getUser.withArgs(request).rejects(Boom.unauthorized());
|
||||
|
||||
expect(await isAuthenticated(request)).to.be(false);
|
||||
await expect(isAuthenticated(request)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('throw non-boom errors.', async () => {
|
||||
const request = requestFixture();
|
||||
const nonBoomError = new TypeError();
|
||||
server.plugins.security.getUser
|
||||
.withArgs(request)
|
||||
.returns(Promise.reject(nonBoomError));
|
||||
server.plugins.security.getUser.withArgs(request).rejects(nonBoomError);
|
||||
|
||||
try {
|
||||
await isAuthenticated(request);
|
||||
throw new Error('`isAuthenticated` should throw.');
|
||||
} catch (err) {
|
||||
expect(err).to.be(nonBoomError);
|
||||
}
|
||||
await expect(isAuthenticated(request)).rejects.toThrowError(nonBoomError);
|
||||
});
|
||||
|
||||
it('throw non-401 boom errors.', async () => {
|
||||
const request = requestFixture();
|
||||
const non401Error = Boom.boomify(new TypeError());
|
||||
server.plugins.security.getUser
|
||||
.withArgs(request)
|
||||
.returns(Promise.reject(non401Error));
|
||||
server.plugins.security.getUser.withArgs(request).rejects(non401Error);
|
||||
|
||||
try {
|
||||
await isAuthenticated(request);
|
||||
throw new Error('`isAuthenticated` should throw.');
|
||||
} catch (err) {
|
||||
expect(err).to.be(non401Error);
|
||||
}
|
||||
await expect(isAuthenticated(request)).rejects.toThrowError(non401Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,26 +4,39 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Legacy } from 'kibana';
|
||||
import { getClient } from '../../../../../server/lib/get_client_shield';
|
||||
import { AuthScopeService } from '../auth_scope_service';
|
||||
import { AuthScopeService, ScopesGetter } from '../auth_scope_service';
|
||||
import { getErrorStatusCode } from '../errors';
|
||||
import { BasicAuthenticationProvider } from './providers/basic';
|
||||
import { SAMLAuthenticationProvider } from './providers/saml';
|
||||
import { TokenAuthenticationProvider } from './providers/token';
|
||||
import {
|
||||
AuthenticationProviderOptions,
|
||||
BaseAuthenticationProvider,
|
||||
BasicAuthenticationProvider,
|
||||
SAMLAuthenticationProvider,
|
||||
TokenAuthenticationProvider,
|
||||
} from './providers';
|
||||
import { AuthenticationResult } from './authentication_result';
|
||||
import { DeauthenticationResult } from './deauthentication_result';
|
||||
import { Session } from './session';
|
||||
import { LoginAttempt } from './login_attempt';
|
||||
|
||||
interface ProviderSession {
|
||||
provider: string;
|
||||
state: unknown;
|
||||
}
|
||||
|
||||
// Mapping between provider key defined in the config and authentication
|
||||
// provider class that can handle specific authentication mechanism.
|
||||
const providerMap = new Map([
|
||||
const providerMap = new Map<
|
||||
string,
|
||||
new (options: AuthenticationProviderOptions) => BaseAuthenticationProvider
|
||||
>([
|
||||
['basic', BasicAuthenticationProvider],
|
||||
['saml', SAMLAuthenticationProvider],
|
||||
['token', TokenAuthenticationProvider],
|
||||
]);
|
||||
|
||||
function assertRequest(request) {
|
||||
function assertRequest(request: Legacy.Request) {
|
||||
if (!request || typeof request !== 'object') {
|
||||
throw new Error(`Request should be a valid object, was [${typeof request}].`);
|
||||
}
|
||||
|
@ -31,10 +44,9 @@ function assertRequest(request) {
|
|||
|
||||
/**
|
||||
* Prepares options object that is shared among all authentication providers.
|
||||
* @param {Hapi.Server} server HapiJS Server instance.
|
||||
* @returns {Object}
|
||||
* @param server Server instance.
|
||||
*/
|
||||
function getProviderOptions(server) {
|
||||
function getProviderOptions(server: Legacy.Server) {
|
||||
const config = server.config();
|
||||
|
||||
return {
|
||||
|
@ -42,14 +54,28 @@ function getProviderOptions(server) {
|
|||
log: server.log.bind(server),
|
||||
|
||||
protocol: server.info.protocol,
|
||||
hostname: config.get('server.host'),
|
||||
port: config.get('server.port'),
|
||||
basePath: config.get('server.basePath'),
|
||||
hostname: config.get<string>('server.host'),
|
||||
port: config.get<number>('server.port'),
|
||||
basePath: config.get<string>('server.basePath'),
|
||||
|
||||
...config.get('xpack.security.public')
|
||||
...config.get('xpack.security.public'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates authentication provider based on the provider key from config.
|
||||
* @param providerType Provider type key.
|
||||
* @param options Options to pass to provider's constructor.
|
||||
*/
|
||||
function instantiateProvider(providerType: string, options: AuthenticationProviderOptions) {
|
||||
const ProviderClassName = providerMap.get(providerType);
|
||||
if (!ProviderClassName) {
|
||||
throw new Error(`Unsupported authentication provider name: ${providerType}.`);
|
||||
}
|
||||
|
||||
return new ProviderClassName(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticator is responsible for authentication of the request using chain of
|
||||
* authentication providers. The chain is essentially a prioritized list of configured
|
||||
|
@ -64,47 +90,24 @@ function getProviderOptions(server) {
|
|||
* will be returned.
|
||||
*/
|
||||
class Authenticator {
|
||||
/**
|
||||
* HapiJS server instance.
|
||||
* @type {Hapi.Server}
|
||||
* @private
|
||||
*/
|
||||
_server = null;
|
||||
|
||||
/**
|
||||
* Service that gathers all `scopes` for particular user.
|
||||
* @type {AuthScopeService}
|
||||
* @private
|
||||
*/
|
||||
_authScope = null;
|
||||
|
||||
/**
|
||||
* List of configured and instantiated authentication providers.
|
||||
* @type {Map.<string, Object>}
|
||||
* @private
|
||||
*/
|
||||
_providers = null;
|
||||
|
||||
/**
|
||||
* Session class instance.
|
||||
* @type {Session}
|
||||
* @private
|
||||
*/
|
||||
_session = null;
|
||||
private readonly providers: Map<string, BaseAuthenticationProvider>;
|
||||
|
||||
/**
|
||||
* Instantiates Authenticator and bootstrap configured providers.
|
||||
* @param {Hapi.Server} server HapiJS Server instance.
|
||||
* @param {AuthScopeService} authScope AuthScopeService instance.
|
||||
* @param {Session} session Session instance.
|
||||
* @param server Server instance.
|
||||
* @param authScope AuthScopeService instance.
|
||||
* @param session Session instance.
|
||||
*/
|
||||
constructor(server, authScope, session) {
|
||||
this._server = server;
|
||||
this._authScope = authScope;
|
||||
this._session = session;
|
||||
|
||||
const config = this._server.config();
|
||||
const authProviders = config.get('xpack.security.authProviders');
|
||||
constructor(
|
||||
private readonly server: Legacy.Server,
|
||||
private readonly authScope: AuthScopeService,
|
||||
private readonly session: Session
|
||||
) {
|
||||
const config = this.server.config();
|
||||
const authProviders = config.get<string[]>('xpack.security.authProviders');
|
||||
if (authProviders.length === 0) {
|
||||
throw new Error(
|
||||
'No authentication provider is configured. Verify `xpack.security.authProviders` config value.'
|
||||
|
@ -113,50 +116,55 @@ class Authenticator {
|
|||
|
||||
const providerOptions = Object.freeze(getProviderOptions(server));
|
||||
|
||||
this._providers = new Map(
|
||||
this.providers = new Map(
|
||||
authProviders.map(
|
||||
(providerType) => [providerType, this._instantiateProvider(providerType, providerOptions)]
|
||||
providerType =>
|
||||
[providerType, instantiateProvider(providerType, providerOptions)] as [
|
||||
string,
|
||||
BaseAuthenticationProvider
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs request authentication using configured chain of authentication providers.
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @returns {Promise.<AuthenticationResult>}.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
async authenticate(request) {
|
||||
async authenticate(request: Legacy.Request) {
|
||||
assertRequest(request);
|
||||
|
||||
const isSystemApiRequest = this._server.plugins.kibana.systemApi.isSystemApiRequest(request);
|
||||
const existingSession = await this._getSessionValue(request);
|
||||
const isSystemApiRequest = this.server.plugins.kibana.systemApi.isSystemApiRequest(request);
|
||||
const existingSession = await this.getSessionValue(request);
|
||||
|
||||
let authenticationResult;
|
||||
for (const [providerType, provider] of this._providerIterator(existingSession)) {
|
||||
for (const [providerType, provider] of this.providerIterator(existingSession)) {
|
||||
// Check if current session has been set by this provider.
|
||||
const ownsSession = existingSession && existingSession.provider === providerType;
|
||||
|
||||
authenticationResult = await provider.authenticate(
|
||||
request,
|
||||
ownsSession ? existingSession.state : null
|
||||
ownsSession ? existingSession!.state : null
|
||||
);
|
||||
|
||||
if (ownsSession || authenticationResult.shouldUpdateState()) {
|
||||
// If authentication succeeds or requires redirect we should automatically extend existing user session,
|
||||
// unless authentication has been triggered by a system API request. In case provider explicitly returns new
|
||||
// state we should store it in the session regardless of whether it's a system API request or not.
|
||||
const sessionCanBeUpdated = (authenticationResult.succeeded() || authenticationResult.redirected())
|
||||
&& (authenticationResult.shouldUpdateState() || !isSystemApiRequest);
|
||||
const sessionCanBeUpdated =
|
||||
(authenticationResult.succeeded() || authenticationResult.redirected()) &&
|
||||
(authenticationResult.shouldUpdateState() || !isSystemApiRequest);
|
||||
|
||||
// If provider owned the session, but failed to authenticate anyway, that likely means that
|
||||
// session is not valid and we should clear it. Also provider can specifically ask to clear
|
||||
// session by setting it to `null` even if authentication attempt didn't fail.
|
||||
if (authenticationResult.shouldClearState() || (
|
||||
authenticationResult.failed() && getErrorStatusCode(authenticationResult.error) === 401)
|
||||
if (
|
||||
authenticationResult.shouldClearState() ||
|
||||
(authenticationResult.failed() && getErrorStatusCode(authenticationResult.error) === 401)
|
||||
) {
|
||||
await this._session.clear(request);
|
||||
await this.session.clear(request);
|
||||
} else if (sessionCanBeUpdated) {
|
||||
await this._session.set(
|
||||
await this.session.set(
|
||||
request,
|
||||
authenticationResult.shouldUpdateState()
|
||||
? { state: authenticationResult.state, provider: providerType }
|
||||
|
@ -173,8 +181,8 @@ class Authenticator {
|
|||
return AuthenticationResult.succeeded({
|
||||
...authenticationResult.user,
|
||||
// Complement user returned from the provider with scopes.
|
||||
scope: await this._authScope.getForRequestAndUser(request, authenticationResult.user)
|
||||
});
|
||||
scope: await this.authScope.getForRequestAndUser(request, authenticationResult.user!),
|
||||
} as any);
|
||||
} else if (authenticationResult.redirected()) {
|
||||
return authenticationResult;
|
||||
}
|
||||
|
@ -185,17 +193,16 @@ class Authenticator {
|
|||
|
||||
/**
|
||||
* Deauthenticates current request.
|
||||
* @param {Hapi.Request} request HapiJS request instance.
|
||||
* @returns {Promise.<DeauthenticationResult>}
|
||||
* @param request Request instance.
|
||||
*/
|
||||
async deauthenticate(request) {
|
||||
async deauthenticate(request: Legacy.Request) {
|
||||
assertRequest(request);
|
||||
|
||||
const sessionValue = await this._getSessionValue(request);
|
||||
const sessionValue = await this.getSessionValue(request);
|
||||
if (sessionValue) {
|
||||
await this._session.clear(request);
|
||||
await this.session.clear(request);
|
||||
|
||||
return this._providers.get(sessionValue.provider).deauthenticate(request, sessionValue.state);
|
||||
return this.providers.get(sessionValue.provider)!.deauthenticate(request, sessionValue.state);
|
||||
}
|
||||
|
||||
// Normally when there is no active session in Kibana, `deauthenticate` method shouldn't do anything
|
||||
|
@ -204,44 +211,29 @@ class Authenticator {
|
|||
// SP associated with the current user session to do the logout. So if Kibana (without active session)
|
||||
// receives such a request it shouldn't redirect user to the home page, but rather redirect back to IdP
|
||||
// with correct logout response and only Elasticsearch knows how to do that.
|
||||
if (request.query.SAMLRequest && this._providers.has('saml')) {
|
||||
return this._providers.get('saml').deauthenticate(request);
|
||||
if ((request.query as Record<string, string>).SAMLRequest && this.providers.has('saml')) {
|
||||
return this.providers.get('saml')!.deauthenticate(request);
|
||||
}
|
||||
|
||||
return DeauthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates authentication provider based on the provider key from config.
|
||||
* @param {string} providerType Provider type key.
|
||||
* @param {Object} options Options to pass to provider's constructor.
|
||||
* @returns {Object} Authentication provider instance.
|
||||
* @private
|
||||
*/
|
||||
_instantiateProvider(providerType, options) {
|
||||
const ProviderClassName = providerMap.get(providerType);
|
||||
if (!ProviderClassName) {
|
||||
throw new Error(`Unsupported authentication provider name: ${providerType}.`);
|
||||
}
|
||||
|
||||
return new ProviderClassName(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns provider iterator where providers are sorted in the order of priority (based on the session ownership).
|
||||
* @param {Object} sessionValue Current session value.
|
||||
* @returns {Iterator.<Object>}
|
||||
* @param sessionValue Current session value.
|
||||
*/
|
||||
*_providerIterator(sessionValue) {
|
||||
*providerIterator(
|
||||
sessionValue: ProviderSession | null
|
||||
): IterableIterator<[string, BaseAuthenticationProvider]> {
|
||||
// If there is no session to predict which provider to use first, let's use the order
|
||||
// providers are configured in. Otherwise return provider that owns session first, and only then the rest
|
||||
// of providers.
|
||||
if (!sessionValue) {
|
||||
yield* this._providers;
|
||||
yield* this.providers;
|
||||
} else {
|
||||
yield [sessionValue.provider, this._providers.get(sessionValue.provider)];
|
||||
yield [sessionValue.provider, this.providers.get(sessionValue.provider)!];
|
||||
|
||||
for (const [providerType, provider] of this._providers) {
|
||||
for (const [providerType, provider] of this.providers) {
|
||||
if (providerType !== sessionValue.provider) {
|
||||
yield [providerType, provider];
|
||||
}
|
||||
|
@ -252,18 +244,16 @@ class Authenticator {
|
|||
/**
|
||||
* Extracts session value for the specified request. Under the hood it can
|
||||
* clear session if it belongs to the provider that is not available.
|
||||
* @param {Hapi.Request} request Request to extract session value for.
|
||||
* @returns {Promise.<Object|null>}
|
||||
* @private
|
||||
* @param request Request to extract session value for.
|
||||
*/
|
||||
async _getSessionValue(request) {
|
||||
let sessionValue = await this._session.get(request);
|
||||
private async getSessionValue(request: Legacy.Request) {
|
||||
let sessionValue = await this.session.get<ProviderSession>(request);
|
||||
|
||||
// If for some reason we have a session stored for the provider that is not available
|
||||
// (e.g. when user was logged in with one provider, but then configuration has changed
|
||||
// and that provider is no longer available), then we should clear session entirely.
|
||||
if (sessionValue && !this._providers.has(sessionValue.provider)) {
|
||||
await this._session.clear(request);
|
||||
if (sessionValue && !this.providers.has(sessionValue.provider)) {
|
||||
await this.session.clear(request);
|
||||
sessionValue = null;
|
||||
}
|
||||
|
||||
|
@ -271,13 +261,13 @@ class Authenticator {
|
|||
}
|
||||
}
|
||||
|
||||
export async function initAuthenticator(server) {
|
||||
export async function initAuthenticator(server: Legacy.Server) {
|
||||
const session = await Session.create(server);
|
||||
const authScope = new AuthScopeService();
|
||||
const authenticator = new Authenticator(server, authScope, session);
|
||||
|
||||
const loginAttempts = new WeakMap();
|
||||
server.decorate('request', 'loginAttempt', function () {
|
||||
server.decorate('request', 'loginAttempt', function(this: Legacy.Request) {
|
||||
const request = this;
|
||||
if (!loginAttempts.has(request)) {
|
||||
loginAttempts.set(request, new LoginAttempt());
|
||||
|
@ -285,13 +275,17 @@ export async function initAuthenticator(server) {
|
|||
return loginAttempts.get(request);
|
||||
});
|
||||
|
||||
server.expose('authenticate', (request) => authenticator.authenticate(request));
|
||||
server.expose('deauthenticate', (request) => authenticator.deauthenticate(request));
|
||||
server.expose('registerAuthScopeGetter', (scopeExtender) => authScope.registerGetter(scopeExtender));
|
||||
server.expose('authenticate', (request: Legacy.Request) => authenticator.authenticate(request));
|
||||
server.expose('deauthenticate', (request: Legacy.Request) =>
|
||||
authenticator.deauthenticate(request)
|
||||
);
|
||||
server.expose('registerAuthScopeGetter', (scopeExtender: ScopesGetter) =>
|
||||
authScope.registerGetter(scopeExtender)
|
||||
);
|
||||
|
||||
server.expose('isAuthenticated', async (request) => {
|
||||
server.expose('isAuthenticated', async (request: Legacy.Request) => {
|
||||
try {
|
||||
await server.plugins.security.getUser(request);
|
||||
await server.plugins.security!.getUser(request);
|
||||
return true;
|
||||
} catch (err) {
|
||||
// Don't swallow server errors.
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 { AuthenticationResult } from './authentication_result';
|
||||
export { DeauthenticationResult } from './deauthentication_result';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { stub } from 'sinon';
|
||||
import { AuthenticationProviderOptions } from './base';
|
||||
|
||||
export function mockAuthenticationProviderOptions(
|
||||
providerOptions: Partial<AuthenticationProviderOptions> = {}
|
||||
) {
|
||||
return {
|
||||
hostname: 'test-hostname',
|
||||
port: 1234,
|
||||
protocol: 'test-protocol',
|
||||
client: { callWithRequest: stub(), callWithInternalUser: stub() },
|
||||
log: stub(),
|
||||
basePath: '/base-path',
|
||||
...providerOptions,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { Legacy } from 'kibana';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
|
||||
/**
|
||||
* Represents available provider options.
|
||||
*/
|
||||
export interface AuthenticationProviderOptions {
|
||||
protocol: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
basePath: string;
|
||||
client: Legacy.Plugins.elasticsearch.Cluster;
|
||||
log: (tags: string[], message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class that all authentication providers should extend.
|
||||
*/
|
||||
export abstract class BaseAuthenticationProvider {
|
||||
/**
|
||||
* Instantiates AuthenticationProvider.
|
||||
* @param options Provider options object.
|
||||
*/
|
||||
constructor(protected readonly options: Readonly<AuthenticationProviderOptions>) {}
|
||||
|
||||
/**
|
||||
* Performs request authentication.
|
||||
* @param request Request instance.
|
||||
* @param [state] Optional state object associated with the provider.
|
||||
*/
|
||||
abstract authenticate(request: Legacy.Request, state?: unknown): Promise<AuthenticationResult>;
|
||||
|
||||
/**
|
||||
* Invalidates user session associated with the request.
|
||||
* @param request Request instance.
|
||||
* @param [state] Optional state object associated with the provider that needs to be invalidated.
|
||||
*/
|
||||
abstract deauthenticate(
|
||||
request: Legacy.Request,
|
||||
state?: unknown
|
||||
): Promise<DeauthenticationResult>;
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
import sinon from 'sinon';
|
||||
import { requestFixture } from '../../__tests__/__fixtures__/request';
|
||||
import { LoginAttempt } from '../login_attempt';
|
||||
import { mockAuthenticationProviderOptions } from './base.mock';
|
||||
import { BasicAuthenticationProvider, BasicCredentials } from './basic';
|
||||
|
||||
function generateAuthorizationHeader(username: string, password: string) {
|
||||
|
@ -22,14 +23,9 @@ describe('BasicAuthenticationProvider', () => {
|
|||
let provider: BasicAuthenticationProvider;
|
||||
let callWithRequest: sinon.SinonStub;
|
||||
beforeEach(() => {
|
||||
callWithRequest = sinon.stub();
|
||||
provider = new BasicAuthenticationProvider({
|
||||
client: { callWithRequest } as any,
|
||||
log() {
|
||||
// no-op
|
||||
},
|
||||
basePath: '/base-path',
|
||||
});
|
||||
const providerOptions = mockAuthenticationProviderOptions();
|
||||
callWithRequest = providerOptions.client.callWithRequest as sinon.SinonStub;
|
||||
provider = new BasicAuthenticationProvider(providerOptions);
|
||||
});
|
||||
|
||||
it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => {
|
||||
|
@ -176,13 +172,7 @@ describe('BasicAuthenticationProvider', () => {
|
|||
describe('`deauthenticate` method', () => {
|
||||
let provider: BasicAuthenticationProvider;
|
||||
beforeEach(() => {
|
||||
provider = new BasicAuthenticationProvider({
|
||||
client: { callWithRequest: sinon.stub() } as any,
|
||||
log() {
|
||||
// no-op
|
||||
},
|
||||
basePath: '/base-path',
|
||||
});
|
||||
provider = new BasicAuthenticationProvider(mockAuthenticationProviderOptions());
|
||||
});
|
||||
|
||||
it('always redirects to the login page.', async () => {
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { Request } from 'hapi';
|
||||
import { Cluster } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { Legacy } from 'kibana';
|
||||
import { canRedirectRequest } from '../../can_redirect_request';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
import { LoginAttempt } from '../login_attempt';
|
||||
import { BaseAuthenticationProvider } from './base';
|
||||
|
||||
/**
|
||||
* Utility class that knows how to decorate request with proper Basic authentication headers.
|
||||
|
@ -20,11 +20,15 @@ export class BasicCredentials {
|
|||
/**
|
||||
* Takes provided `username` and `password`, transforms them into proper `Basic ***` authorization
|
||||
* header and decorates passed request with it.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param username User name.
|
||||
* @param password User password.
|
||||
*/
|
||||
public static decorateRequest<T extends Request>(request: T, username: string, password: string) {
|
||||
public static decorateRequest<T extends Legacy.Request>(
|
||||
request: T,
|
||||
username: string,
|
||||
password: string
|
||||
) {
|
||||
const typeOfRequest = typeof request;
|
||||
if (!request || typeOfRequest !== 'object') {
|
||||
throw new Error('Request should be a valid object.');
|
||||
|
@ -44,19 +48,10 @@ export class BasicCredentials {
|
|||
}
|
||||
}
|
||||
|
||||
type RequestWithLoginAttempt = Request & {
|
||||
type RequestWithLoginAttempt = Legacy.Request & {
|
||||
loginAttempt: () => LoginAttempt;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents available provider options.
|
||||
*/
|
||||
interface ProviderOptions {
|
||||
basePath: string;
|
||||
client: Cluster;
|
||||
log: (tags: string[], message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The state supported by the provider.
|
||||
*/
|
||||
|
@ -72,16 +67,10 @@ interface ProviderState {
|
|||
/**
|
||||
* Provider that supports request authentication via Basic HTTP Authentication.
|
||||
*/
|
||||
export class BasicAuthenticationProvider {
|
||||
/**
|
||||
* Instantiates BasicAuthenticationProvider.
|
||||
* @param options Provider options object.
|
||||
*/
|
||||
constructor(private readonly options: ProviderOptions) {}
|
||||
|
||||
export class BasicAuthenticationProvider extends BaseAuthenticationProvider {
|
||||
/**
|
||||
* Performs request authentication using Basic HTTP Authentication.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param [state] Optional state object associated with the provider.
|
||||
*/
|
||||
public async authenticate(request: RequestWithLoginAttempt, state?: ProviderState | null) {
|
||||
|
@ -117,9 +106,9 @@ export class BasicAuthenticationProvider {
|
|||
|
||||
/**
|
||||
* Redirects user to the login page preserving query string parameters.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
public async deauthenticate(request: Request) {
|
||||
public async deauthenticate(request: Legacy.Request) {
|
||||
// Query string may contain the path where logout has been called or
|
||||
// logout reason that login page may need to know.
|
||||
return DeauthenticationResult.redirectTo(
|
||||
|
@ -130,7 +119,7 @@ export class BasicAuthenticationProvider {
|
|||
/**
|
||||
* Validates whether request contains a login payload and authenticates the
|
||||
* user if necessary.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
private async authenticateViaLoginAttempt(request: RequestWithLoginAttempt) {
|
||||
this.debug('Trying to authenticate via login attempt.');
|
||||
|
@ -162,9 +151,9 @@ export class BasicAuthenticationProvider {
|
|||
/**
|
||||
* Validates whether request contains `Basic ***` Authorization header and just passes it
|
||||
* forward to Elasticsearch backend.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
private async authenticateViaHeader(request: Request) {
|
||||
private async authenticateViaHeader(request: Legacy.Request) {
|
||||
this.debug('Trying to authenticate via header.');
|
||||
|
||||
const authorization = request.headers.authorization;
|
||||
|
@ -197,10 +186,10 @@ export class BasicAuthenticationProvider {
|
|||
/**
|
||||
* Tries to extract authorization header from the state and adds it to the request before
|
||||
* it's forwarded to Elasticsearch backend.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
private async authenticateViaState(request: Request, { authorization }: ProviderState) {
|
||||
private async authenticateViaState(request: Legacy.Request, { authorization }: ProviderState) {
|
||||
this.debug('Trying to authenticate via state.');
|
||||
|
||||
if (!authorization) {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { BaseAuthenticationProvider, AuthenticationProviderOptions } from './base';
|
||||
export { BasicAuthenticationProvider, BasicCredentials } from './basic';
|
||||
export { SAMLAuthenticationProvider } from './saml';
|
||||
export { TokenAuthenticationProvider } from './token';
|
|
@ -8,6 +8,7 @@ import Boom from 'boom';
|
|||
import sinon from 'sinon';
|
||||
|
||||
import { requestFixture } from '../../__tests__/__fixtures__/request';
|
||||
import { mockAuthenticationProviderOptions } from './base.mock';
|
||||
|
||||
import { SAMLAuthenticationProvider } from './saml';
|
||||
|
||||
|
@ -16,19 +17,11 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
let callWithRequest: sinon.SinonStub;
|
||||
let callWithInternalUser: sinon.SinonStub;
|
||||
beforeEach(() => {
|
||||
callWithRequest = sinon.stub();
|
||||
callWithInternalUser = sinon.stub();
|
||||
const providerOptions = mockAuthenticationProviderOptions({ basePath: '/test-base-path' });
|
||||
callWithRequest = providerOptions.client.callWithRequest as sinon.SinonStub;
|
||||
callWithInternalUser = providerOptions.client.callWithInternalUser as sinon.SinonStub;
|
||||
|
||||
provider = new SAMLAuthenticationProvider({
|
||||
client: { callWithRequest, callWithInternalUser } as any,
|
||||
log() {
|
||||
// no-op
|
||||
},
|
||||
protocol: 'test-protocol',
|
||||
hostname: 'test-hostname',
|
||||
port: 1234,
|
||||
basePath: '/test-base-path',
|
||||
});
|
||||
provider = new SAMLAuthenticationProvider(providerOptions);
|
||||
});
|
||||
|
||||
describe('`authenticate` method', () => {
|
||||
|
|
|
@ -5,25 +5,13 @@
|
|||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { Request } from 'hapi';
|
||||
import { Cluster } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { Legacy } from 'kibana';
|
||||
import { canRedirectRequest } from '../../can_redirect_request';
|
||||
import { getErrorStatusCode } from '../../errors';
|
||||
import { AuthenticatedUser } from '../../../../common/model';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
|
||||
/**
|
||||
* Represents available provider options.
|
||||
*/
|
||||
interface ProviderOptions {
|
||||
protocol: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
basePath: string;
|
||||
client: Cluster;
|
||||
log: (tags: string[], message: string) => void;
|
||||
}
|
||||
import { BaseAuthenticationProvider } from './base';
|
||||
|
||||
/**
|
||||
* The state supported by the provider (for the SAML handshake or established session).
|
||||
|
@ -62,7 +50,7 @@ interface SAMLRequestQuery {
|
|||
/**
|
||||
* Defines the shape of the request with a body containing SAML response.
|
||||
*/
|
||||
type RequestWithSAMLPayload = Request & {
|
||||
type RequestWithSAMLPayload = Legacy.Request & {
|
||||
payload: { SAMLResponse: string; RelayState?: string };
|
||||
};
|
||||
|
||||
|
@ -87,9 +75,11 @@ function isAccessTokenExpiredError(err?: any) {
|
|||
|
||||
/**
|
||||
* Checks whether request payload contains SAML response from IdP.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
function isRequestWithSAMLResponsePayload(request: Request): request is RequestWithSAMLPayload {
|
||||
function isRequestWithSAMLResponsePayload(
|
||||
request: Legacy.Request
|
||||
): request is RequestWithSAMLPayload {
|
||||
return request.payload != null && !!(request.payload as any).SAMLResponse;
|
||||
}
|
||||
|
||||
|
@ -104,19 +94,13 @@ function isSAMLRequestQuery(query: any): query is SAMLRequestQuery {
|
|||
/**
|
||||
* Provider that supports SAML request authentication.
|
||||
*/
|
||||
export class SAMLAuthenticationProvider {
|
||||
/**
|
||||
* Instantiates SAMLAuthenticationProvider.
|
||||
* @param options Options that may be needed by authentication provider.
|
||||
*/
|
||||
constructor(private readonly options: ProviderOptions) {}
|
||||
|
||||
export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
||||
/**
|
||||
* Performs SAML request authentication.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param [state] Optional state object associated with the provider.
|
||||
*/
|
||||
public async authenticate(request: Request, state?: ProviderState | null) {
|
||||
public async authenticate(request: Legacy.Request, state?: ProviderState | null) {
|
||||
this.debug(`Trying to authenticate user request to ${request.url.path}.`);
|
||||
|
||||
let {
|
||||
|
@ -160,10 +144,10 @@ export class SAMLAuthenticationProvider {
|
|||
|
||||
/**
|
||||
* Invalidates SAML access token if it exists.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
public async deauthenticate(request: Request, state?: ProviderState) {
|
||||
public async deauthenticate(request: Legacy.Request, state?: ProviderState) {
|
||||
this.debug(`Trying to deauthenticate user via ${request.url.path}.`);
|
||||
|
||||
if ((!state || !state.accessToken) && !isSAMLRequestQuery(request.query)) {
|
||||
|
@ -194,9 +178,9 @@ export class SAMLAuthenticationProvider {
|
|||
/**
|
||||
* Validates whether request contains `Bearer ***` Authorization header and just passes it
|
||||
* forward to Elasticsearch backend.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
private async authenticateViaHeader(request: Request) {
|
||||
private async authenticateViaHeader(request: Legacy.Request) {
|
||||
this.debug('Trying to authenticate via header.');
|
||||
|
||||
const authorization = request.headers.authorization;
|
||||
|
@ -237,7 +221,7 @@ export class SAMLAuthenticationProvider {
|
|||
* When login succeeds access token is stored in the state and user is redirected to the URL
|
||||
* that was requested before SAML handshake or to default Kibana location in case of IdP
|
||||
* initiated login.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param [state] Optional state object associated with the provider.
|
||||
*/
|
||||
private async authenticateViaPayload(
|
||||
|
@ -298,7 +282,7 @@ export class SAMLAuthenticationProvider {
|
|||
* The tokens are stored in the state and user is redirected to the default Kibana location, unless
|
||||
* we detect that user from existing session isn't the same as defined in SAML payload. In this case
|
||||
* we'll forward user to a page with the respective warning.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param existingState State existing user session is based on.
|
||||
* @param user User returned for the existing session.
|
||||
*/
|
||||
|
@ -368,10 +352,10 @@ export class SAMLAuthenticationProvider {
|
|||
/**
|
||||
* Tries to extract access token from state and adds it to the request before it's
|
||||
* forwarded to Elasticsearch backend.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
private async authenticateViaState(request: Request, { accessToken }: ProviderState) {
|
||||
private async authenticateViaState(request: Legacy.Request, { accessToken }: ProviderState) {
|
||||
this.debug('Trying to authenticate via state.');
|
||||
|
||||
if (!accessToken) {
|
||||
|
@ -404,10 +388,13 @@ export class SAMLAuthenticationProvider {
|
|||
* This method is only called when authentication via access token stored in the state failed because of expired
|
||||
* token. So we should use refresh token, that is also stored in the state, to extend expired access token and
|
||||
* authenticate user with it.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
private async authenticateViaRefreshToken(request: Request, { refreshToken }: ProviderState) {
|
||||
private async authenticateViaRefreshToken(
|
||||
request: Legacy.Request,
|
||||
{ refreshToken }: ProviderState
|
||||
) {
|
||||
this.debug('Trying to refresh access token.');
|
||||
|
||||
if (!refreshToken) {
|
||||
|
@ -480,9 +467,9 @@ export class SAMLAuthenticationProvider {
|
|||
|
||||
/**
|
||||
* Tries to start SAML handshake and eventually receive a token.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
private async authenticateViaHandshake(request: Request) {
|
||||
private async authenticateViaHandshake(request: Legacy.Request) {
|
||||
this.debug('Trying to initiate SAML handshake.');
|
||||
|
||||
// If client can't handle redirect response, we shouldn't initiate SAML handshake.
|
||||
|
@ -577,9 +564,9 @@ export class SAMLAuthenticationProvider {
|
|||
/**
|
||||
* Calls `saml/invalidate` with the `SAMLRequest` query string parameter received from the Identity
|
||||
* Provider and redirects user back to the Identity Provider if needed.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
private async performIdPInitiatedSingleLogout(request: Request) {
|
||||
private async performIdPInitiatedSingleLogout(request: Legacy.Request) {
|
||||
this.debug('Single logout has been initiated by the Identity Provider.');
|
||||
|
||||
// This operation should be performed on behalf of the user with a privilege that normal
|
||||
|
|
|
@ -8,6 +8,7 @@ import { errors } from 'elasticsearch';
|
|||
import sinon from 'sinon';
|
||||
import { requestFixture } from '../../__tests__/__fixtures__/request';
|
||||
import { LoginAttempt } from '../login_attempt';
|
||||
import { mockAuthenticationProviderOptions } from './base.mock';
|
||||
import { TokenAuthenticationProvider } from './token';
|
||||
|
||||
describe('TokenAuthenticationProvider', () => {
|
||||
|
@ -16,15 +17,11 @@ describe('TokenAuthenticationProvider', () => {
|
|||
let callWithRequest: sinon.SinonStub;
|
||||
let callWithInternalUser: sinon.SinonStub;
|
||||
beforeEach(() => {
|
||||
callWithRequest = sinon.stub();
|
||||
callWithInternalUser = sinon.stub();
|
||||
provider = new TokenAuthenticationProvider({
|
||||
client: { callWithRequest, callWithInternalUser },
|
||||
log() {
|
||||
// no-op
|
||||
},
|
||||
basePath: '/base-path',
|
||||
});
|
||||
const providerOptions = mockAuthenticationProviderOptions();
|
||||
callWithRequest = providerOptions.client.callWithRequest as sinon.SinonStub;
|
||||
callWithInternalUser = providerOptions.client.callWithInternalUser as sinon.SinonStub;
|
||||
|
||||
provider = new TokenAuthenticationProvider(providerOptions);
|
||||
});
|
||||
|
||||
it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => {
|
||||
|
@ -465,14 +462,10 @@ describe('TokenAuthenticationProvider', () => {
|
|||
let provider: TokenAuthenticationProvider;
|
||||
let callWithInternalUser: sinon.SinonStub;
|
||||
beforeEach(() => {
|
||||
callWithInternalUser = sinon.stub();
|
||||
provider = new TokenAuthenticationProvider({
|
||||
client: { callWithInternalUser } as any,
|
||||
log() {
|
||||
// no-op
|
||||
},
|
||||
basePath: '/base-path',
|
||||
});
|
||||
const providerOptions = mockAuthenticationProviderOptions();
|
||||
callWithInternalUser = providerOptions.client.callWithInternalUser as sinon.SinonStub;
|
||||
|
||||
provider = new TokenAuthenticationProvider(providerOptions);
|
||||
});
|
||||
|
||||
describe('`deauthenticate` method', () => {
|
||||
|
|
|
@ -4,22 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Request } from 'hapi';
|
||||
import { Cluster } from 'src/legacy/core_plugins/elasticsearch';
|
||||
import { Legacy } from 'kibana';
|
||||
import { canRedirectRequest } from '../../can_redirect_request';
|
||||
import { getErrorStatusCode } from '../../errors';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
import { LoginAttempt } from '../login_attempt';
|
||||
|
||||
/**
|
||||
* Represents available provider options.
|
||||
*/
|
||||
interface ProviderOptions {
|
||||
basePath: string;
|
||||
client: Cluster;
|
||||
log: (tags: string[], message: string) => void;
|
||||
}
|
||||
import { BaseAuthenticationProvider } from './base';
|
||||
|
||||
/**
|
||||
* The state supported by the provider.
|
||||
|
@ -38,7 +29,7 @@ interface ProviderState {
|
|||
refreshToken?: string;
|
||||
}
|
||||
|
||||
type RequestWithLoginAttempt = Request & {
|
||||
type RequestWithLoginAttempt = Legacy.Request & {
|
||||
loginAttempt: () => LoginAttempt;
|
||||
};
|
||||
|
||||
|
@ -64,16 +55,10 @@ function isAccessTokenExpiredError(err?: any) {
|
|||
/**
|
||||
* Provider that supports token-based request authentication.
|
||||
*/
|
||||
export class TokenAuthenticationProvider {
|
||||
/**
|
||||
* Instantiates TokenAuthenticationProvider.
|
||||
* @param options Options that may be needed by authentication provider.
|
||||
*/
|
||||
constructor(private readonly options: ProviderOptions) {}
|
||||
|
||||
export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
|
||||
/**
|
||||
* Performs token-based request authentication
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param [state] Optional state object associated with the provider.
|
||||
*/
|
||||
public async authenticate(request: RequestWithLoginAttempt, state?: ProviderState | null) {
|
||||
|
@ -113,10 +98,10 @@ export class TokenAuthenticationProvider {
|
|||
|
||||
/**
|
||||
* Redirects user to the login page preserving query string parameters.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
public async deauthenticate(request: Request, state?: ProviderState | null) {
|
||||
public async deauthenticate(request: Legacy.Request, state?: ProviderState | null) {
|
||||
this.debug(`Trying to deauthenticate user via ${request.url.path}.`);
|
||||
|
||||
if (!state || !state.accessToken || !state.refreshToken) {
|
||||
|
@ -173,9 +158,9 @@ export class TokenAuthenticationProvider {
|
|||
/**
|
||||
* Validates whether request contains `Bearer ***` Authorization header and just passes it
|
||||
* forward to Elasticsearch backend.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
private async authenticateViaHeader(request: Request) {
|
||||
private async authenticateViaHeader(request: Legacy.Request) {
|
||||
this.debug('Trying to authenticate via header.');
|
||||
|
||||
const authorization = request.headers.authorization;
|
||||
|
@ -207,7 +192,7 @@ export class TokenAuthenticationProvider {
|
|||
/**
|
||||
* Validates whether request contains a login payload and authenticates the
|
||||
* user if necessary.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
private async authenticateViaLoginAttempt(request: RequestWithLoginAttempt) {
|
||||
this.debug('Trying to authenticate via login attempt.');
|
||||
|
@ -264,10 +249,10 @@ export class TokenAuthenticationProvider {
|
|||
/**
|
||||
* Tries to extract authorization header from the state and adds it to the request before
|
||||
* it's forwarded to Elasticsearch backend.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
private async authenticateViaState(request: Request, { accessToken }: ProviderState) {
|
||||
private async authenticateViaState(request: Legacy.Request, { accessToken }: ProviderState) {
|
||||
this.debug('Trying to authenticate via state.');
|
||||
|
||||
if (!accessToken) {
|
||||
|
@ -300,10 +285,13 @@ export class TokenAuthenticationProvider {
|
|||
* This method is only called when authentication via access token stored in the state failed because of expired
|
||||
* token. So we should use refresh token, that is also stored in the state, to extend expired access token and
|
||||
* authenticate user with it.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param state State value previously stored by the provider.
|
||||
*/
|
||||
private async authenticateViaRefreshToken(request: Request, { refreshToken }: ProviderState) {
|
||||
private async authenticateViaRefreshToken(
|
||||
request: Legacy.Request,
|
||||
{ refreshToken }: ProviderState
|
||||
) {
|
||||
this.debug('Trying to refresh access token.');
|
||||
|
||||
if (!refreshToken) {
|
||||
|
@ -367,9 +355,9 @@ export class TokenAuthenticationProvider {
|
|||
|
||||
/**
|
||||
* Constructs login page URL using current url path as `next` query string parameter.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
private getLoginPageURL(request: Request) {
|
||||
private getLoginPageURL(request: Legacy.Request) {
|
||||
const nextURL = encodeURIComponent(`${request.getBasePath()}${request.url.path}`);
|
||||
return `${this.options.basePath}/login?next=${nextURL}`;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Request } from 'hapi';
|
||||
import hapiAuthCookie from 'hapi-auth-cookie';
|
||||
import { Legacy } from 'kibana';
|
||||
|
||||
|
@ -29,7 +28,7 @@ interface InternalSession {
|
|||
expires: number | null;
|
||||
}
|
||||
|
||||
function assertRequest(request: Request) {
|
||||
function assertRequest(request: Legacy.Request) {
|
||||
if (!request || typeof request !== 'object') {
|
||||
throw new Error(`Request should be a valid object, was [${typeof request}].`);
|
||||
}
|
||||
|
@ -47,7 +46,7 @@ export class Session {
|
|||
/**
|
||||
* Instantiates Session. Constructor is not supposed to be used directly. To make sure that all
|
||||
* `Session` dependencies/plugins are properly initialized one should use static `Session.create` instead.
|
||||
* @param server HapiJS Server instance.
|
||||
* @param server Server instance.
|
||||
*/
|
||||
constructor(private readonly server: Legacy.Server) {
|
||||
this.ttl = this.server.config().get<number | null>('xpack.security.sessionTimeout');
|
||||
|
@ -55,9 +54,9 @@ export class Session {
|
|||
|
||||
/**
|
||||
* Retrieves session value from the session storage (e.g. cookie).
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
async get(request: Request) {
|
||||
async get<T>(request: Legacy.Request) {
|
||||
assertRequest(request);
|
||||
|
||||
try {
|
||||
|
@ -65,12 +64,12 @@ export class Session {
|
|||
|
||||
// If it's not an array, just return the session value
|
||||
if (!Array.isArray(session)) {
|
||||
return session.value;
|
||||
return session.value as T;
|
||||
}
|
||||
|
||||
// If we have an array with one value, we're good also
|
||||
if (session.length === 1) {
|
||||
return session[0].value;
|
||||
return session[0].value as T;
|
||||
}
|
||||
|
||||
// Otherwise, we have more than one and won't be authing the user because we don't
|
||||
|
@ -88,10 +87,10 @@ export class Session {
|
|||
|
||||
/**
|
||||
* Puts current session value into the session storage.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param value Any object that will be associated with the request.
|
||||
*/
|
||||
async set(request: Request, value: unknown) {
|
||||
async set(request: Legacy.Request, value: unknown) {
|
||||
assertRequest(request);
|
||||
|
||||
request.cookieAuth.set({
|
||||
|
@ -102,9 +101,9 @@ export class Session {
|
|||
|
||||
/**
|
||||
* Clears current session.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
*/
|
||||
async clear(request: Request) {
|
||||
async clear(request: Legacy.Request) {
|
||||
assertRequest(request);
|
||||
|
||||
request.cookieAuth.clear();
|
||||
|
@ -112,7 +111,7 @@ export class Session {
|
|||
|
||||
/**
|
||||
* Prepares and creates a session instance.
|
||||
* @param server HapiJS Server instance.
|
||||
* @param server Server instance.
|
||||
*/
|
||||
static async create(server: Legacy.Server) {
|
||||
// Register HAPI plugin that manages session cookie and delegate parsing of the session cookie to it.
|
||||
|
@ -151,10 +150,10 @@ export class Session {
|
|||
/**
|
||||
* Validation function that is passed to hapi-auth-cookie plugin and is responsible
|
||||
* only for cookie expiration time validation.
|
||||
* @param request HapiJS request instance.
|
||||
* @param request Request instance.
|
||||
* @param session Session value object retrieved from cookie.
|
||||
*/
|
||||
private static validateCookie(request: Request, session: InternalSession) {
|
||||
private static validateCookie(request: Legacy.Request, session: InternalSession) {
|
||||
if (session.expires && session.expires < Date.now()) {
|
||||
return { valid: false };
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
export { Actions } from './actions';
|
||||
// @ts-ignore
|
||||
export { createAuthorizationService } from './service';
|
||||
export { disableUICapabilitesFactory } from './disable_ui_capabilities';
|
||||
export { initAPIAuthorization } from './api_authorization';
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { requestFixture } from '../__tests__/__fixtures__/request';
|
||||
import { authorizationModeFactory } from './mode';
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
mockPrivilegesFactory,
|
||||
} from './service.test.mocks';
|
||||
|
||||
// @ts-ignore
|
||||
import { getClient } from '../../../../../server/lib/get_client_shield';
|
||||
import { actionsFactory } from './actions';
|
||||
import { checkPrivilegesWithRequestFactory } from './check_privileges';
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { Server } from 'hapi';
|
||||
|
||||
// @ts-ignore
|
||||
import { getClient } from '../../../../../server/lib/get_client_shield';
|
||||
import { SpacesPlugin } from '../../../../spaces/types';
|
||||
import { XPackFeature, XPackMainPlugin } from '../../../../xpack_main/xpack_main';
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Request } from 'hapi';
|
||||
// @ts-ignore
|
||||
import { Legacy } from 'kibana';
|
||||
import { getClient } from '../../../../server/lib/get_client_shield';
|
||||
|
||||
export function getUserProvider(server: any) {
|
||||
const callWithRequest = getClient(server).callWithRequest;
|
||||
|
||||
server.expose('getUser', async (request: Request) => {
|
||||
server.expose('getUser', async (request: Legacy.Request) => {
|
||||
const xpackInfo = server.plugins.xpack_main.info;
|
||||
if (xpackInfo && xpackInfo.isAvailable() && !xpackInfo.feature('security').isEnabled()) {
|
||||
return Promise.resolve(null);
|
||||
|
|
|
@ -4,24 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import Boom from 'boom';
|
||||
import { PluginProperties, Server } from 'hapi';
|
||||
import { Server } from 'hapi';
|
||||
import { RawKibanaPrivileges } from '../../../../../common/model';
|
||||
import { initGetPrivilegesApi } from './get';
|
||||
|
||||
interface KibanaPluginProperties extends PluginProperties {
|
||||
security: {
|
||||
authorization: {
|
||||
privileges: {
|
||||
get: () => RawKibanaPrivileges;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface KibanaServer extends Server {
|
||||
plugins: KibanaPluginProperties;
|
||||
}
|
||||
|
||||
const createRawKibanaPrivileges: () => RawKibanaPrivileges = () => {
|
||||
return {
|
||||
features: {
|
||||
|
@ -48,7 +34,7 @@ const createRawKibanaPrivileges: () => RawKibanaPrivileges = () => {
|
|||
};
|
||||
|
||||
const createMockServer = () => {
|
||||
const mockServer: KibanaServer = new Server({ debug: false, port: 8080 }) as KibanaServer;
|
||||
const mockServer = new Server({ debug: false, port: 8080 });
|
||||
|
||||
mockServer.plugins.security = {
|
||||
authorization: {
|
||||
|
@ -58,7 +44,7 @@ const createMockServer = () => {
|
|||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
} as any;
|
||||
return mockServer;
|
||||
};
|
||||
|
||||
|
|
|
@ -8,14 +8,13 @@ jest.mock('../../../../server/lib/get_client_shield', () => ({
|
|||
}));
|
||||
|
||||
import Boom from 'boom';
|
||||
// @ts-ignore
|
||||
import { getClient } from '../../../../server/lib/get_client_shield';
|
||||
import { createDefaultSpace } from './create_default_space';
|
||||
|
||||
let mockCallWithRequest;
|
||||
beforeEach(() => {
|
||||
mockCallWithRequest = jest.fn();
|
||||
getClient.mockReturnValue({
|
||||
(getClient as jest.Mock).mockReturnValue({
|
||||
callWithRequest: mockCallWithRequest,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
// @ts-ignore
|
||||
import { getClient } from '../../../../server/lib/get_client_shield';
|
||||
import { DEFAULT_SPACE_ID } from '../../common/constants';
|
||||
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
*/
|
||||
|
||||
import { once } from 'lodash';
|
||||
import { Legacy } from 'kibana';
|
||||
// @ts-ignore
|
||||
import esShield from './esjs_shield_plugin';
|
||||
|
||||
export const getClient = once((server) => {
|
||||
export const getClient = once((server: Legacy.Server) => {
|
||||
return server.plugins.elasticsearch.createCluster('security', { plugins: [esShield] });
|
||||
});
|
2
x-pack/typings/hapi.d.ts
vendored
2
x-pack/typings/hapi.d.ts
vendored
|
@ -7,11 +7,13 @@
|
|||
import 'hapi';
|
||||
|
||||
import { CloudPlugin } from '../plugins/cloud';
|
||||
import { SecurityPlugin } from '../plugins/security';
|
||||
import { XPackMainPlugin } from '../plugins/xpack_main/xpack_main';
|
||||
|
||||
declare module 'hapi' {
|
||||
interface PluginProperties {
|
||||
cloud?: CloudPlugin;
|
||||
xpack_main: XPackMainPlugin;
|
||||
security?: SecurityPlugin;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue