Expose "is TLS enabled" flag for Kibana HTTP Server (#40336)

* expose it TLS enabled on Http server

* setup contract is under one section in tests

* regenerate docs

* isTLSEnabled --> isTlsEnabled
This commit is contained in:
Mikhail Shustov 2019-07-09 19:18:25 +02:00 committed by GitHub
parent 0ff97af37f
commit ff5b7a8df4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 337 additions and 311 deletions

View file

@ -13,5 +13,6 @@ http: {
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
createNewServer: HttpServiceSetup['createNewServer'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
};
```

View file

@ -17,5 +17,5 @@ export interface CoreSetup
| Property | Type | Description |
| --- | --- | --- |
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>{</code><br/><code> adminClient$: Observable&lt;ClusterClient&gt;;</code><br/><code> dataClient$: Observable&lt;ClusterClient&gt;;</code><br/><code> }</code> | |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>{</code><br/><code> registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];</code><br/><code> registerAuth: HttpServiceSetup['registerAuth'];</code><br/><code> registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];</code><br/><code> basePath: HttpServiceSetup['basePath'];</code><br/><code> createNewServer: HttpServiceSetup['createNewServer'];</code><br/><code> }</code> | |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>{</code><br/><code> registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];</code><br/><code> registerAuth: HttpServiceSetup['registerAuth'];</code><br/><code> registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];</code><br/><code> basePath: HttpServiceSetup['basePath'];</code><br/><code> createNewServer: HttpServiceSetup['createNewServer'];</code><br/><code> isTlsEnabled: HttpServiceSetup['isTlsEnabled'];</code><br/><code> }</code> | |

View file

@ -33,18 +33,49 @@ import { HttpConfig, Router } from '.';
import { loggingServiceMock } from '../logging/logging_service.mock';
import { HttpServer } from './http_server';
const cookieOptions = {
name: 'sid',
encryptionKey: 'something_at_least_32_characters',
validate: () => true,
isSecure: false,
};
interface User {
id: string;
roles?: string[];
}
interface StorageData {
value: User;
expires: number;
}
const chance = new Chance();
let server: HttpServer;
let config: HttpConfig;
let configWithSSL: HttpConfig;
const logger = loggingServiceMock.create();
beforeEach(() => {
config = {
host: '127.0.0.1',
maxPayload: new ByteSizeValue(1024),
port: chance.integer({ min: 10000, max: 15000 }),
ssl: {},
ssl: { enabled: false },
} as HttpConfig;
configWithSSL = {
...config,
ssl: {
enabled: true,
certificate: '/certificate',
cipherSuites: ['cipherSuite'],
getSecureOptions: () => 0,
key: '/key',
redirectHttpFromPort: config.port + 1,
},
} as HttpConfig;
server = new HttpServer(logger, 'tests');
@ -527,30 +558,14 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => {
});
});
describe('with defined `redirectHttpFromPort`', () => {
let configWithSSL: HttpConfig;
test('with defined `redirectHttpFromPort`', async () => {
const router = new Router('/');
router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: 'value:/' }));
beforeEach(async () => {
configWithSSL = {
...config,
ssl: {
certificate: '/certificate',
cipherSuites: ['cipherSuite'],
enabled: true,
getSecureOptions: () => 0,
key: '/key',
redirectHttpFromPort: config.port + 1,
},
} as HttpConfig;
const { registerRouter } = await server.setup(configWithSSL);
registerRouter(router);
const router = new Router('/');
router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: 'value:/' }));
const { registerRouter } = await server.setup(configWithSSL);
registerRouter(router);
await server.start();
});
await server.start();
});
test('returns server and connection options on start', async () => {
@ -578,205 +593,6 @@ test('throws an error if starts without set up', async () => {
);
});
const cookieOptions = {
name: 'sid',
encryptionKey: 'something_at_least_32_characters',
validate: () => true,
isSecure: false,
};
interface User {
id: string;
roles?: string[];
}
interface StorageData {
value: User;
expires: number;
}
describe('#registerAuth', () => {
it('registers auth request interceptor only once', async () => {
const { registerAuth } = await server.setup(config);
const doRegister = () =>
registerAuth(() => null as any, {
encryptionKey: 'any_password',
} as any);
await doRegister();
expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered');
});
it('supports implementing custom authentication logic', async () => {
const router = new Router('');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const { sessionStorageFactory } = await registerAuth<StorageData>((req, t) => {
const user = { id: '42' };
const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.set({ value: user, expires: Date.now() + 1000 });
return t.authenticated({ state: user });
}, cookieOptions);
registerRouter(router);
await server.start();
const response = await supertest(innerServer.listener)
.get('/')
.expect(200, { content: 'ok' });
expect(response.header['set-cookie']).toBeDefined();
const cookies = response.header['set-cookie'];
expect(cookies).toHaveLength(1);
const sessionCookie = request.cookie(cookies[0]);
if (!sessionCookie) {
throw new Error('session cookie expected to be defined');
}
expect(sessionCookie).toBeDefined();
expect(sessionCookie.key).toBe('sid');
expect(sessionCookie.value).toBeDefined();
expect(sessionCookie.path).toBe('/');
expect(sessionCookie.httpOnly).toBe(true);
});
it('supports rejecting a request from an unauthenticated user', async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
registerRouter(router);
await registerAuth((req, t) => t.rejected(Boom.unauthorized()), cookieOptions);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(401);
});
it('supports redirecting', async () => {
const redirectTo = '/redirect-url';
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
registerRouter(router);
await registerAuth((req, t) => {
return t.redirected(redirectTo);
}, cookieOptions);
await server.start();
const response = await supertest(innerServer.listener)
.get('/')
.expect(302);
expect(response.header.location).toBe(redirectTo);
});
it(`doesn't expose internal error details`, async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
registerRouter(router);
await registerAuth((req, t) => {
throw new Error('sensitive info');
}, cookieOptions);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect({
statusCode: 500,
error: 'Internal Server Error',
message: 'An internal server error occurred',
});
});
it('allows manipulating cookies from route handler', async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const { sessionStorageFactory } = await registerAuth<StorageData>((req, t) => {
const user = { id: '42' };
const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.set({ value: user, expires: Date.now() + 1000 });
return t.authenticated();
}, cookieOptions);
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) => res.ok({ content: 'ok' }));
router.get({ path: '/with-cookie', validate: false }, (req, res) => {
const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.clear();
return res.ok({ content: 'ok' });
});
registerRouter(router);
await server.start();
const responseToSetCookie = await supertest(innerServer.listener)
.get('/')
.expect(200);
expect(responseToSetCookie.header['set-cookie']).toBeDefined();
const responseToResetCookie = await supertest(innerServer.listener)
.get('/with-cookie')
.expect(200);
expect(responseToResetCookie.header['set-cookie']).toEqual([
'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/',
]);
});
it.skip('is the only place with access to the authorization header', async () => {
const token = 'Basic: user:password';
const {
registerAuth,
registerOnPreAuth,
registerOnPostAuth,
registerRouter,
server: innerServer,
} = await server.setup(config);
let fromRegisterOnPreAuth;
await registerOnPreAuth((req, t) => {
fromRegisterOnPreAuth = req.headers.authorization;
return t.next();
});
let fromRegisterAuth;
await registerAuth((req, t) => {
fromRegisterAuth = req.headers.authorization;
return t.authenticated();
}, cookieOptions);
let fromRegisterOnPostAuth;
await registerOnPostAuth((req, t) => {
fromRegisterOnPostAuth = req.headers.authorization;
return t.next();
});
let fromRouteHandler;
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) => {
fromRouteHandler = req.headers.authorization;
return res.ok({ content: 'ok' });
});
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.get('/')
.set('Authorization', token)
.expect(200);
expect(fromRegisterOnPreAuth).toEqual({});
expect(fromRegisterAuth).toEqual({ authorization: token });
expect(fromRegisterOnPostAuth).toEqual({});
expect(fromRouteHandler).toEqual({});
});
});
test('enables auth for a route by default if registerAuth has been called', async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
@ -878,105 +694,308 @@ test('exposes route details of incoming request to a route handler', async () =>
});
});
describe('#auth.isAuthenticated()', () => {
it('returns true if has been authorized', async () => {
const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(config);
describe('setup contract', () => {
describe('#registerAuth', () => {
it('registers auth request interceptor only once', async () => {
const { registerAuth } = await server.setup(config);
const doRegister = () =>
registerAuth(() => null as any, {
encryptionKey: 'any_password',
} as any);
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) =>
res.ok({ isAuthenticated: auth.isAuthenticated(req) })
);
registerRouter(router);
await doRegister();
expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered');
});
await registerAuth((req, t) => t.authenticated(), cookieOptions);
it('supports implementing custom authentication logic', async () => {
const router = new Router('');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { isAuthenticated: true });
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const { sessionStorageFactory } = await registerAuth<StorageData>((req, t) => {
const user = { id: '42' };
const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.set({ value: user, expires: Date.now() + 1000 });
return t.authenticated({ state: user });
}, cookieOptions);
registerRouter(router);
await server.start();
const response = await supertest(innerServer.listener)
.get('/')
.expect(200, { content: 'ok' });
expect(response.header['set-cookie']).toBeDefined();
const cookies = response.header['set-cookie'];
expect(cookies).toHaveLength(1);
const sessionCookie = request.cookie(cookies[0]);
if (!sessionCookie) {
throw new Error('session cookie expected to be defined');
}
expect(sessionCookie).toBeDefined();
expect(sessionCookie.key).toBe('sid');
expect(sessionCookie.value).toBeDefined();
expect(sessionCookie.path).toBe('/');
expect(sessionCookie.httpOnly).toBe(true);
});
it('supports rejecting a request from an unauthenticated user', async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
registerRouter(router);
await registerAuth((req, t) => t.rejected(Boom.unauthorized()), cookieOptions);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(401);
});
it('supports redirecting', async () => {
const redirectTo = '/redirect-url';
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
registerRouter(router);
await registerAuth((req, t) => {
return t.redirected(redirectTo);
}, cookieOptions);
await server.start();
const response = await supertest(innerServer.listener)
.get('/')
.expect(302);
expect(response.header.location).toBe(redirectTo);
});
it(`doesn't expose internal error details`, async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('');
router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
registerRouter(router);
await registerAuth((req, t) => {
throw new Error('sensitive info');
}, cookieOptions);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect({
statusCode: 500,
error: 'Internal Server Error',
message: 'An internal server error occurred',
});
});
it('allows manipulating cookies from route handler', async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const { sessionStorageFactory } = await registerAuth<StorageData>((req, t) => {
const user = { id: '42' };
const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.set({ value: user, expires: Date.now() + 1000 });
return t.authenticated();
}, cookieOptions);
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) => res.ok({ content: 'ok' }));
router.get({ path: '/with-cookie', validate: false }, (req, res) => {
const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.clear();
return res.ok({ content: 'ok' });
});
registerRouter(router);
await server.start();
const responseToSetCookie = await supertest(innerServer.listener)
.get('/')
.expect(200);
expect(responseToSetCookie.header['set-cookie']).toBeDefined();
const responseToResetCookie = await supertest(innerServer.listener)
.get('/with-cookie')
.expect(200);
expect(responseToResetCookie.header['set-cookie']).toEqual([
'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/',
]);
});
it.skip('is the only place with access to the authorization header', async () => {
const token = 'Basic: user:password';
const {
registerAuth,
registerOnPreAuth,
registerOnPostAuth,
registerRouter,
server: innerServer,
} = await server.setup(config);
let fromRegisterOnPreAuth;
await registerOnPreAuth((req, t) => {
fromRegisterOnPreAuth = req.headers.authorization;
return t.next();
});
let fromRegisterAuth;
await registerAuth((req, t) => {
fromRegisterAuth = req.headers.authorization;
return t.authenticated();
}, cookieOptions);
let fromRegisterOnPostAuth;
await registerOnPostAuth((req, t) => {
fromRegisterOnPostAuth = req.headers.authorization;
return t.next();
});
let fromRouteHandler;
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) => {
fromRouteHandler = req.headers.authorization;
return res.ok({ content: 'ok' });
});
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.get('/')
.set('Authorization', token)
.expect(200);
expect(fromRegisterOnPreAuth).toEqual({});
expect(fromRegisterAuth).toEqual({ authorization: token });
expect(fromRegisterOnPostAuth).toEqual({});
expect(fromRouteHandler).toEqual({});
});
});
it('returns false if has not been authorized', async () => {
const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(config);
describe('#auth.isAuthenticated()', () => {
it('returns true if has been authorized', async () => {
const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(
config
);
const router = new Router('');
router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) =>
res.ok({ isAuthenticated: auth.isAuthenticated(req) })
);
registerRouter(router);
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) =>
res.ok({ isAuthenticated: auth.isAuthenticated(req) })
);
registerRouter(router);
await registerAuth((req, t) => t.authenticated(), cookieOptions);
await registerAuth((req, t) => t.authenticated(), cookieOptions);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { isAuthenticated: false });
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { isAuthenticated: true });
});
it('returns false if has not been authorized', async () => {
const { registerAuth, registerRouter, server: innerServer, auth } = await server.setup(
config
);
const router = new Router('');
router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) =>
res.ok({ isAuthenticated: auth.isAuthenticated(req) })
);
registerRouter(router);
await registerAuth((req, t) => t.authenticated(), cookieOptions);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { isAuthenticated: false });
});
it('returns false if no authorization mechanism has been registered', async () => {
const { registerRouter, server: innerServer, auth } = await server.setup(config);
const router = new Router('');
router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) =>
res.ok({ isAuthenticated: auth.isAuthenticated(req) })
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { isAuthenticated: false });
});
});
it('returns false if no authorization mechanism has been registered', async () => {
const { registerRouter, server: innerServer, auth } = await server.setup(config);
describe('#auth.get()', () => {
it('returns authenticated status and allow associate auth state with request', async () => {
const user = { id: '42' };
const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(
config
);
const { sessionStorageFactory } = await registerAuth<StorageData>((req, t) => {
sessionStorageFactory.asScoped(req).set({ value: user, expires: Date.now() + 1000 });
return t.authenticated({ state: user });
}, cookieOptions);
const router = new Router('');
router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) =>
res.ok({ isAuthenticated: auth.isAuthenticated(req) })
);
registerRouter(router);
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) => res.ok(auth.get(req)));
registerRouter(router);
await server.start();
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { isAuthenticated: false });
});
});
describe('#auth.get()', () => {
it('returns authenticated status and allow associate auth state with request', async () => {
const user = { id: '42' };
const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config);
const { sessionStorageFactory } = await registerAuth<StorageData>((req, t) => {
sessionStorageFactory.asScoped(req).set({ value: user, expires: Date.now() + 1000 });
return t.authenticated({ state: user });
}, cookieOptions);
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) => res.ok(auth.get(req)));
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { state: user, status: 'authenticated' });
});
it('returns correct authentication unknown status', async () => {
const { registerRouter, server: innerServer, auth } = await server.setup(config);
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) => res.ok(auth.get(req)));
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { status: 'unknown' });
});
it('returns correct unauthenticated status', async () => {
const authenticate = jest.fn();
const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config);
await registerAuth(authenticate, cookieOptions);
const router = new Router('');
router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) =>
res.ok(auth.get(req))
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { status: 'unauthenticated' });
expect(authenticate).not.toHaveBeenCalled();
await supertest(innerServer.listener)
.get('/')
.expect(200, { state: user, status: 'authenticated' });
});
it('returns correct authentication unknown status', async () => {
const { registerRouter, server: innerServer, auth } = await server.setup(config);
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) => res.ok(auth.get(req)));
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { status: 'unknown' });
});
it('returns correct unauthenticated status', async () => {
const authenticate = jest.fn();
const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(
config
);
await registerAuth(authenticate, cookieOptions);
const router = new Router('');
router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) =>
res.ok(auth.get(req))
);
registerRouter(router);
await server.start();
await supertest(innerServer.listener)
.get('/')
.expect(200, { status: 'unauthenticated' });
expect(authenticate).not.toHaveBeenCalled();
});
});
describe('#isTlsEnabled', () => {
it('returns "true" if TLS enabled', async () => {
const { isTlsEnabled } = await server.setup(configWithSSL);
expect(isTlsEnabled).toBe(true);
});
it('returns "false" if TLS not enabled', async () => {
const { isTlsEnabled } = await server.setup(config);
expect(isTlsEnabled).toBe(false);
});
});
});

View file

@ -75,6 +75,7 @@ export interface HttpServerSetup {
isAuthenticated: AuthStateStorage['isAuthenticated'];
getAuthHeaders: AuthHeadersStorage['get'];
};
isTlsEnabled: boolean;
}
export class HttpServer {
@ -127,6 +128,7 @@ export class HttpServer {
isAuthenticated: this.authState.isAuthenticated,
getAuthHeaders: this.authHeaders.get,
},
isTlsEnabled: config.ssl.enabled,
// Return server instance with the connection options so that we can properly
// bridge core and the "legacy" Kibana internally. Once this bridge isn't
// needed anymore we shouldn't return the instance from this method.

View file

@ -45,6 +45,7 @@ const createSetupContractMock = () => {
getAuthHeaders: jest.fn(),
},
createNewServer: jest.fn(),
isTlsEnabled: false,
};
setupContract.createNewServer.mockResolvedValue({} as HttpServerSetup);
return setupContract;

View file

@ -122,6 +122,7 @@ export interface CoreSetup {
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
createNewServer: HttpServiceSetup['createNewServer'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
};
}

View file

@ -122,6 +122,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
registerOnPostAuth: deps.http.registerOnPostAuth,
basePath: deps.http.basePath,
createNewServer: deps.http.createNewServer,
isTlsEnabled: deps.http.isTlsEnabled,
},
};
}

View file

@ -97,6 +97,7 @@ export interface CoreSetup {
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
createNewServer: HttpServiceSetup['createNewServer'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
};
}