Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate the "Blacklist / Whitelist" nomenclature #1319

Closed
10 tasks done
tlfeng opened this issue Mar 4, 2022 · 2 comments · Fixed by #1808
Closed
10 tasks done

Deprecate the "Blacklist / Whitelist" nomenclature #1319

tlfeng opened this issue Mar 4, 2022 · 2 comments · Fixed by #1808
Assignees

Comments

@tlfeng
Copy link

tlfeng commented Mar 4, 2022

Background

OpenSearch repository is going to replace the terminology "blacklist / whitelist" with "denylist / allowlist".
issue: opensearch-project/OpenSearch#1483, with the plan for its terminology replacement.

Although the existing usages with "master" will be supported in OpenSearch version 2.x to keep the backwards compatibility, please prepare for the nomenclature change in advance, and replace all the usages with "master" terminology in the code base.
All the OpenSearch REST APIs and settings that contain "master" terminology will be deprecated in 2.0, and alternative usages will be added.

Solution

Replace the terminology "blacklist" with "denylist".
Replace the terminology "whitelist" with "allowlist".

When being compatible with OpenSearch 2.0:

  • Replace "blacklist" in descriptive text
  • Replace "whitelist" in descriptive text and internal variable name
  • Replace "whitelist" in setting name
  • Replace "whitelist" in documentation

Tasks

  • Deprecate http.compression.referrerWhitelist in favor of http.compression.referrerAllowlist ( Unknown configuration key(s)/ does not exists)
  • Deprecate http.compression.referrerWhitelistConfigured in favor of http.compression.referrerAllowlistConfigured( Unknown configuration key(s)/ does not exists)
  • Deprecate http.xsrf.whitelist in favor of http.xsrf.allowlist ( Unknown configuration key(s)/ does not exists)
  • Deprecate http.xsrf.whitelistConfigured in favor of http.xsrf.allowlistConfigured
  • Deprecate opensearch.requestHeadersWhitelist in favor of opensearch.requestHeadersAllowlist
  • Deprecate opensearch.requestHeadersWhitelistConfigured in favor of opensearch.requestHeadersAllowlistConfigured
  • Deprecate server.compression.referrerWhitelist in favor of server.compression.referrerAllowlist
  • Deprecate server.xsrf.whitelist in favor of server.xsrf.allowlist
  • Replace blacklist with denylist in comments and local variables
  • Replace whitelist with allowlist in comments and local variables

Non-inclusive instances of blacklist and whitelist

APIs, configuration

http.compression.referrerWhitelist config

test('accepts valid referrer whitelist', () => {
const {
compression: { referrerWhitelist },
} = config.schema.validate({
compression: {
referrerWhitelist: validHostnames,
},
});
expect(referrerWhitelist).toMatchSnapshot();
});
test('throws if invalid referrer whitelist', () => {
const httpSchema = config.schema;
const invalidHostnames = {
compression: {
referrerWhitelist: [invalidHostname],
},
};
const emptyArray = {
compression: {
referrerWhitelist: [],
},
};
expect(() => httpSchema.validate(invalidHostnames)).toThrowErrorMatchingSnapshot();
expect(() => httpSchema.validate(emptyArray)).toThrowErrorMatchingSnapshot();
});
test('throws if referrer whitelist is specified and compression is disabled', () => {
const httpSchema = config.schema;
const obj = {
compression: {
enabled: false,
referrerWhitelist: validHostnames,

referrerWhitelist: schema.maybe(

if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) {
return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false';

describe('with defined `compression.referrerWhitelist`', () => {
let listener: Server;
beforeEach(async () => {
listener = await setupServer({
...config,
compression: { enabled: true, referrerWhitelist: ['foo'] },

const { enabled, referrerWhitelist: list } = config.compression;

exports[`with compression accepts valid referrer whitelist 1`] = `
Array [
"www.example.com",
"8.8.8.8",
"::1",
"localhost",
]
`;
exports[`with compression throws if invalid referrer whitelist 1`] = `"[compression.referrerWhitelist.0]: value must be a valid hostname (see RFC 1123)."`;
exports[`with compression throws if invalid referrer whitelist 2`] = `"[compression.referrerWhitelist]: array size is [0], but cannot be smaller than [1]"`;
exports[`with compression throws if referrer whitelist is specified and compression is disabled 1`] = `"cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false"`;

http.compression.referrerWhitelistConfigured config

referrerWhitelistConfigured: isConfigured.array(http.compression.referrerWhitelist),

referrerWhitelistConfigured: boolean;

http.xsrf.whitelist config

whitelistConfigured: isConfigured.array(http.xsrf.whitelist),

test('throws if xsrf.whitelist element does not start with a slash', () => {
const httpSchema = config.schema;
const obj = {
xsrf: {
whitelist: ['/valid-path', 'invalid-path'],
},
};
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
`"[xsrf.whitelist.1]: must start with a slash"`

public xsrf: { disableProtection: boolean; whitelist: string[] };

const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'get', headers: {} });
toolkit.next.mockReturnValue('next' as any);
const result = handler(request, responseFactory, toolkit);
expect(responseFactory.badRequest).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
});
describe('destructive methods', () => {
it('accepts requests with xsrf header', () => {
const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'post', headers: { 'osd-xsrf': 'xsrf' } });
toolkit.next.mockReturnValue('next' as any);
const result = handler(request, responseFactory, toolkit);
expect(responseFactory.badRequest).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
it('accepts requests with version header', () => {
const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'post', headers: { 'osd-version': 'some-version' } });
toolkit.next.mockReturnValue('next' as any);
const result = handler(request, responseFactory, toolkit);
expect(responseFactory.badRequest).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
it('returns a bad request if called without xsrf or version header', () => {
const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'post' });
responseFactory.badRequest.mockReturnValue('badRequest' as any);
const result = handler(request, responseFactory, toolkit);
expect(toolkit.next).not.toHaveBeenCalled();
expect(responseFactory.badRequest).toHaveBeenCalledTimes(1);
expect(responseFactory.badRequest.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"body": "Request must contain a osd-xsrf header.",
}
`);
expect(result).toEqual('badRequest');
});
it('accepts requests if protection is disabled', () => {
const config = createConfig({ xsrf: { whitelist: [], disableProtection: true } });
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'post', headers: {} });
toolkit.next.mockReturnValue('next' as any);
const result = handler(request, responseFactory, toolkit);
expect(responseFactory.badRequest).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
it('accepts requests if path is whitelisted', () => {
const config = createConfig({
xsrf: { whitelist: ['/some-path'], disableProtection: false },
});
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'post', headers: {}, path: '/some-path' });
toolkit.next.mockReturnValue('next' as any);
const result = handler(request, responseFactory, toolkit);
expect(responseFactory.badRequest).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
it('accepts requests if xsrf protection on a route is disabled', () => {
const config = createConfig({
xsrf: { whitelist: [], disableProtection: false },

const { whitelist, disableProtection } = config.xsrf;
return (request, response, toolkit) => {
if (
disableProtection ||
whitelist.includes(request.route.path) ||

const whitelistedTestPath = '/xsrf/test/route/whitelisted';
const xsrfDisabledTestPath = '/xsrf/test/route/disabled';
const opensearchDashboardsName = 'my-opensearch-dashboards-name';
const setupDeps = {
context: contextServiceMock.createSetupContract(),
};
describe('core lifecycle handlers', () => {
let server: HttpService;
let innerServer: HttpServerSetup['server'];
let router: IRouter;
beforeEach(async () => {
const configService = configServiceMock.create();
configService.atPath.mockReturnValue(
new BehaviorSubject({
hosts: ['localhost'],
maxPayload: new ByteSizeValue(1024),
autoListen: true,
ssl: {
enabled: false,
},
compression: { enabled: true },
name: opensearchDashboardsName,
customResponseHeaders: {
'some-header': 'some-value',
},
xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] },
requestId: {
allowFromAnyIp: true,
ipAllowlist: [],
},
} as any)
);
server = createHttpServer({ configService });
const serverSetup = await server.setup(setupDeps);
router = serverSetup.createRouter('/');
innerServer = serverSetup.server;
}, 30000);
afterEach(async () => {
await server.stop();
});
describe('versionCheck post-auth handler', () => {
const testRoute = '/version_check/test/route';
beforeEach(async () => {
router.get({ path: testRoute, validate: false }, (context, req, res) => {
return res.ok({ body: 'ok' });
});
await server.start();
});
it('accepts requests with the correct version passed in the version header', async () => {
await supertest(innerServer.listener)
.get(testRoute)
.set(versionHeader, actualVersion)
.expect(200, 'ok');
});
it('accepts requests that do not include a version header', async () => {
await supertest(innerServer.listener).get(testRoute).expect(200, 'ok');
});
it('rejects requests with an incorrect version passed in the version header', async () => {
await supertest(innerServer.listener)
.get(testRoute)
.set(versionHeader, 'invalid-version')
.expect(400, /Browser client is out of date/);
});
});
describe('customHeaders pre-response handler', () => {
const testRoute = '/custom_headers/test/route';
const testErrorRoute = '/custom_headers/test/error_route';
beforeEach(async () => {
router.get({ path: testRoute, validate: false }, (context, req, res) => {
return res.ok({ body: 'ok' });
});
router.get({ path: testErrorRoute, validate: false }, (context, req, res) => {
return res.badRequest({ body: 'bad request' });
});
await server.start();
});
it('adds the osd-name header', async () => {
const result = await supertest(innerServer.listener).get(testRoute).expect(200, 'ok');
const headers = result.header as Record<string, string>;
expect(headers).toEqual(
expect.objectContaining({
[nameHeader]: opensearchDashboardsName,
})
);
});
it('adds the osd-name header in case of error', async () => {
const result = await supertest(innerServer.listener).get(testErrorRoute).expect(400);
const headers = result.header as Record<string, string>;
expect(headers).toEqual(
expect.objectContaining({
[nameHeader]: opensearchDashboardsName,
})
);
});
it('adds the custom headers', async () => {
const result = await supertest(innerServer.listener).get(testRoute).expect(200, 'ok');
const headers = result.header as Record<string, string>;
expect(headers).toEqual(expect.objectContaining({ 'some-header': 'some-value' }));
});
it('adds the custom headers in case of error', async () => {
const result = await supertest(innerServer.listener).get(testErrorRoute).expect(400);
const headers = result.header as Record<string, string>;
expect(headers).toEqual(expect.objectContaining({ 'some-header': 'some-value' }));
});
});
describe('xsrf post-auth handler', () => {
const testPath = '/xsrf/test/route';
const destructiveMethods = ['POST', 'PUT', 'DELETE'];
const nonDestructiveMethods = ['GET', 'HEAD'];
const getSupertest = (method: string, path: string): supertest.Test => {
return (supertest(innerServer.listener) as any)[method.toLowerCase()](path) as supertest.Test;
};
beforeEach(async () => {
router.get({ path: testPath, validate: false }, (context, req, res) => {
return res.ok({ body: 'ok' });
});
destructiveMethods.forEach((method) => {
((router as any)[method.toLowerCase()] as RouteRegistrar<any>)<any, any, any>(
{ path: testPath, validate: false },
(context, req, res) => {
return res.ok({ body: 'ok' });
}
);
((router as any)[method.toLowerCase()] as RouteRegistrar<any>)<any, any, any>(
{ path: whitelistedTestPath, validate: false },
(context, req, res) => {
return res.ok({ body: 'ok' });
}
);
((router as any)[method.toLowerCase()] as RouteRegistrar<any>)<any, any, any>(
{ path: xsrfDisabledTestPath, validate: false, options: { xsrfRequired: false } },
(context, req, res) => {
return res.ok({ body: 'ok' });
}
);
});
await server.start();
});
nonDestructiveMethods.forEach((method) => {
describe(`When using non-destructive ${method} method`, () => {
it('accepts requests without a token', async () => {
await getSupertest(method.toLowerCase(), testPath).expect(
200,
method === 'HEAD' ? undefined : 'ok'
);
});
it('accepts requests with the xsrf header', async () => {
await getSupertest(method.toLowerCase(), testPath)
.set(xsrfHeader, 'anything')
.expect(200, method === 'HEAD' ? undefined : 'ok');
});
});
});
destructiveMethods.forEach((method) => {
describe(`When using destructive ${method} method`, () => {
it('accepts requests with the xsrf header', async () => {
await getSupertest(method.toLowerCase(), testPath)
.set(xsrfHeader, 'anything')
.expect(200, 'ok');
});
it('accepts requests with the version header', async () => {
await getSupertest(method.toLowerCase(), testPath)
.set(versionHeader, actualVersion)
.expect(200, 'ok');
});
it('rejects requests without either an xsrf or version header', async () => {
await getSupertest(method.toLowerCase(), testPath).expect(400, {
statusCode: 400,
error: 'Bad Request',
message: 'Request must contain a osd-xsrf header.',
});
});
it('accepts whitelisted requests without either an xsrf or version header', async () => {
await getSupertest(method.toLowerCase(), whitelistedTestPath).expect(200, 'ok');

http.xsrf.whitelistConfigured config

whitelistConfigured: boolean;

whitelistConfigured: isConfigured.array(http.xsrf.whitelist),

whitelistConfigured: isConfigured.array(http.xsrf.whitelist),

opensearch.requestHeadersWhitelist config

#opensearch.requestHeadersWhitelist: [ authorization ]
# Header names and values that are sent to OpenSearch. Any custom headers cannot be overwritten
# by client-side headers, regardless of the opensearch.requestHeadersWhitelist configuration.

requestHeadersWhitelist: ["${OSD_ENV_VAR1}", "${OSD_ENV_VAR2}"]

requestHeadersWhitelist: ["${OSD_ENV_VAR1}", "${OSD_ENV_VAR2}"]

requestHeadersWhitelist: Type<string | string[]>;

referrerWhitelistConfigured: boolean;

export type LegacyOpenSearchClientConfig = Pick<ConfigOptions, 'keepAlive' | 'log' | 'plugins'> & Pick<OpenSearchConfig, 'apiVersion' | 'customHeaders' | 'logQueries' | 'requestHeadersWhitelist' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'hosts' | 'username' | 'password'> & {

export type OpenSearchClientConfig = Pick<OpenSearchConfig, 'customHeaders' | 'logQueries' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'requestHeadersWhitelist' | 'sniffInterval' | 'hosts' | 'username' | 'password'> & {

readonly requestHeadersWhitelist: string[];

"requestHeadersWhitelist": Array [
"authorization",
],
"requestTimeout": "PT30S",
"shardTimeout": "PT30S",
"sniffInterval": false,
"sniffOnConnectionFault": false,
"sniffOnStart": false,
"ssl": Object {
"alwaysPresentCertificate": false,
"certificate": undefined,
"certificateAuthorities": undefined,
"key": undefined,
"keyPassphrase": undefined,
"verificationMode": "full",
},
"username": undefined,
}
`);
});
test('#hosts accepts both string and array of strings', () => {
let configValue = new OpenSearchConfig(
config.schema.validate({ hosts: 'http://some.host:1234' })
);
expect(configValue.hosts).toEqual(['http://some.host:1234']);
configValue = new OpenSearchConfig(config.schema.validate({ hosts: ['http://some.host:1234'] }));
expect(configValue.hosts).toEqual(['http://some.host:1234']);
configValue = new OpenSearchConfig(
config.schema.validate({
hosts: ['http://some.host:1234', 'https://some.another.host'],
})
);
expect(configValue.hosts).toEqual(['http://some.host:1234', 'https://some.another.host']);
});
test('#requestHeadersWhitelist accepts both string and array of strings', () => {
let configValue = new OpenSearchConfig(
config.schema.validate({ requestHeadersWhitelist: 'token' })
);
expect(configValue.requestHeadersWhitelist).toEqual(['token']);
configValue = new OpenSearchConfig(
config.schema.validate({ requestHeadersWhitelist: ['token'] })
);
expect(configValue.requestHeadersWhitelist).toEqual(['token']);
configValue = new OpenSearchConfig(
config.schema.validate({
requestHeadersWhitelist: ['token', 'X-Forwarded-Proto'],
})
);
expect(configValue.requestHeadersWhitelist).toEqual(['token', 'X-Forwarded-Proto']);
});
describe('reads files', () => {
beforeEach(() => {
mockReadFileSync.mockReset();
mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
mockReadPkcs12Keystore.mockReset();
mockReadPkcs12Keystore.mockImplementation((path: string) => ({
key: `content-of-${path}.key`,
cert: `content-of-${path}.cert`,
ca: [`content-of-${path}.ca`],
}));
mockReadPkcs12Truststore.mockReset();
mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]);
});
it('reads certificate authorities when ssl.keystore.path is specified', () => {
const configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { keystore: { path: 'some-path' } } })
);
expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path.ca']);
});
it('reads certificate authorities when ssl.truststore.path is specified', () => {
const configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { truststore: { path: 'some-path' } } })
);
expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);
});
it('reads certificate authorities when ssl.certificateAuthorities is specified', () => {
let configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { certificateAuthorities: 'some-path' } })
);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);
mockReadFileSync.mockClear();
configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } })
);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);
mockReadFileSync.mockClear();
configValue = new OpenSearchConfig(
config.schema.validate({
ssl: { certificateAuthorities: ['some-path', 'another-path'] },
})
);
expect(mockReadFileSync).toHaveBeenCalledTimes(2);
expect(configValue.ssl.certificateAuthorities).toEqual([
'content-of-some-path',
'content-of-another-path',
]);
});
it('reads certificate authorities when ssl.keystore.path, ssl.truststore.path, and ssl.certificateAuthorities are specified', () => {
const configValue = new OpenSearchConfig(
config.schema.validate({
ssl: {
keystore: { path: 'some-path' },
truststore: { path: 'another-path' },
certificateAuthorities: 'yet-another-path',
},
})
);
expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual([
'content-of-some-path.ca',
'content-of-another-path',
'content-of-yet-another-path',
]);
});
it('reads a private key and certificate when ssl.keystore.path is specified', () => {
const configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { keystore: { path: 'some-path' } } })
);
expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
expect(configValue.ssl.key).toEqual('content-of-some-path.key');
expect(configValue.ssl.certificate).toEqual('content-of-some-path.cert');
});
it('reads a private key when ssl.key is specified', () => {
const configValue = new OpenSearchConfig(config.schema.validate({ ssl: { key: 'some-path' } }));
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.key).toEqual('content-of-some-path');
});
it('reads a certificate when ssl.certificate is specified', () => {
const configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { certificate: 'some-path' } })
);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificate).toEqual('content-of-some-path');
});
});
describe('throws when config is invalid', () => {
beforeAll(() => {
const realFs = jest.requireActual('fs');
mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path));
const utils = jest.requireActual('../utils');
mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) =>
utils.readPkcs12Keystore(path, password)
);
mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) =>
utils.readPkcs12Truststore(path, password)
);
});
it('throws if key is invalid', () => {
const value = { ssl: { key: '/invalid/key' } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/key'"`
);
});
it('throws if certificate is invalid', () => {
const value = { ssl: { certificate: '/invalid/cert' } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/cert'"`
);
});
it('throws if certificateAuthorities is invalid', () => {
const value = { ssl: { certificateAuthorities: '/invalid/ca' } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(`"ENOENT: no such file or directory, open '/invalid/ca'"`);
});
it('throws if keystore path is invalid', () => {
const value = { ssl: { keystore: { path: '/invalid/keystore' } } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/keystore'"`
);
});
it('throws if keystore does not contain a key', () => {
mockReadPkcs12Keystore.mockReturnValueOnce({});
const value = { ssl: { keystore: { path: 'some-path' } } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(`"Did not find key in OpenSearch keystore."`);
});
it('throws if keystore does not contain a certificate', () => {
mockReadPkcs12Keystore.mockReturnValueOnce({ key: 'foo' });
const value = { ssl: { keystore: { path: 'some-path' } } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(`"Did not find certificate in OpenSearch keystore."`);
});
it('throws if truststore path is invalid', () => {
const value = { ssl: { keystore: { path: '/invalid/truststore' } } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/truststore'"`
);
});
it('throws if key and keystore.path are both specified', () => {
const value = { ssl: { key: 'foo', keystore: { path: 'bar' } } };
expect(() => config.schema.validate(value)).toThrowErrorMatchingInlineSnapshot(
`"[ssl]: cannot use [key] when [keystore.path] is specified"`
);
});
it('throws if certificate and keystore.path are both specified', () => {
const value = { ssl: { certificate: 'foo', keystore: { path: 'bar' } } };
expect(() => config.schema.validate(value)).toThrowErrorMatchingInlineSnapshot(
`"[ssl]: cannot use [certificate] when [keystore.path] is specified"`
);
});
});
describe('deprecations', () => {
it('logs a warning if opensearch.username is set to "elastic"', () => {
const { messages } = applyOpenSearchDeprecations({ username: 'elastic' });
expect(messages).toMatchInlineSnapshot(`
Array [
"Setting [opensearch.username] to \\"elastic\\" is deprecated. You should use the \\"opensearch_dashboards_system\\" user instead.",
]
`);
});
it('logs a warning if opensearch.username is set to "opensearchDashboards"', () => {
const { messages } = applyOpenSearchDeprecations({ username: 'opensearchDashboards' });
expect(messages).toMatchInlineSnapshot(`
Array [
"Setting [opensearch.username] to \\"opensearchDashboards\\" is deprecated. You should use the \\"opensearch_dashboards_system\\" user instead.",
]
`);
});
it('does not log a warning if opensearch.username is set to something besides "elastic" or "opensearchDashboards"', () => {
const { messages } = applyOpenSearchDeprecations({ username: 'otheruser' });
expect(messages).toHaveLength(0);
});
it('does not log a warning if opensearch.username is unset', () => {
const { messages } = applyOpenSearchDeprecations({});
expect(messages).toHaveLength(0);
});
it('logs a warning if ssl.key is set and ssl.certificate is not', () => {
const { messages } = applyOpenSearchDeprecations({ ssl: { key: '' } });
expect(messages).toMatchInlineSnapshot(`
Array [
"Setting [opensearch.ssl.key] without [opensearch.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to OpenSearch.",
]
`);
});
it('logs a warning if ssl.certificate is set and ssl.key is not', () => {
const { messages } = applyOpenSearchDeprecations({ ssl: { certificate: '' } });
expect(messages).toMatchInlineSnapshot(`
Array [
"Setting [opensearch.ssl.certificate] without [opensearch.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to OpenSearch.",
]
`);
});
it('does not log a warning if both ssl.key and ssl.certificate are set', () => {
const { messages } = applyOpenSearchDeprecations({ ssl: { key: '', certificate: '' } });
expect(messages).toEqual([]);
});
it('logs a warning if elasticsearch.sniffOnStart is set and opensearch.sniffOnStart is not', () => {
const { messages } = applyLegacyDeprecations({ sniffOnStart: true });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.sniffOnStart\\" is deprecated and has been replaced by \\"opensearch.sniffOnStart\\"",
]
`);
});
it('logs a warning if elasticsearch.sniffInterval is set and opensearch.sniffInterval is not', () => {
const { messages } = applyLegacyDeprecations({ sniffInterval: true });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.sniffInterval\\" is deprecated and has been replaced by \\"opensearch.sniffInterval\\"",
]
`);
});
it('logs a warning if elasticsearch.sniffOnConnectionFault is set and opensearch.sniffOnConnectionFault is not', () => {
const { messages } = applyLegacyDeprecations({ sniffOnConnectionFault: true });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.sniffOnConnectionFault\\" is deprecated and has been replaced by \\"opensearch.sniffOnConnectionFault\\"",
]
`);
});
it('logs a warning if elasticsearch.hosts is set and opensearch.hosts is not', () => {
const { messages } = applyLegacyDeprecations({ hosts: [''] });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.hosts\\" is deprecated and has been replaced by \\"opensearch.hosts\\"",
]
`);
});
it('logs a warning if elasticsearch.username is set and opensearch.username is not', () => {
const { messages } = applyLegacyDeprecations({ username: '' });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.username\\" is deprecated and has been replaced by \\"opensearch.username\\"",
]
`);
});
it('logs a warning if elasticsearch.password is set and opensearch.password is not', () => {
const { messages } = applyLegacyDeprecations({ password: '' });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.password\\" is deprecated and has been replaced by \\"opensearch.password\\"",
]
`);
});
it('logs a warning if elasticsearch.requestHeadersWhitelist is set and opensearch.requestHeadersWhitelist is not', () => {
const { messages } = applyLegacyDeprecations({ requestHeadersWhitelist: [''] });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.requestHeadersWhitelist\\" is deprecated and has been replaced by \\"opensearch.requestHeadersWhitelist\\"",
"\\"opensearch.requestHeadersWhitelist\\" is deprecated and has been replaced by \\"opensearch.requestHeadersAllowlist\\"",

requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], {
defaultValue: ['authorization'],
}),
memoryCircuitBreaker: schema.object({
enabled: schema.boolean({ defaultValue: false }),
maxPercentage: schema.number({ defaultValue: 1.0 }),
}),
customHeaders: schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }),
shardTimeout: schema.duration({ defaultValue: '30s' }),
requestTimeout: schema.duration({ defaultValue: '30s' }),
pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }),
logQueries: schema.boolean({ defaultValue: false }),
optimizedHealthcheckId: schema.maybe(schema.string()),
ssl: schema.object(
{
verificationMode: schema.oneOf(
[schema.literal('none'), schema.literal('certificate'), schema.literal('full')],
{ defaultValue: 'full' }
),
certificateAuthorities: schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })])
),
certificate: schema.maybe(schema.string()),
key: schema.maybe(schema.string()),
keyPassphrase: schema.maybe(schema.string()),
keystore: schema.object({
path: schema.maybe(schema.string()),
password: schema.maybe(schema.string()),
}),
truststore: schema.object({
path: schema.maybe(schema.string()),
password: schema.maybe(schema.string()),
}),
alwaysPresentCertificate: schema.boolean({ defaultValue: false }),
},
{
validate: (rawConfig) => {
if (rawConfig.key && rawConfig.keystore.path) {
return 'cannot use [key] when [keystore.path] is specified';
}
if (rawConfig.certificate && rawConfig.keystore.path) {
return 'cannot use [certificate] when [keystore.path] is specified';
}
},
}
),
apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }),
healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }),
ignoreVersionMismatch: schema.conditional(
schema.contextRef('dev'),
false,
schema.boolean({
validate: (rawValue) => {
if (rawValue === true) {
return '"ignoreVersionMismatch" can only be set to true in development mode';
}
},
defaultValue: false,
}),
schema.boolean({ defaultValue: false })
),
});
const deprecations: ConfigDeprecationProvider = ({ renameFromRoot, renameFromRootWithoutMap }) => [
renameFromRoot('elasticsearch.sniffOnStart', 'opensearch.sniffOnStart'),
renameFromRoot('elasticsearch.sniffInterval', 'opensearch.sniffInterval'),
renameFromRoot('elasticsearch.sniffOnConnectionFault', 'opensearch.sniffOnConnectionFault'),
renameFromRoot('elasticsearch.hosts', 'opensearch.hosts'),
renameFromRoot('elasticsearch.username', 'opensearch.username'),
renameFromRoot('elasticsearch.password', 'opensearch.password'),
renameFromRoot('elasticsearch.requestHeadersWhitelist', 'opensearch.requestHeadersWhitelist'),
renameFromRootWithoutMap(
'opensearch.requestHeadersWhitelist',
'opensearch.requestHeadersAllowlist'
),
renameFromRoot('elasticsearch.customHeaders', 'opensearch.customHeaders'),
renameFromRoot('elasticsearch.shardTimeout', 'opensearch.shardTimeout'),
renameFromRoot('elasticsearch.requestTimeout', 'opensearch.requestTimeout'),
renameFromRoot('elasticsearch.pingTimeout', 'opensearch.pingTimeout'),
renameFromRoot('elasticsearch.logQueries', 'opensearch.logQueries'),
renameFromRoot('elasticsearch.optimizedHealthcheckId', 'opensearch.optimizedHealthcheckId'),
renameFromRoot('elasticsearch.ssl', 'opensearch.ssl'),
renameFromRoot('elasticsearch.apiVersion', 'opensearch.apiVersion'),
renameFromRoot('elasticsearch.healthCheck', 'opensearch.healthCheck'),
renameFromRoot('elasticsearch.ignoreVersionMismatch', 'opensearch.ignoreVersionMismatch'),
(settings, fromPath, log) => {
const opensearch = settings[fromPath];
if (!opensearch) {
return settings;
}
if (opensearch.username === 'elastic') {
log(
`Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "opensearch_dashboards_system" user instead.`
);
} else if (opensearch.username === 'opensearchDashboards') {
log(
`Setting [${fromPath}.username] to "opensearchDashboards" is deprecated. You should use the "opensearch_dashboards_system" user instead.`
);
}
if (opensearch.ssl?.key !== undefined && opensearch.ssl?.certificate === undefined) {
log(
`Setting [${fromPath}.ssl.key] without [${fromPath}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to OpenSearch.`
);
} else if (opensearch.ssl?.certificate !== undefined && opensearch.ssl?.key === undefined) {
log(
`Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to OpenSearch.`
);
}
return settings;
},
];
export const config: ServiceConfigDescriptor<OpenSearchConfigType> = {
path: 'opensearch',
schema: configSchema,
deprecations,
};
/**
* Wrapper of config schema.
* @public
*/
export class OpenSearchConfig {
/**
* The interval between health check requests OpenSearch Dashboards sends to the OpenSearch.
*/
public readonly healthCheckDelay: Duration;
/**
* Whether to allow opensearch-dashboards to connect to a non-compatible opensearch node.
*/
public readonly ignoreVersionMismatch: boolean;
/**
* Version of the OpenSearch (6.7, 7.1 or `master`) client will be connecting to.
*/
public readonly apiVersion: string;
/**
* Specifies whether all queries to the client should be logged (status code,
* method, query etc.).
*/
public readonly logQueries: boolean;
/**
* Specifies whether Dashboards should only query the local OpenSearch node when
* all nodes in the cluster have the same node attribute value
*/
public readonly optimizedHealthcheckId?: string;
/**
* Hosts that the client will connect to. If sniffing is enabled, this list will
* be used as seeds to discover the rest of your cluster.
*/
public readonly hosts: string[];
/**
* List of OpenSearch Dashboards client-side headers to send to OpenSearch when request
* scoped cluster client is used. If this is an empty array then *no* client-side
* will be sent.
*/
public readonly requestHeadersWhitelist: string[];
/**
* Timeout after which PING HTTP request will be aborted and retried.
*/
public readonly pingTimeout: Duration;
/**
* Timeout after which HTTP request will be aborted and retried.
*/
public readonly requestTimeout: Duration;
/**
* Timeout for OpenSearch to wait for responses from shards. Set to 0 to disable.
*/
public readonly shardTimeout: Duration;
/**
* Set of options to configure memory circuit breaker for query response.
* The `maxPercentage` field is to determine the threshold for maximum heap size for memory circuit breaker. By default the value is `1.0`.
* The `enabled` field specifies whether the client should protect large response that can't fit into memory.
*/
public readonly memoryCircuitBreaker: OpenSearchConfigType['memoryCircuitBreaker'];
/**
* Specifies whether the client should attempt to detect the rest of the cluster
* when it is first instantiated.
*/
public readonly sniffOnStart: boolean;
/**
* Interval to perform a sniff operation and make sure the list of nodes is complete.
* If `false` then sniffing is disabled.
*/
public readonly sniffInterval: false | Duration;
/**
* Specifies whether the client should immediately sniff for a more current list
* of nodes when a connection dies.
*/
public readonly sniffOnConnectionFault: boolean;
/**
* If OpenSearch is protected with basic authentication, this setting provides
* the username that the OpenSearch Dashboards server uses to perform its administrative functions.
*/
public readonly username?: string;
/**
* If OpenSearch is protected with basic authentication, this setting provides
* the password that the OpenSearch Dashboards server uses to perform its administrative functions.
*/
public readonly password?: string;
/**
* Set of settings configure SSL connection between OpenSearch Dashboards and OpenSearch that
* are required when `xpack.ssl.verification_mode` in OpenSearch is set to
* either `certificate` or `full`.
*/
public readonly ssl: Pick<
SslConfigSchema,
Exclude<keyof SslConfigSchema, 'certificateAuthorities' | 'keystore' | 'truststore'>
> & { certificateAuthorities?: string[] };
/**
* Header names and values to send to OpenSearch with every request. These
* headers cannot be overwritten by client-side headers and aren't affected by
* `requestHeadersWhitelist` configuration.
*/
public readonly customHeaders: OpenSearchConfigType['customHeaders'];
constructor(rawConfig: OpenSearchConfigType) {
this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch;
this.apiVersion = rawConfig.apiVersion;
this.logQueries = rawConfig.logQueries;
this.optimizedHealthcheckId = rawConfig.optimizedHealthcheckId;
this.hosts = Array.isArray(rawConfig.hosts) ? rawConfig.hosts : [rawConfig.hosts];
this.requestHeadersWhitelist = Array.isArray(rawConfig.requestHeadersWhitelist)
? rawConfig.requestHeadersWhitelist
: [rawConfig.requestHeadersWhitelist];

"requestHeadersWhitelist": Array [
undefined,
],
"ssl": Object {
"certificate": "certificate-value",
"verificationMode": "none",
},
}
`);
});
it('falls back to opensearch config if custom config not passed', async () => {
const setupContract = await opensearchService.setup(setupDeps);
// reset all mocks called during setup phase
MockLegacyClusterClient.mockClear();
setupContract.legacy.createClient('another-type');
const config = MockLegacyClusterClient.mock.calls[0][0];
expect(config).toMatchInlineSnapshot(`
Object {
"healthCheckDelay": "PT0.01S",
"hosts": Array [
"http://1.2.3.4",
],
"requestHeadersWhitelist": Array [
undefined,
],
"ssl": Object {
"alwaysPresentCertificate": undefined,
"certificate": undefined,
"certificateAuthorities": undefined,
"key": undefined,
"keyPassphrase": undefined,
"verificationMode": "none",
},
}
`);
});
it('does not merge opensearch hosts if custom config overrides', async () => {
configService.atPath.mockReturnValueOnce(
new BehaviorSubject({
hosts: ['http://1.2.3.4', 'http://9.8.7.6'],
healthCheck: {
delay: duration(2000),
},
ssl: {
verificationMode: 'none',
},
} as any)
);
opensearchService = new OpenSearchService(coreContext);
const setupContract = await opensearchService.setup(setupDeps);
// reset all mocks called during setup phase
MockLegacyClusterClient.mockClear();
const customConfig = {
hosts: ['http://8.8.8.8'],
logQueries: true,
ssl: { certificate: 'certificate-value' },
};
setupContract.legacy.createClient('some-custom-type', customConfig);
const config = MockLegacyClusterClient.mock.calls[0][0];
expect(config).toMatchInlineSnapshot(`
Object {
"healthCheckDelay": "PT2S",
"hosts": Array [
"http://8.8.8.8",
],
"logQueries": true,
"requestHeadersWhitelist": Array [
undefined,
],
"ssl": Object {
"certificate": "certificate-value",
"verificationMode": "none",
},
}
`);
});
});
it('opensearchNodeVersionCompatibility$ only starts polling when subscribed to', (done) => {
const mockedClient = mockClusterClientInstance.asInternalUser;
mockedClient.nodes.info.mockImplementation(() =>
opensearchClientMock.createErrorTransportRequestPromise(new Error())
);
opensearchService.setup(setupDeps).then((setupContract) => {
delay(10).then(() => {
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(0);
setupContract.opensearchNodesCompatibility$.subscribe(() => {
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1);
done();
});
});
});
});
it('opensearchNodeVersionCompatibility$ stops polling when unsubscribed from', async () => {
const mockedClient = mockClusterClientInstance.asInternalUser;
mockedClient.nodes.info.mockImplementation(() =>
opensearchClientMock.createErrorTransportRequestPromise(new Error())
);
const setupContract = await opensearchService.setup(setupDeps);
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(0);
const sub = setupContract.opensearchNodesCompatibility$.subscribe(async () => {
sub.unsubscribe();
await delay(100);
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1);
});
});
});
describe('#start', () => {
it('throws if called before `setup`', async () => {
expect(() => opensearchService.start(startDeps)).rejects.toMatchInlineSnapshot(
`[Error: OpenSearchService needs to be setup before calling start]`
);
});
it('returns opensearch client as a part of the contract', async () => {
await opensearchService.setup(setupDeps);
const startContract = await opensearchService.start(startDeps);
const client = startContract.client;
expect(client.asInternalUser).toBe(mockClusterClientInstance.asInternalUser);
});
describe('#createClient', () => {
it('allows to specify config properties', async () => {
await opensearchService.setup(setupDeps);
const startContract = await opensearchService.start(startDeps);
// reset all mocks called during setup phase
MockClusterClient.mockClear();
const customConfig = { logQueries: true };
const clusterClient = startContract.createClient('custom-type', customConfig);
expect(clusterClient).toBe(mockClusterClientInstance);
expect(MockClusterClient).toHaveBeenCalledTimes(1);
expect(MockClusterClient).toHaveBeenCalledWith(
expect.objectContaining(customConfig),
expect.objectContaining({ context: ['opensearch', 'custom-type'] }),
expect.any(Function)
);
});
it('creates a new client on each call', async () => {
await opensearchService.setup(setupDeps);
const startContract = await opensearchService.start(startDeps);
// reset all mocks called during setup phase
MockClusterClient.mockClear();
const customConfig = { logQueries: true };
startContract.createClient('custom-type', customConfig);
startContract.createClient('another-type', customConfig);
expect(MockClusterClient).toHaveBeenCalledTimes(2);
});
it('falls back to opensearch default config values if property not specified', async () => {
await opensearchService.setup(setupDeps);
const startContract = await opensearchService.start(startDeps);
// reset all mocks called during setup phase
MockClusterClient.mockClear();
const customConfig = {
hosts: ['http://8.8.8.8'],
logQueries: true,
ssl: { certificate: 'certificate-value' },
};
startContract.createClient('some-custom-type', customConfig);
const config = MockClusterClient.mock.calls[0][0];
expect(config).toMatchInlineSnapshot(`
Object {
"healthCheckDelay": "PT0.01S",
"hosts": Array [
"http://8.8.8.8",
],
"logQueries": true,
"requestHeadersWhitelist": Array [

requestHeadersWhitelist: ['authorization'],

requestHeadersWhitelist: ['authorization'],
customHeaders: {},
hosts: ['http://localhost'],
...parts,
};
};
describe('ClusterClient', () => {
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
let getAuthHeaders: jest.MockedFunction<GetAuthHeaders>;
let internalClient: ReturnType<typeof opensearchClientMock.createInternalClient>;
let scopedClient: ReturnType<typeof opensearchClientMock.createInternalClient>;
beforeEach(() => {
logger = loggingSystemMock.createLogger();
internalClient = opensearchClientMock.createInternalClient();
scopedClient = opensearchClientMock.createInternalClient();
getAuthHeaders = jest.fn().mockImplementation(() => ({
authorization: 'auth',
foo: 'bar',
}));
configureClientMock.mockImplementation((config, { scoped = false }) => {
return scoped ? scopedClient : internalClient;
});
});
afterEach(() => {
configureClientMock.mockReset();
});
it('creates a single internal and scoped client during initialization', () => {
const config = createConfig();
new ClusterClient(config, logger, getAuthHeaders);
expect(configureClientMock).toHaveBeenCalledTimes(2);
expect(configureClientMock).toHaveBeenCalledWith(config, { logger });
expect(configureClientMock).toHaveBeenCalledWith(config, { logger, scoped: true });
});
describe('#asInternalUser', () => {
it('returns the internal client', () => {
const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders);
expect(clusterClient.asInternalUser).toBe(internalClient);
});
});
describe('#asScoped', () => {
it('returns a scoped cluster client bound to the request', () => {
const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest();
const scopedClusterClient = clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({ headers: expect.any(Object) });
expect(scopedClusterClient.asInternalUser).toBe(clusterClient.asInternalUser);
expect(scopedClusterClient.asCurrentUser).toBe(scopedClient.child.mock.results[0].value);
});
it('returns a distinct scoped cluster client on each call', () => {
const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest();
const scopedClusterClient1 = clusterClient.asScoped(request);
const scopedClusterClient2 = clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(2);
expect(scopedClusterClient1).not.toBe(scopedClusterClient2);
expect(scopedClusterClient1.asInternalUser).toBe(scopedClusterClient2.asInternalUser);
});
it('creates a scoped client with filtered request headers', () => {
const config = createConfig({
requestHeadersWhitelist: ['foo'],
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
headers: {
foo: 'bar',
hello: 'dolly',
},
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: { ...DEFAULT_HEADERS, foo: 'bar', 'x-opaque-id': expect.any(String) },
});
});
it('creates a scoped facade with filtered auth headers', () => {
const config = createConfig({
requestHeadersWhitelist: ['authorization'],
});
getAuthHeaders.mockReturnValue({
authorization: 'auth',
other: 'nope',
});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) },
});
});
it('respects auth headers precedence', () => {
const config = createConfig({
requestHeadersWhitelist: ['authorization'],
});
getAuthHeaders.mockReturnValue({
authorization: 'auth',
other: 'nope',
});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
headers: {
authorization: 'override',
},
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) },
});
});
it('includes the `customHeaders` from the config without filtering them', () => {
const config = createConfig({
customHeaders: {
foo: 'bar',
hello: 'dolly',
},
requestHeadersWhitelist: ['authorization'],
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
...DEFAULT_HEADERS,
foo: 'bar',
hello: 'dolly',
'x-opaque-id': expect.any(String),
},
});
});
it('adds the x-opaque-id header based on the request id', () => {
const config = createConfig();
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
opensearchDashboardsRequestState: {
requestId: 'my-fake-id',
requestUuid: 'ignore-this-id',
},
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
...DEFAULT_HEADERS,
'x-opaque-id': 'my-fake-id',
},
});
});
it('respect the precedence of auth headers over config headers', () => {
const config = createConfig({
customHeaders: {
foo: 'config',
hello: 'dolly',
},
requestHeadersWhitelist: ['foo'],
});
getAuthHeaders.mockReturnValue({
foo: 'auth',
});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
...DEFAULT_HEADERS,
foo: 'auth',
hello: 'dolly',
'x-opaque-id': expect.any(String),
},
});
});
it('respect the precedence of request headers over config headers', () => {
const config = createConfig({
customHeaders: {
foo: 'config',
hello: 'dolly',
},
requestHeadersWhitelist: ['foo'],
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
headers: { foo: 'request' },
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
...DEFAULT_HEADERS,
foo: 'request',
hello: 'dolly',
'x-opaque-id': expect.any(String),
},
});
});
it('respect the precedence of config headers over default headers', () => {
const headerKey = Object.keys(DEFAULT_HEADERS)[0];
const config = createConfig({
customHeaders: {
[headerKey]: 'foo',
},
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest();
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
[headerKey]: 'foo',
'x-opaque-id': expect.any(String),
},
});
});
it('respect the precedence of request headers over default headers', () => {
const headerKey = Object.keys(DEFAULT_HEADERS)[0];
const config = createConfig({
requestHeadersWhitelist: [headerKey],
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
headers: { [headerKey]: 'foo' },
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
[headerKey]: 'foo',
'x-opaque-id': expect.any(String),
},
});
});
it('respect the precedence of x-opaque-id header over config headers', () => {
const config = createConfig({
customHeaders: {
'x-opaque-id': 'from config',
},
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
headers: { foo: 'request' },
opensearchDashboardsRequestState: {
requestId: 'from request',
requestUuid: 'ignore-this-id',
},
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
...DEFAULT_HEADERS,
'x-opaque-id': 'from request',
},
});
});
it('filter headers when called with a `FakeRequest`', () => {
const config = createConfig({
requestHeadersWhitelist: ['authorization'],
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = {
headers: {
authorization: 'auth',
hello: 'dolly',
},
};
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: { ...DEFAULT_HEADERS, authorization: 'auth' },
});
});
it('does not add auth headers when called with a `FakeRequest`', () => {
const config = createConfig({
requestHeadersWhitelist: ['authorization', 'foo'],

...this.config.requestHeadersWhitelist,
]);
} else {
scopedHeaders = filterHeaders(request?.headers ?? {}, this.config.requestHeadersWhitelist);

requestHeadersWhitelist: ['one', 'two'],
} as any;
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
jest.clearAllMocks();
});
test('creates additional OpenSearch client only once', () => {
const firstScopedClusterClient = clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { one: '1' } })
);
expect(firstScopedClusterClient).toBeDefined();
expect(mockParseOpenSearchClientConfig).toHaveBeenCalledTimes(1);
expect(mockParseOpenSearchClientConfig).toHaveBeenLastCalledWith(
mockOpenSearchConfig,
mockLogger,
{
auth: false,
ignoreCertAndKey: true,
}
);
expect(MockClient).toHaveBeenCalledTimes(1);
expect(MockClient).toHaveBeenCalledWith(mockParseOpenSearchClientConfig.mock.results[0].value);
jest.clearAllMocks();
const secondScopedClusterClient = clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { two: '2' } })
);
expect(secondScopedClusterClient).toBeDefined();
expect(secondScopedClusterClient).not.toBe(firstScopedClusterClient);
expect(mockParseOpenSearchClientConfig).not.toHaveBeenCalled();
expect(MockClient).not.toHaveBeenCalled();
});
test('properly configures `ignoreCertAndKey` for various configurations', () => {
// Config without SSL.
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
mockParseOpenSearchClientConfig.mockClear();
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } }));
expect(mockParseOpenSearchClientConfig).toHaveBeenCalledTimes(1);
expect(mockParseOpenSearchClientConfig).toHaveBeenLastCalledWith(
mockOpenSearchConfig,
mockLogger,
{
auth: false,
ignoreCertAndKey: true,
}
);
// Config ssl.alwaysPresentCertificate === false
mockOpenSearchConfig = {
...mockOpenSearchConfig,
ssl: { alwaysPresentCertificate: false },
} as any;
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
mockParseOpenSearchClientConfig.mockClear();
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } }));
expect(mockParseOpenSearchClientConfig).toHaveBeenCalledTimes(1);
expect(mockParseOpenSearchClientConfig).toHaveBeenLastCalledWith(
mockOpenSearchConfig,
mockLogger,
{
auth: false,
ignoreCertAndKey: true,
}
);
// Config ssl.alwaysPresentCertificate === true
mockOpenSearchConfig = {
...mockOpenSearchConfig,
ssl: { alwaysPresentCertificate: true },
} as any;
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
mockParseOpenSearchClientConfig.mockClear();
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } }));
expect(mockParseOpenSearchClientConfig).toHaveBeenCalledTimes(1);
expect(mockParseOpenSearchClientConfig).toHaveBeenLastCalledWith(
mockOpenSearchConfig,
mockLogger,
{
auth: false,
ignoreCertAndKey: false,
}
);
});
test('passes only filtered headers to the scoped cluster client', () => {
clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } })
);
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ one: '1', two: '2' },
expect.any(Object)
);
});
test('passes x-opaque-id header with request id', () => {
clusterClient.asScoped(
httpServerMock.createOpenSearchDashboardsRequest({
opensearchDashboardsRequestState: { requestId: 'alpha', requestUuid: 'ignore-this-id' },
})
);
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ 'x-opaque-id': 'alpha' },
expect.any(Object)
);
});
test('both scoped and internal API caller fail if cluster client is closed', async () => {
clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } })
);
clusterClient.close();
const [[internalAPICaller, scopedAPICaller]] = MockScopedClusterClient.mock.calls;
await expect(internalAPICaller('ping')).rejects.toThrowErrorMatchingInlineSnapshot(
`"Cluster client cannot be used after it has been closed."`
);
await expect(scopedAPICaller('ping', {})).rejects.toThrowErrorMatchingInlineSnapshot(
`"Cluster client cannot be used after it has been closed."`
);
});
test('does not fail when scope to not defined request', async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
clusterClient.asScoped();
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{},
undefined
);
});
test('does not fail when scope to a request without headers', async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
clusterClient.asScoped({} as any);
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{},
undefined
);
});
test('calls getAuthHeaders and filters results for a real request', async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory,
() => ({
one: '1',
three: '3',
})
);
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { two: '2' } }));
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ one: '1', two: '2' },
expect.any(Object)
);
});
test('getAuthHeaders results rewrite extends a request headers', async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory,
() => ({ one: 'foo' })
);
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1', two: '2' } }));
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ one: 'foo', two: '2' },
expect.any(Object)
);
});
test("doesn't call getAuthHeaders for a fake request", async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory,
() => ({})
);
clusterClient.asScoped({ headers: { one: 'foo' } });
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ one: 'foo' },
undefined
);
});
test('filters a fake request headers', async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } });
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ one: '1', two: '2' },
undefined
);
});
describe('Auditor', () => {
it('creates Auditor for OpenSearchDashboardsRequest', async () => {
const auditor = auditTrailServiceMock.createAuditor();
const auditorFactory = auditTrailServiceMock.createAuditorFactory();
auditorFactory.asScoped.mockReturnValue(auditor);
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
() => auditorFactory
);
clusterClient.asScoped(httpServerMock.createOpenSearchDashboardsRequest());
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
expect.objectContaining({ 'x-opaque-id': expect.any(String) }),
auditor
);
});
it("doesn't create Auditor for a fake request", async () => {
const getAuthHeaders = jest.fn();
clusterClient = new LegacyClusterClient(mockOpenSearchConfig, mockLogger, getAuthHeaders);
clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } });
expect(getAuthHeaders).not.toHaveBeenCalled();
});
it("doesn't create Auditor when no request passed", async () => {
const getAuthHeaders = jest.fn();
clusterClient = new LegacyClusterClient(mockOpenSearchConfig, mockLogger, getAuthHeaders);
clusterClient.asScoped();
expect(getAuthHeaders).not.toHaveBeenCalled();
});
});
});
describe('#close', () => {
let mockOpenSearchClientInstance: { close: jest.Mock };
let mockScopedOpenSearchClientInstance: { close: jest.Mock };
let clusterClient: LegacyClusterClient;
beforeEach(() => {
mockOpenSearchClientInstance = { close: jest.fn() };
mockScopedOpenSearchClientInstance = { close: jest.fn() };
MockClient.mockImplementationOnce(() => mockOpenSearchClientInstance).mockImplementationOnce(
() => mockScopedOpenSearchClientInstance
);
clusterClient = new LegacyClusterClient(
{ apiVersion: 'opensearch-version', requestHeadersWhitelist: [] } as any,

...this.config.requestHeadersWhitelist,

requestHeadersWhitelist: [],
},
logger.get()
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "master",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "localhost",
"path": "/opensearch",
"port": "80",
"protocol": "http:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": false,
"sniffOnStart": false,
}
`);
});
test('parses fully specified config', () => {
const opensearchConfig: LegacyOpenSearchClientConfig = {
apiVersion: 'v7.0.0',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: [
'http://localhost/opensearch',
'http://domain.com:1234/opensearch',
'https://opensearch.local',
],
requestHeadersWhitelist: [],
username: 'opensearch',
password: 'changeme',
pingTimeout: 12345,
requestTimeout: 54321,
sniffInterval: 11223344,
ssl: {
verificationMode: 'certificate',
certificateAuthorities: ['content-of-ca-path-1', 'content-of-ca-path-2'],
certificate: 'content-of-certificate-path',
key: 'content-of-key-path',
keyPassphrase: 'key-pass',
alwaysPresentCertificate: true,
},
};
const opensearchClientConfig = parseOpenSearchClientConfig(opensearchConfig, logger.get());
// Check that original references aren't used.
for (const host of opensearchClientConfig.hosts) {
expect(opensearchConfig.customHeaders).not.toBe(host.headers);
}
expect(opensearchConfig.ssl).not.toBe(opensearchClientConfig.ssl);
expect(opensearchClientConfig).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"auth": "opensearch:changeme",
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "localhost",
"path": "/opensearch",
"port": "80",
"protocol": "http:",
"query": null,
},
Object {
"auth": "opensearch:changeme",
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "domain.com",
"path": "/opensearch",
"port": "1234",
"protocol": "http:",
"query": null,
},
Object {
"auth": "opensearch:changeme",
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"pingTimeout": 12345,
"requestTimeout": 54321,
"sniffInterval": 11223344,
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
"ca": Array [
"content-of-ca-path-1",
"content-of-ca-path-2",
],
"cert": "content-of-certificate-path",
"checkServerIdentity": [Function],
"key": "content-of-key-path",
"passphrase": "key-pass",
"rejectUnauthorized": true,
},
}
`);
});
test('parses config timeouts of moment.Duration type', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'master',
customHeaders: { xsrf: 'something' },
logQueries: false,
sniffOnStart: false,
sniffOnConnectionFault: false,
pingTimeout: duration(100, 'ms'),
requestTimeout: duration(30, 's'),
sniffInterval: duration(1, 'minute'),
hosts: ['http://localhost:9200/opensearch'],
requestHeadersWhitelist: [],
},
logger.get()
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "master",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "localhost",
"path": "/opensearch",
"port": "9200",
"protocol": "http:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"pingTimeout": 100,
"requestTimeout": 30000,
"sniffInterval": 60000,
"sniffOnConnectionFault": false,
"sniffOnStart": false,
}
`);
});
describe('#auth', () => {
test('is not set if #auth = false even if username and password are provided', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['http://user:password@localhost/opensearch', 'https://opensearch.local'],
username: 'opensearch',
password: 'changeme',
requestHeadersWhitelist: [],
},
logger.get(),
{ auth: false }
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "localhost",
"path": "/opensearch",
"port": "80",
"protocol": "http:",
"query": null,
},
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
}
`);
});
test('is not set if username is not specified', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
password: 'changeme',
},
logger.get(),
{ auth: true }
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
}
`);
});
test('is not set if password is not specified', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
username: 'opensearch',
},
logger.get(),
{ auth: true }
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
}
`);
});
});
describe('#customHeaders', () => {
test('override the default headers', () => {
const headerKey = Object.keys(DEFAULT_HEADERS)[0];
const parsedConfig = parseOpenSearchClientConfig(
{
apiVersion: 'master',
customHeaders: { [headerKey]: 'foo' },
logQueries: false,
sniffOnStart: false,
sniffOnConnectionFault: false,
hosts: ['http://localhost/opensearch'],
requestHeadersWhitelist: [],
},
logger.get()
);
expect(parsedConfig.hosts[0].headers).toEqual({
[headerKey]: 'foo',
});
});
});
describe('#log', () => {
test('default logger with #logQueries = false', () => {
const parsedConfig = parseOpenSearchClientConfig(
{
apiVersion: 'master',
customHeaders: { xsrf: 'something' },
logQueries: false,
sniffOnStart: false,
sniffOnConnectionFault: false,
hosts: ['http://localhost/opensearch'],
requestHeadersWhitelist: [],
},
logger.get()
);
const Logger = new parsedConfig.log();
Logger.error('some-error');
Logger.warning('some-warning');
Logger.trace('some-trace');
Logger.info('some-info');
Logger.debug('some-debug');
expect(typeof Logger.close).toBe('function');
expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(`
Object {
"debug": Array [],
"error": Array [
Array [
"some-error",
],
],
"fatal": Array [],
"info": Array [],
"log": Array [],
"trace": Array [],
"warn": Array [
Array [
"some-warning",
],
],
}
`);
});
test('default logger with #logQueries = true', () => {
const parsedConfig = parseOpenSearchClientConfig(
{
apiVersion: 'master',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: false,
sniffOnConnectionFault: false,
hosts: ['http://localhost/opensearch'],
requestHeadersWhitelist: [],
},
logger.get()
);
const Logger = new parsedConfig.log();
Logger.error('some-error');
Logger.warning('some-warning');
Logger.trace('METHOD', { path: '/some-path' }, '?query=2', 'unknown', '304');
Logger.info('some-info');
Logger.debug('some-debug');
expect(typeof Logger.close).toBe('function');
expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(`
Object {
"debug": Array [
Array [
"304
METHOD /some-path
?query=2",
Object {
"tags": Array [
"query",
],
},
],
],
"error": Array [
Array [
"some-error",
],
],
"fatal": Array [],
"info": Array [],
"log": Array [],
"trace": Array [],
"warn": Array [
Array [
"some-warning",
],
],
}
`);
});
test('custom logger', () => {
const customLogger = jest.fn();
const parsedConfig = parseOpenSearchClientConfig(
{
apiVersion: 'master',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: false,
sniffOnConnectionFault: false,
hosts: ['http://localhost/opensearch'],
requestHeadersWhitelist: [],
log: customLogger,
},
logger.get()
);
expect(parsedConfig.log).toBe(customLogger);
});
});
describe('#ssl', () => {
test('#verificationMode = none', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: {},
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
ssl: { verificationMode: 'none' },
},
logger.get()
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
"ca": undefined,
"rejectUnauthorized": false,
},
}
`);
});
test('#verificationMode = certificate', () => {
const clientConfig = parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: {},
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
ssl: { verificationMode: 'certificate' },
},
logger.get()
);
// `checkServerIdentity` shouldn't check hostname when verificationMode is certificate.
expect(
clientConfig.ssl!.checkServerIdentity!('right.com', { subject: { CN: 'wrong.com' } } as any)
).toBeUndefined();
expect(clientConfig).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
"ca": undefined,
"checkServerIdentity": [Function],
"rejectUnauthorized": true,
},
}
`);
});
test('#verificationMode = full', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: {},
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
ssl: { verificationMode: 'full' },
},
logger.get()
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
"ca": undefined,
"rejectUnauthorized": true,
},
}
`);
});
test('#verificationMode is unknown', () => {
expect(() =>
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: {},
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
ssl: { verificationMode: 'misspelled' as any },
},
logger.get()
)
).toThrowErrorMatchingInlineSnapshot(`"Unknown ssl verificationMode: misspelled"`);
});
test('#ignoreCertAndKey = true', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: {},
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],

requestHeadersWhitelist: string[];

const filteredHeaders = filterHeaders(headers, opensearchConfig.requestHeadersWhitelist);

opensearch.requestHeadersWhitelistConfigured config

requestHeadersWhitelistConfigured: boolean;

requestHeadersWhitelistConfigured: isConfigured.stringOrArray(

requestHeadersWhitelistConfigured: boolean;

server.compression.referrerWhitelist config

'--server.compression.referrerWhitelist=["some-host.com"]',

it(`uses compression when there is a whitelisted referer`, async () => {
await supertest
.get('/app/opensearch-dashboards')
.set('accept-encoding', 'gzip')
.set('referer', 'https://some-host.com')
.then((response) => {
expect(response.headers).to.have.property('content-encoding', 'gzip');
});
});
it(`doesn't use compression when there is a non-whitelisted referer`, async () => {

server.xsrf.whitelist config

xsrf: {
disableProtection: false,
whitelist: [],
},
},
});
const configAdapterWithDisabledSSL = new LegacyObjectToConfigAdapter({
server: {
name: 'opensearch-dashboards-hostname',
autoListen: true,
basePath: '/abc',
cors: false,
customResponseHeaders: { 'custom-header': 'custom-value' },
host: 'host',
maxPayloadBytes: 1000,
keepaliveTimeout: 5000,
socketTimeout: 2000,
port: 1234,
rewriteBasePath: false,
ssl: { enabled: false, certificate: 'cert', key: 'key' },
compression: { enabled: true },
someNotSupportedValue: 'val',
xsrf: {
disableProtection: false,
whitelist: [],
},

"whitelist": Array [],
},
}
`;
exports[`#get correctly handles server config.: disabled ssl 1`] = `
Object {
"autoListen": true,
"basePath": "/abc",
"compression": Object {
"enabled": true,
},
"cors": false,
"customResponseHeaders": Object {
"custom-header": "custom-value",
},
"host": "host",
"keepaliveTimeout": 5000,
"maxPayload": 1000,
"name": "opensearch-dashboards-hostname",
"port": 1234,
"rewriteBasePath": false,
"socketTimeout": 2000,
"ssl": Object {
"certificate": "cert",
"enabled": false,
"key": "key",
},
"uuid": undefined,
"xsrf": Object {
"disableProtection": false,
"whitelist": Array [],

it('logs a warning if server.xsrf.whitelist is set', () => {
const { messages } = applyCoreDeprecations({
server: { xsrf: { whitelist: ['/path'] } },
});
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"server.xsrf.whitelist\\" is deprecated and has been replaced by \\"server.xsrf.allowlist\\"",
"It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. Instead, supply the \\"osd-xsrf\\" header.",

if ((settings.server?.xsrf?.whitelist ?? []).length > 0) {
log(
'It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. ' +
'Instead, supply the "osd-xsrf" header.'
);
}
return settings;
};
const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) {
log(
'You should set server.basePath along with server.rewriteBasePath. OpenSearch Dashboards ' +
'will expect that all requests start with server.basePath rather than expecting you to rewrite ' +
'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' +
'current behavior and silence this warning.'
);
}
return settings;
};
const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
const NONCE_STRING = `{nonce}`;
// Policies that should include the 'self' source
const SELF_POLICIES = Object.freeze(['script-src', 'style-src']);
const SELF_STRING = `'self'`;
const rules: string[] = get(settings, 'csp.rules');
if (rules) {
const parsed = new Map(
rules.map((ruleStr) => {
const parts = ruleStr.split(/\s+/);
return [parts[0], parts.slice(1)];
})
);
settings.csp.rules = [...parsed].map(([policy, sourceList]) => {
if (sourceList.find((source) => source.includes(NONCE_STRING))) {
log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`);
sourceList = sourceList.filter((source) => !source.includes(NONCE_STRING));
// Add 'self' if not present
if (!sourceList.find((source) => source.includes(SELF_STRING))) {
sourceList.push(SELF_STRING);
}
}
if (
SELF_POLICIES.includes(policy) &&
!sourceList.find((source) => source.includes(SELF_STRING))
) {
log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`);
sourceList.push(SELF_STRING);
}
return `${policy} ${sourceList.join(' ')}`.trim();
});
}
return settings;
};
const mapManifestServiceUrlDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
if (has(settings, 'map.manifestServiceUrl')) {
log(
'You should no longer use the map.manifestServiceUrl setting in opensearch_dashboards.yml to configure the location ' +
'of the Maps Service settings. These settings have moved to the "map.emsTileApiUrl" and ' +
'"map.emsFileApiUrl" settings instead. These settings are for development use only and should not be ' +
'modified for use in production environments.'
);
}
return settings;
};
export const coreDeprecationProvider: ConfigDeprecationProvider = ({
unusedFromRoot,
renameFromRoot,
renameFromRootWithoutMap,
}) => [
unusedFromRoot('savedObjects.indexCheckTimeout'),
unusedFromRoot('server.xsrf.token'),
unusedFromRoot('maps.manifestServiceUrl'),
unusedFromRoot('optimize.lazy'),
unusedFromRoot('optimize.lazyPort'),
unusedFromRoot('optimize.lazyHost'),
unusedFromRoot('optimize.lazyPrebuild'),
unusedFromRoot('optimize.lazyProxyTimeout'),
unusedFromRoot('optimize.enabled'),
unusedFromRoot('optimize.bundleFilter'),
unusedFromRoot('optimize.bundleDir'),
unusedFromRoot('optimize.viewCaching'),
unusedFromRoot('optimize.watch'),
unusedFromRoot('optimize.watchPort'),
unusedFromRoot('optimize.watchHost'),
unusedFromRoot('optimize.watchPrebuild'),
unusedFromRoot('optimize.watchProxyTimeout'),
unusedFromRoot('optimize.useBundleCache'),
unusedFromRoot('optimize.sourceMaps'),
unusedFromRoot('optimize.workers'),
unusedFromRoot('optimize.profile'),
unusedFromRoot('optimize.validateSyntaxOfNodeModules'),
renameFromRoot('cpu.cgroup.path.override', 'ops.cGroupOverrides.cpuPath'),
renameFromRoot('cpuacct.cgroup.path.override', 'ops.cGroupOverrides.cpuAcctPath'),
unusedFromRoot('opensearch.preserveHost'),
unusedFromRoot('opensearch.startupTimeout'),
renameFromRootWithoutMap('server.xsrf.whitelist', 'server.xsrf.allowlist'),

Other code

Comments/variables

test('excludes references and migrationVersion which are part of the blacklist', () => {

a security tool for macOS that monitors process executions and can blacklist/whitelist binaries. \

test('enables compression for whitelisted referer', async () => {
const response = await supertest(listener)
.get('/')
.set('accept-encoding', 'gzip')
.set('referer', 'http://foo:1234');
expect(response.header).toHaveProperty('content-encoding', 'gzip');
});
test('disables compression for non-whitelisted referer', async () => {

export const LICENSE_WHITELIST = [

export const DEV_ONLY_LICENSE_WHITELIST = ['MPL-2.0'];

export { LICENSE_WHITELIST, DEV_ONLY_LICENSE_WHITELIST, LICENSE_OVERRIDES } from './config';

import { LICENSE_WHITELIST, DEV_ONLY_LICENSE_WHITELIST, LICENSE_OVERRIDES } from './config';
import { assertLicensesValid } from './valid';
run(
async ({ log, flags }) => {
const packages = await getInstalledPackages({
directory: REPO_ROOT,
licenseOverrides: LICENSE_OVERRIDES,
includeDev: !!flags.dev,
});
// Assert if the found licenses in the production
// packages are valid
assertLicensesValid({
packages: packages.filter((pkg) => !pkg.isDevOnly),
validLicenses: LICENSE_WHITELIST,
});
log.success('All production dependency licenses are allowed');
// Do the same as above for the packages only used in development
// if the dev flag is found
if (flags.dev) {
assertLicensesValid({
packages: packages.filter((pkg) => pkg.isDevOnly),
validLicenses: LICENSE_WHITELIST.concat(DEV_ONLY_LICENSE_WHITELIST),

const whiteListedRules = ['backticks', 'emphasis', 'link', 'list'];
export function Content({ text }) {
return (
<Markdown
className="euiText"
markdown={text}
openLinksInNewTab={true}
whiteListedRules={whiteListedRules}

a security tool for macOS that monitors process executions and can blacklist/whitelist binaries. \

test('whiteListedRules', () => {
const component = shallow(
<Markdown markdown={markdown} whiteListedRules={['backticks', 'emphasis']} />
);
expect(component).toMatchSnapshot();
});
test('should update markdown when openLinksInNewTab prop change', () => {
const component = shallow(<Markdown markdown={markdown} openLinksInNewTab={false} />);
expect(component.render().find('a').prop('target')).not.toBe('_blank');
component.setProps({ openLinksInNewTab: true });
expect(component.render().find('a').prop('target')).toBe('_blank');
});
test('should update markdown when whiteListedRules prop change', () => {
const md = '*emphasis* `backticks`';
const component = shallow(
<Markdown markdown={md} whiteListedRules={['emphasis', 'backticks']} />
);
expect(component.render().find('em')).toHaveLength(1);
expect(component.render().find('code')).toHaveLength(1);
component.setProps({ whiteListedRules: ['backticks'] });

* whiteListedRules and openLinksInNewTab configurations.
* @param {Array of Strings} whiteListedRules - white list of markdown rules
* list of rules can be found at https://github.com/markdown-it/markdown-it/issues/361
* @param {Boolean} openLinksInNewTab
* @return {Function} Returns an Object to use with dangerouslySetInnerHTML
* with the rendered markdown HTML
*/
export const markdownFactory = memoize(
(whiteListedRules: string[] = [], openLinksInNewTab: boolean = false) => {
let markdownIt: MarkdownIt;
// It is imperative that the html config property be set to false, to mitigate XSS: the output of markdown-it is
// fed directly to the DOM via React's dangerouslySetInnerHTML below.
if (whiteListedRules && whiteListedRules.length > 0) {
markdownIt = new MarkdownIt('zero', { html: false, linkify: true });
markdownIt.enable(whiteListedRules);
} else {
markdownIt = new MarkdownIt({ html: false, linkify: true });
}
if (openLinksInNewTab) {
// All links should open in new browser tab.
// Define custom renderer to add 'target' attribute
// https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
const originalLinkRender =
markdownIt.renderer.rules.link_open ||
function (tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
markdownIt.renderer.rules.link_open = function (tokens, idx, options, env, self) {
const href = tokens[idx].attrGet('href');
const target = '_blank';
const rel = getSecureRelForTarget({ href: href === null ? undefined : href, target });
// https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/
tokens[idx].attrPush(['target', target]);
if (rel) {
tokens[idx].attrPush(['rel', rel]);
}
return originalLinkRender(tokens, idx, options, env, self);
};
}
/**
* This method is used to render markdown from the passed parameter
* into HTML. It will just return an empty string when the markdown is empty.
* @param {String} markdown - The markdown String
* @return {String} - Returns the rendered HTML as string.
*/
return (markdown: string) => {
return markdown ? markdownIt.render(markdown) : '';
};
},
(whiteListedRules: string[] = [], openLinksInNewTab: boolean = false) => {
return `${whiteListedRules.join('_')}${openLinksInNewTab}`;
}
);
export interface MarkdownProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
markdown?: string;
openLinksInNewTab?: boolean;
whiteListedRules?: string[];
}
export class Markdown extends PureComponent<MarkdownProps> {
render() {
const { className, markdown = '', openLinksInNewTab, whiteListedRules, ...rest } = this.props;
const classes = classNames('osdMarkdown__body', className);
const markdownRenderer = markdownFactory(whiteListedRules, openLinksInNewTab);

WHITE_LISTED_GROUP_BY_FIELDS = 'whiteListedGroupByFields',
/**
* Key for getting the white listed metrics from the UIRestrictions object.
*/
WHITE_LISTED_METRICS = 'whiteListedMetrics',
/**
* Key for getting the white listed Time Range modes from the UIRestrictions object.
*/
WHITE_LISTED_TIMERANGE_MODES = 'whiteListedTimerangeModes',

get whiteListedMetrics() {
return this.createUiRestriction();
}
get whiteListedGroupByFields() {
return this.createUiRestriction();
}
get whiteListedTimerangeModes() {
return this.createUiRestriction();
}
get uiRestrictions() {
return {
[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: this.whiteListedMetrics,
[RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: this.whiteListedGroupByFields,
[RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES]: this.whiteListedTimerangeModes,

whiteListedMetrics: { '*': true },
whiteListedGroupByFields: { '*': true },
whiteListedTimerangeModes: { '*': true },

@boktorbb
Copy link
Contributor

Resolved in #1467

@tmarkley
Copy link
Contributor

tmarkley commented Apr 20, 2022

Instances of blacklist and whitelist

APIs, configuration

http.compression.referrerWhitelist config

test('accepts valid referrer whitelist', () => {
const {
compression: { referrerWhitelist },
} = config.schema.validate({
compression: {
referrerWhitelist: validHostnames,
},
});
expect(referrerWhitelist).toMatchSnapshot();
});
test('throws if invalid referrer whitelist', () => {
const httpSchema = config.schema;
const invalidHostnames = {
compression: {
referrerWhitelist: [invalidHostname],
},
};
const emptyArray = {
compression: {
referrerWhitelist: [],
},
};
expect(() => httpSchema.validate(invalidHostnames)).toThrowErrorMatchingSnapshot();
expect(() => httpSchema.validate(emptyArray)).toThrowErrorMatchingSnapshot();
});
test('throws if referrer whitelist is specified and compression is disabled', () => {
const httpSchema = config.schema;
const obj = {
compression: {
enabled: false,
referrerWhitelist: validHostnames,

referrerWhitelist: schema.maybe(

if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) {
return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false';

describe('with defined `compression.referrerWhitelist`', () => {
let listener: Server;
beforeEach(async () => {
listener = await setupServer({
...config,
compression: { enabled: true, referrerWhitelist: ['foo'] },

const { enabled, referrerWhitelist: list } = config.compression;

exports[`with compression accepts valid referrer whitelist 1`] = `
Array [
"www.example.com",
"8.8.8.8",
"::1",
"localhost",
]
`;
exports[`with compression throws if invalid referrer whitelist 1`] = `"[compression.referrerWhitelist.0]: value must be a valid hostname (see RFC 1123)."`;
exports[`with compression throws if invalid referrer whitelist 2`] = `"[compression.referrerWhitelist]: array size is [0], but cannot be smaller than [1]"`;
exports[`with compression throws if referrer whitelist is specified and compression is disabled 1`] = `"cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false"`;

http.compression.referrerWhitelistConfigured config

referrerWhitelistConfigured: isConfigured.array(http.compression.referrerWhitelist),

referrerWhitelistConfigured: boolean;

http.xsrf.whitelist config

whitelistConfigured: isConfigured.array(http.xsrf.whitelist),

test('throws if xsrf.whitelist element does not start with a slash', () => {
const httpSchema = config.schema;
const obj = {
xsrf: {
whitelist: ['/valid-path', 'invalid-path'],
},
};
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
`"[xsrf.whitelist.1]: must start with a slash"`

public xsrf: { disableProtection: boolean; whitelist: string[] };

const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'get', headers: {} });
toolkit.next.mockReturnValue('next' as any);
const result = handler(request, responseFactory, toolkit);
expect(responseFactory.badRequest).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
});
describe('destructive methods', () => {
it('accepts requests with xsrf header', () => {
const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'post', headers: { 'osd-xsrf': 'xsrf' } });
toolkit.next.mockReturnValue('next' as any);
const result = handler(request, responseFactory, toolkit);
expect(responseFactory.badRequest).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
it('accepts requests with version header', () => {
const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'post', headers: { 'osd-version': 'some-version' } });
toolkit.next.mockReturnValue('next' as any);
const result = handler(request, responseFactory, toolkit);
expect(responseFactory.badRequest).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
it('returns a bad request if called without xsrf or version header', () => {
const config = createConfig({ xsrf: { whitelist: [], disableProtection: false } });
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'post' });
responseFactory.badRequest.mockReturnValue('badRequest' as any);
const result = handler(request, responseFactory, toolkit);
expect(toolkit.next).not.toHaveBeenCalled();
expect(responseFactory.badRequest).toHaveBeenCalledTimes(1);
expect(responseFactory.badRequest.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"body": "Request must contain a osd-xsrf header.",
}
`);
expect(result).toEqual('badRequest');
});
it('accepts requests if protection is disabled', () => {
const config = createConfig({ xsrf: { whitelist: [], disableProtection: true } });
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'post', headers: {} });
toolkit.next.mockReturnValue('next' as any);
const result = handler(request, responseFactory, toolkit);
expect(responseFactory.badRequest).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
it('accepts requests if path is whitelisted', () => {
const config = createConfig({
xsrf: { whitelist: ['/some-path'], disableProtection: false },
});
const handler = createXsrfPostAuthHandler(config);
const request = forgeRequest({ method: 'post', headers: {}, path: '/some-path' });
toolkit.next.mockReturnValue('next' as any);
const result = handler(request, responseFactory, toolkit);
expect(responseFactory.badRequest).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
expect(result).toEqual('next');
});
it('accepts requests if xsrf protection on a route is disabled', () => {
const config = createConfig({
xsrf: { whitelist: [], disableProtection: false },

const { whitelist, disableProtection } = config.xsrf;
return (request, response, toolkit) => {
if (
disableProtection ||
whitelist.includes(request.route.path) ||

const whitelistedTestPath = '/xsrf/test/route/whitelisted';
const xsrfDisabledTestPath = '/xsrf/test/route/disabled';
const opensearchDashboardsName = 'my-opensearch-dashboards-name';
const setupDeps = {
context: contextServiceMock.createSetupContract(),
};
describe('core lifecycle handlers', () => {
let server: HttpService;
let innerServer: HttpServerSetup['server'];
let router: IRouter;
beforeEach(async () => {
const configService = configServiceMock.create();
configService.atPath.mockReturnValue(
new BehaviorSubject({
hosts: ['localhost'],
maxPayload: new ByteSizeValue(1024),
autoListen: true,
ssl: {
enabled: false,
},
compression: { enabled: true },
name: opensearchDashboardsName,
customResponseHeaders: {
'some-header': 'some-value',
},
xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] },
requestId: {
allowFromAnyIp: true,
ipAllowlist: [],
},
} as any)
);
server = createHttpServer({ configService });
const serverSetup = await server.setup(setupDeps);
router = serverSetup.createRouter('/');
innerServer = serverSetup.server;
}, 30000);
afterEach(async () => {
await server.stop();
});
describe('versionCheck post-auth handler', () => {
const testRoute = '/version_check/test/route';
beforeEach(async () => {
router.get({ path: testRoute, validate: false }, (context, req, res) => {
return res.ok({ body: 'ok' });
});
await server.start();
});
it('accepts requests with the correct version passed in the version header', async () => {
await supertest(innerServer.listener)
.get(testRoute)
.set(versionHeader, actualVersion)
.expect(200, 'ok');
});
it('accepts requests that do not include a version header', async () => {
await supertest(innerServer.listener).get(testRoute).expect(200, 'ok');
});
it('rejects requests with an incorrect version passed in the version header', async () => {
await supertest(innerServer.listener)
.get(testRoute)
.set(versionHeader, 'invalid-version')
.expect(400, /Browser client is out of date/);
});
});
describe('customHeaders pre-response handler', () => {
const testRoute = '/custom_headers/test/route';
const testErrorRoute = '/custom_headers/test/error_route';
beforeEach(async () => {
router.get({ path: testRoute, validate: false }, (context, req, res) => {
return res.ok({ body: 'ok' });
});
router.get({ path: testErrorRoute, validate: false }, (context, req, res) => {
return res.badRequest({ body: 'bad request' });
});
await server.start();
});
it('adds the osd-name header', async () => {
const result = await supertest(innerServer.listener).get(testRoute).expect(200, 'ok');
const headers = result.header as Record<string, string>;
expect(headers).toEqual(
expect.objectContaining({
[nameHeader]: opensearchDashboardsName,
})
);
});
it('adds the osd-name header in case of error', async () => {
const result = await supertest(innerServer.listener).get(testErrorRoute).expect(400);
const headers = result.header as Record<string, string>;
expect(headers).toEqual(
expect.objectContaining({
[nameHeader]: opensearchDashboardsName,
})
);
});
it('adds the custom headers', async () => {
const result = await supertest(innerServer.listener).get(testRoute).expect(200, 'ok');
const headers = result.header as Record<string, string>;
expect(headers).toEqual(expect.objectContaining({ 'some-header': 'some-value' }));
});
it('adds the custom headers in case of error', async () => {
const result = await supertest(innerServer.listener).get(testErrorRoute).expect(400);
const headers = result.header as Record<string, string>;
expect(headers).toEqual(expect.objectContaining({ 'some-header': 'some-value' }));
});
});
describe('xsrf post-auth handler', () => {
const testPath = '/xsrf/test/route';
const destructiveMethods = ['POST', 'PUT', 'DELETE'];
const nonDestructiveMethods = ['GET', 'HEAD'];
const getSupertest = (method: string, path: string): supertest.Test => {
return (supertest(innerServer.listener) as any)[method.toLowerCase()](path) as supertest.Test;
};
beforeEach(async () => {
router.get({ path: testPath, validate: false }, (context, req, res) => {
return res.ok({ body: 'ok' });
});
destructiveMethods.forEach((method) => {
((router as any)[method.toLowerCase()] as RouteRegistrar<any>)<any, any, any>(
{ path: testPath, validate: false },
(context, req, res) => {
return res.ok({ body: 'ok' });
}
);
((router as any)[method.toLowerCase()] as RouteRegistrar<any>)<any, any, any>(
{ path: whitelistedTestPath, validate: false },
(context, req, res) => {
return res.ok({ body: 'ok' });
}
);
((router as any)[method.toLowerCase()] as RouteRegistrar<any>)<any, any, any>(
{ path: xsrfDisabledTestPath, validate: false, options: { xsrfRequired: false } },
(context, req, res) => {
return res.ok({ body: 'ok' });
}
);
});
await server.start();
});
nonDestructiveMethods.forEach((method) => {
describe(`When using non-destructive ${method} method`, () => {
it('accepts requests without a token', async () => {
await getSupertest(method.toLowerCase(), testPath).expect(
200,
method === 'HEAD' ? undefined : 'ok'
);
});
it('accepts requests with the xsrf header', async () => {
await getSupertest(method.toLowerCase(), testPath)
.set(xsrfHeader, 'anything')
.expect(200, method === 'HEAD' ? undefined : 'ok');
});
});
});
destructiveMethods.forEach((method) => {
describe(`When using destructive ${method} method`, () => {
it('accepts requests with the xsrf header', async () => {
await getSupertest(method.toLowerCase(), testPath)
.set(xsrfHeader, 'anything')
.expect(200, 'ok');
});
it('accepts requests with the version header', async () => {
await getSupertest(method.toLowerCase(), testPath)
.set(versionHeader, actualVersion)
.expect(200, 'ok');
});
it('rejects requests without either an xsrf or version header', async () => {
await getSupertest(method.toLowerCase(), testPath).expect(400, {
statusCode: 400,
error: 'Bad Request',
message: 'Request must contain a osd-xsrf header.',
});
});
it('accepts whitelisted requests without either an xsrf or version header', async () => {
await getSupertest(method.toLowerCase(), whitelistedTestPath).expect(200, 'ok');

http.xsrf.whitelistConfigured config

whitelistConfigured: boolean;

whitelistConfigured: isConfigured.array(http.xsrf.whitelist),

whitelistConfigured: isConfigured.array(http.xsrf.whitelist),

opensearch.requestHeadersWhitelist config

#opensearch.requestHeadersWhitelist: [ authorization ]
# Header names and values that are sent to OpenSearch. Any custom headers cannot be overwritten
# by client-side headers, regardless of the opensearch.requestHeadersWhitelist configuration.

requestHeadersWhitelist: ["${OSD_ENV_VAR1}", "${OSD_ENV_VAR2}"]

requestHeadersWhitelist: ["${OSD_ENV_VAR1}", "${OSD_ENV_VAR2}"]

requestHeadersWhitelist: Type<string | string[]>;

referrerWhitelistConfigured: boolean;

export type LegacyOpenSearchClientConfig = Pick<ConfigOptions, 'keepAlive' | 'log' | 'plugins'> & Pick<OpenSearchConfig, 'apiVersion' | 'customHeaders' | 'logQueries' | 'requestHeadersWhitelist' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'hosts' | 'username' | 'password'> & {

export type OpenSearchClientConfig = Pick<OpenSearchConfig, 'customHeaders' | 'logQueries' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'requestHeadersWhitelist' | 'sniffInterval' | 'hosts' | 'username' | 'password'> & {

readonly requestHeadersWhitelist: string[];

"requestHeadersWhitelist": Array [
"authorization",
],
"requestTimeout": "PT30S",
"shardTimeout": "PT30S",
"sniffInterval": false,
"sniffOnConnectionFault": false,
"sniffOnStart": false,
"ssl": Object {
"alwaysPresentCertificate": false,
"certificate": undefined,
"certificateAuthorities": undefined,
"key": undefined,
"keyPassphrase": undefined,
"verificationMode": "full",
},
"username": undefined,
}
`);
});
test('#hosts accepts both string and array of strings', () => {
let configValue = new OpenSearchConfig(
config.schema.validate({ hosts: 'http://some.host:1234' })
);
expect(configValue.hosts).toEqual(['http://some.host:1234']);
configValue = new OpenSearchConfig(config.schema.validate({ hosts: ['http://some.host:1234'] }));
expect(configValue.hosts).toEqual(['http://some.host:1234']);
configValue = new OpenSearchConfig(
config.schema.validate({
hosts: ['http://some.host:1234', 'https://some.another.host'],
})
);
expect(configValue.hosts).toEqual(['http://some.host:1234', 'https://some.another.host']);
});
test('#requestHeadersWhitelist accepts both string and array of strings', () => {
let configValue = new OpenSearchConfig(
config.schema.validate({ requestHeadersWhitelist: 'token' })
);
expect(configValue.requestHeadersWhitelist).toEqual(['token']);
configValue = new OpenSearchConfig(
config.schema.validate({ requestHeadersWhitelist: ['token'] })
);
expect(configValue.requestHeadersWhitelist).toEqual(['token']);
configValue = new OpenSearchConfig(
config.schema.validate({
requestHeadersWhitelist: ['token', 'X-Forwarded-Proto'],
})
);
expect(configValue.requestHeadersWhitelist).toEqual(['token', 'X-Forwarded-Proto']);
});
describe('reads files', () => {
beforeEach(() => {
mockReadFileSync.mockReset();
mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
mockReadPkcs12Keystore.mockReset();
mockReadPkcs12Keystore.mockImplementation((path: string) => ({
key: `content-of-${path}.key`,
cert: `content-of-${path}.cert`,
ca: [`content-of-${path}.ca`],
}));
mockReadPkcs12Truststore.mockReset();
mockReadPkcs12Truststore.mockImplementation((path: string) => [`content-of-${path}`]);
});
it('reads certificate authorities when ssl.keystore.path is specified', () => {
const configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { keystore: { path: 'some-path' } } })
);
expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path.ca']);
});
it('reads certificate authorities when ssl.truststore.path is specified', () => {
const configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { truststore: { path: 'some-path' } } })
);
expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);
});
it('reads certificate authorities when ssl.certificateAuthorities is specified', () => {
let configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { certificateAuthorities: 'some-path' } })
);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);
mockReadFileSync.mockClear();
configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { certificateAuthorities: ['some-path'] } })
);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual(['content-of-some-path']);
mockReadFileSync.mockClear();
configValue = new OpenSearchConfig(
config.schema.validate({
ssl: { certificateAuthorities: ['some-path', 'another-path'] },
})
);
expect(mockReadFileSync).toHaveBeenCalledTimes(2);
expect(configValue.ssl.certificateAuthorities).toEqual([
'content-of-some-path',
'content-of-another-path',
]);
});
it('reads certificate authorities when ssl.keystore.path, ssl.truststore.path, and ssl.certificateAuthorities are specified', () => {
const configValue = new OpenSearchConfig(
config.schema.validate({
ssl: {
keystore: { path: 'some-path' },
truststore: { path: 'another-path' },
certificateAuthorities: 'yet-another-path',
},
})
);
expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
expect(mockReadPkcs12Truststore).toHaveBeenCalledTimes(1);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificateAuthorities).toEqual([
'content-of-some-path.ca',
'content-of-another-path',
'content-of-yet-another-path',
]);
});
it('reads a private key and certificate when ssl.keystore.path is specified', () => {
const configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { keystore: { path: 'some-path' } } })
);
expect(mockReadPkcs12Keystore).toHaveBeenCalledTimes(1);
expect(configValue.ssl.key).toEqual('content-of-some-path.key');
expect(configValue.ssl.certificate).toEqual('content-of-some-path.cert');
});
it('reads a private key when ssl.key is specified', () => {
const configValue = new OpenSearchConfig(config.schema.validate({ ssl: { key: 'some-path' } }));
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.key).toEqual('content-of-some-path');
});
it('reads a certificate when ssl.certificate is specified', () => {
const configValue = new OpenSearchConfig(
config.schema.validate({ ssl: { certificate: 'some-path' } })
);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
expect(configValue.ssl.certificate).toEqual('content-of-some-path');
});
});
describe('throws when config is invalid', () => {
beforeAll(() => {
const realFs = jest.requireActual('fs');
mockReadFileSync.mockImplementation((path: string) => realFs.readFileSync(path));
const utils = jest.requireActual('../utils');
mockReadPkcs12Keystore.mockImplementation((path: string, password?: string) =>
utils.readPkcs12Keystore(path, password)
);
mockReadPkcs12Truststore.mockImplementation((path: string, password?: string) =>
utils.readPkcs12Truststore(path, password)
);
});
it('throws if key is invalid', () => {
const value = { ssl: { key: '/invalid/key' } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/key'"`
);
});
it('throws if certificate is invalid', () => {
const value = { ssl: { certificate: '/invalid/cert' } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/cert'"`
);
});
it('throws if certificateAuthorities is invalid', () => {
const value = { ssl: { certificateAuthorities: '/invalid/ca' } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(`"ENOENT: no such file or directory, open '/invalid/ca'"`);
});
it('throws if keystore path is invalid', () => {
const value = { ssl: { keystore: { path: '/invalid/keystore' } } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/keystore'"`
);
});
it('throws if keystore does not contain a key', () => {
mockReadPkcs12Keystore.mockReturnValueOnce({});
const value = { ssl: { keystore: { path: 'some-path' } } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(`"Did not find key in OpenSearch keystore."`);
});
it('throws if keystore does not contain a certificate', () => {
mockReadPkcs12Keystore.mockReturnValueOnce({ key: 'foo' });
const value = { ssl: { keystore: { path: 'some-path' } } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(`"Did not find certificate in OpenSearch keystore."`);
});
it('throws if truststore path is invalid', () => {
const value = { ssl: { keystore: { path: '/invalid/truststore' } } };
expect(
() => new OpenSearchConfig(config.schema.validate(value))
).toThrowErrorMatchingInlineSnapshot(
`"ENOENT: no such file or directory, open '/invalid/truststore'"`
);
});
it('throws if key and keystore.path are both specified', () => {
const value = { ssl: { key: 'foo', keystore: { path: 'bar' } } };
expect(() => config.schema.validate(value)).toThrowErrorMatchingInlineSnapshot(
`"[ssl]: cannot use [key] when [keystore.path] is specified"`
);
});
it('throws if certificate and keystore.path are both specified', () => {
const value = { ssl: { certificate: 'foo', keystore: { path: 'bar' } } };
expect(() => config.schema.validate(value)).toThrowErrorMatchingInlineSnapshot(
`"[ssl]: cannot use [certificate] when [keystore.path] is specified"`
);
});
});
describe('deprecations', () => {
it('logs a warning if opensearch.username is set to "elastic"', () => {
const { messages } = applyOpenSearchDeprecations({ username: 'elastic' });
expect(messages).toMatchInlineSnapshot(`
Array [
"Setting [opensearch.username] to \\"elastic\\" is deprecated. You should use the \\"opensearch_dashboards_system\\" user instead.",
]
`);
});
it('logs a warning if opensearch.username is set to "opensearchDashboards"', () => {
const { messages } = applyOpenSearchDeprecations({ username: 'opensearchDashboards' });
expect(messages).toMatchInlineSnapshot(`
Array [
"Setting [opensearch.username] to \\"opensearchDashboards\\" is deprecated. You should use the \\"opensearch_dashboards_system\\" user instead.",
]
`);
});
it('does not log a warning if opensearch.username is set to something besides "elastic" or "opensearchDashboards"', () => {
const { messages } = applyOpenSearchDeprecations({ username: 'otheruser' });
expect(messages).toHaveLength(0);
});
it('does not log a warning if opensearch.username is unset', () => {
const { messages } = applyOpenSearchDeprecations({});
expect(messages).toHaveLength(0);
});
it('logs a warning if ssl.key is set and ssl.certificate is not', () => {
const { messages } = applyOpenSearchDeprecations({ ssl: { key: '' } });
expect(messages).toMatchInlineSnapshot(`
Array [
"Setting [opensearch.ssl.key] without [opensearch.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to OpenSearch.",
]
`);
});
it('logs a warning if ssl.certificate is set and ssl.key is not', () => {
const { messages } = applyOpenSearchDeprecations({ ssl: { certificate: '' } });
expect(messages).toMatchInlineSnapshot(`
Array [
"Setting [opensearch.ssl.certificate] without [opensearch.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to OpenSearch.",
]
`);
});
it('does not log a warning if both ssl.key and ssl.certificate are set', () => {
const { messages } = applyOpenSearchDeprecations({ ssl: { key: '', certificate: '' } });
expect(messages).toEqual([]);
});
it('logs a warning if elasticsearch.sniffOnStart is set and opensearch.sniffOnStart is not', () => {
const { messages } = applyLegacyDeprecations({ sniffOnStart: true });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.sniffOnStart\\" is deprecated and has been replaced by \\"opensearch.sniffOnStart\\"",
]
`);
});
it('logs a warning if elasticsearch.sniffInterval is set and opensearch.sniffInterval is not', () => {
const { messages } = applyLegacyDeprecations({ sniffInterval: true });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.sniffInterval\\" is deprecated and has been replaced by \\"opensearch.sniffInterval\\"",
]
`);
});
it('logs a warning if elasticsearch.sniffOnConnectionFault is set and opensearch.sniffOnConnectionFault is not', () => {
const { messages } = applyLegacyDeprecations({ sniffOnConnectionFault: true });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.sniffOnConnectionFault\\" is deprecated and has been replaced by \\"opensearch.sniffOnConnectionFault\\"",
]
`);
});
it('logs a warning if elasticsearch.hosts is set and opensearch.hosts is not', () => {
const { messages } = applyLegacyDeprecations({ hosts: [''] });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.hosts\\" is deprecated and has been replaced by \\"opensearch.hosts\\"",
]
`);
});
it('logs a warning if elasticsearch.username is set and opensearch.username is not', () => {
const { messages } = applyLegacyDeprecations({ username: '' });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.username\\" is deprecated and has been replaced by \\"opensearch.username\\"",
]
`);
});
it('logs a warning if elasticsearch.password is set and opensearch.password is not', () => {
const { messages } = applyLegacyDeprecations({ password: '' });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.password\\" is deprecated and has been replaced by \\"opensearch.password\\"",
]
`);
});
it('logs a warning if elasticsearch.requestHeadersWhitelist is set and opensearch.requestHeadersWhitelist is not', () => {
const { messages } = applyLegacyDeprecations({ requestHeadersWhitelist: [''] });
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"elasticsearch.requestHeadersWhitelist\\" is deprecated and has been replaced by \\"opensearch.requestHeadersWhitelist\\"",
"\\"opensearch.requestHeadersWhitelist\\" is deprecated and has been replaced by \\"opensearch.requestHeadersAllowlist\\"",

requestHeadersWhitelist: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], {
defaultValue: ['authorization'],
}),
memoryCircuitBreaker: schema.object({
enabled: schema.boolean({ defaultValue: false }),
maxPercentage: schema.number({ defaultValue: 1.0 }),
}),
customHeaders: schema.recordOf(schema.string(), schema.string(), { defaultValue: {} }),
shardTimeout: schema.duration({ defaultValue: '30s' }),
requestTimeout: schema.duration({ defaultValue: '30s' }),
pingTimeout: schema.duration({ defaultValue: schema.siblingRef('requestTimeout') }),
logQueries: schema.boolean({ defaultValue: false }),
optimizedHealthcheckId: schema.maybe(schema.string()),
ssl: schema.object(
{
verificationMode: schema.oneOf(
[schema.literal('none'), schema.literal('certificate'), schema.literal('full')],
{ defaultValue: 'full' }
),
certificateAuthorities: schema.maybe(
schema.oneOf([schema.string(), schema.arrayOf(schema.string(), { minSize: 1 })])
),
certificate: schema.maybe(schema.string()),
key: schema.maybe(schema.string()),
keyPassphrase: schema.maybe(schema.string()),
keystore: schema.object({
path: schema.maybe(schema.string()),
password: schema.maybe(schema.string()),
}),
truststore: schema.object({
path: schema.maybe(schema.string()),
password: schema.maybe(schema.string()),
}),
alwaysPresentCertificate: schema.boolean({ defaultValue: false }),
},
{
validate: (rawConfig) => {
if (rawConfig.key && rawConfig.keystore.path) {
return 'cannot use [key] when [keystore.path] is specified';
}
if (rawConfig.certificate && rawConfig.keystore.path) {
return 'cannot use [certificate] when [keystore.path] is specified';
}
},
}
),
apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }),
healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }),
ignoreVersionMismatch: schema.conditional(
schema.contextRef('dev'),
false,
schema.boolean({
validate: (rawValue) => {
if (rawValue === true) {
return '"ignoreVersionMismatch" can only be set to true in development mode';
}
},
defaultValue: false,
}),
schema.boolean({ defaultValue: false })
),
});
const deprecations: ConfigDeprecationProvider = ({ renameFromRoot, renameFromRootWithoutMap }) => [
renameFromRoot('elasticsearch.sniffOnStart', 'opensearch.sniffOnStart'),
renameFromRoot('elasticsearch.sniffInterval', 'opensearch.sniffInterval'),
renameFromRoot('elasticsearch.sniffOnConnectionFault', 'opensearch.sniffOnConnectionFault'),
renameFromRoot('elasticsearch.hosts', 'opensearch.hosts'),
renameFromRoot('elasticsearch.username', 'opensearch.username'),
renameFromRoot('elasticsearch.password', 'opensearch.password'),
renameFromRoot('elasticsearch.requestHeadersWhitelist', 'opensearch.requestHeadersWhitelist'),
renameFromRootWithoutMap(
'opensearch.requestHeadersWhitelist',
'opensearch.requestHeadersAllowlist'
),
renameFromRoot('elasticsearch.customHeaders', 'opensearch.customHeaders'),
renameFromRoot('elasticsearch.shardTimeout', 'opensearch.shardTimeout'),
renameFromRoot('elasticsearch.requestTimeout', 'opensearch.requestTimeout'),
renameFromRoot('elasticsearch.pingTimeout', 'opensearch.pingTimeout'),
renameFromRoot('elasticsearch.logQueries', 'opensearch.logQueries'),
renameFromRoot('elasticsearch.optimizedHealthcheckId', 'opensearch.optimizedHealthcheckId'),
renameFromRoot('elasticsearch.ssl', 'opensearch.ssl'),
renameFromRoot('elasticsearch.apiVersion', 'opensearch.apiVersion'),
renameFromRoot('elasticsearch.healthCheck', 'opensearch.healthCheck'),
renameFromRoot('elasticsearch.ignoreVersionMismatch', 'opensearch.ignoreVersionMismatch'),
(settings, fromPath, log) => {
const opensearch = settings[fromPath];
if (!opensearch) {
return settings;
}
if (opensearch.username === 'elastic') {
log(
`Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "opensearch_dashboards_system" user instead.`
);
} else if (opensearch.username === 'opensearchDashboards') {
log(
`Setting [${fromPath}.username] to "opensearchDashboards" is deprecated. You should use the "opensearch_dashboards_system" user instead.`
);
}
if (opensearch.ssl?.key !== undefined && opensearch.ssl?.certificate === undefined) {
log(
`Setting [${fromPath}.ssl.key] without [${fromPath}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to OpenSearch.`
);
} else if (opensearch.ssl?.certificate !== undefined && opensearch.ssl?.key === undefined) {
log(
`Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to OpenSearch.`
);
}
return settings;
},
];
export const config: ServiceConfigDescriptor<OpenSearchConfigType> = {
path: 'opensearch',
schema: configSchema,
deprecations,
};
/**
* Wrapper of config schema.
* @public
*/
export class OpenSearchConfig {
/**
* The interval between health check requests OpenSearch Dashboards sends to the OpenSearch.
*/
public readonly healthCheckDelay: Duration;
/**
* Whether to allow opensearch-dashboards to connect to a non-compatible opensearch node.
*/
public readonly ignoreVersionMismatch: boolean;
/**
* Version of the OpenSearch (6.7, 7.1 or `master`) client will be connecting to.
*/
public readonly apiVersion: string;
/**
* Specifies whether all queries to the client should be logged (status code,
* method, query etc.).
*/
public readonly logQueries: boolean;
/**
* Specifies whether Dashboards should only query the local OpenSearch node when
* all nodes in the cluster have the same node attribute value
*/
public readonly optimizedHealthcheckId?: string;
/**
* Hosts that the client will connect to. If sniffing is enabled, this list will
* be used as seeds to discover the rest of your cluster.
*/
public readonly hosts: string[];
/**
* List of OpenSearch Dashboards client-side headers to send to OpenSearch when request
* scoped cluster client is used. If this is an empty array then *no* client-side
* will be sent.
*/
public readonly requestHeadersWhitelist: string[];
/**
* Timeout after which PING HTTP request will be aborted and retried.
*/
public readonly pingTimeout: Duration;
/**
* Timeout after which HTTP request will be aborted and retried.
*/
public readonly requestTimeout: Duration;
/**
* Timeout for OpenSearch to wait for responses from shards. Set to 0 to disable.
*/
public readonly shardTimeout: Duration;
/**
* Set of options to configure memory circuit breaker for query response.
* The `maxPercentage` field is to determine the threshold for maximum heap size for memory circuit breaker. By default the value is `1.0`.
* The `enabled` field specifies whether the client should protect large response that can't fit into memory.
*/
public readonly memoryCircuitBreaker: OpenSearchConfigType['memoryCircuitBreaker'];
/**
* Specifies whether the client should attempt to detect the rest of the cluster
* when it is first instantiated.
*/
public readonly sniffOnStart: boolean;
/**
* Interval to perform a sniff operation and make sure the list of nodes is complete.
* If `false` then sniffing is disabled.
*/
public readonly sniffInterval: false | Duration;
/**
* Specifies whether the client should immediately sniff for a more current list
* of nodes when a connection dies.
*/
public readonly sniffOnConnectionFault: boolean;
/**
* If OpenSearch is protected with basic authentication, this setting provides
* the username that the OpenSearch Dashboards server uses to perform its administrative functions.
*/
public readonly username?: string;
/**
* If OpenSearch is protected with basic authentication, this setting provides
* the password that the OpenSearch Dashboards server uses to perform its administrative functions.
*/
public readonly password?: string;
/**
* Set of settings configure SSL connection between OpenSearch Dashboards and OpenSearch that
* are required when `xpack.ssl.verification_mode` in OpenSearch is set to
* either `certificate` or `full`.
*/
public readonly ssl: Pick<
SslConfigSchema,
Exclude<keyof SslConfigSchema, 'certificateAuthorities' | 'keystore' | 'truststore'>
> & { certificateAuthorities?: string[] };
/**
* Header names and values to send to OpenSearch with every request. These
* headers cannot be overwritten by client-side headers and aren't affected by
* `requestHeadersWhitelist` configuration.
*/
public readonly customHeaders: OpenSearchConfigType['customHeaders'];
constructor(rawConfig: OpenSearchConfigType) {
this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch;
this.apiVersion = rawConfig.apiVersion;
this.logQueries = rawConfig.logQueries;
this.optimizedHealthcheckId = rawConfig.optimizedHealthcheckId;
this.hosts = Array.isArray(rawConfig.hosts) ? rawConfig.hosts : [rawConfig.hosts];
this.requestHeadersWhitelist = Array.isArray(rawConfig.requestHeadersWhitelist)
? rawConfig.requestHeadersWhitelist
: [rawConfig.requestHeadersWhitelist];

"requestHeadersWhitelist": Array [
undefined,
],
"ssl": Object {
"certificate": "certificate-value",
"verificationMode": "none",
},
}
`);
});
it('falls back to opensearch config if custom config not passed', async () => {
const setupContract = await opensearchService.setup(setupDeps);
// reset all mocks called during setup phase
MockLegacyClusterClient.mockClear();
setupContract.legacy.createClient('another-type');
const config = MockLegacyClusterClient.mock.calls[0][0];
expect(config).toMatchInlineSnapshot(`
Object {
"healthCheckDelay": "PT0.01S",
"hosts": Array [
"http://1.2.3.4",
],
"requestHeadersWhitelist": Array [
undefined,
],
"ssl": Object {
"alwaysPresentCertificate": undefined,
"certificate": undefined,
"certificateAuthorities": undefined,
"key": undefined,
"keyPassphrase": undefined,
"verificationMode": "none",
},
}
`);
});
it('does not merge opensearch hosts if custom config overrides', async () => {
configService.atPath.mockReturnValueOnce(
new BehaviorSubject({
hosts: ['http://1.2.3.4', 'http://9.8.7.6'],
healthCheck: {
delay: duration(2000),
},
ssl: {
verificationMode: 'none',
},
} as any)
);
opensearchService = new OpenSearchService(coreContext);
const setupContract = await opensearchService.setup(setupDeps);
// reset all mocks called during setup phase
MockLegacyClusterClient.mockClear();
const customConfig = {
hosts: ['http://8.8.8.8'],
logQueries: true,
ssl: { certificate: 'certificate-value' },
};
setupContract.legacy.createClient('some-custom-type', customConfig);
const config = MockLegacyClusterClient.mock.calls[0][0];
expect(config).toMatchInlineSnapshot(`
Object {
"healthCheckDelay": "PT2S",
"hosts": Array [
"http://8.8.8.8",
],
"logQueries": true,
"requestHeadersWhitelist": Array [
undefined,
],
"ssl": Object {
"certificate": "certificate-value",
"verificationMode": "none",
},
}
`);
});
});
it('opensearchNodeVersionCompatibility$ only starts polling when subscribed to', (done) => {
const mockedClient = mockClusterClientInstance.asInternalUser;
mockedClient.nodes.info.mockImplementation(() =>
opensearchClientMock.createErrorTransportRequestPromise(new Error())
);
opensearchService.setup(setupDeps).then((setupContract) => {
delay(10).then(() => {
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(0);
setupContract.opensearchNodesCompatibility$.subscribe(() => {
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1);
done();
});
});
});
});
it('opensearchNodeVersionCompatibility$ stops polling when unsubscribed from', async () => {
const mockedClient = mockClusterClientInstance.asInternalUser;
mockedClient.nodes.info.mockImplementation(() =>
opensearchClientMock.createErrorTransportRequestPromise(new Error())
);
const setupContract = await opensearchService.setup(setupDeps);
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(0);
const sub = setupContract.opensearchNodesCompatibility$.subscribe(async () => {
sub.unsubscribe();
await delay(100);
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1);
});
});
});
describe('#start', () => {
it('throws if called before `setup`', async () => {
expect(() => opensearchService.start(startDeps)).rejects.toMatchInlineSnapshot(
`[Error: OpenSearchService needs to be setup before calling start]`
);
});
it('returns opensearch client as a part of the contract', async () => {
await opensearchService.setup(setupDeps);
const startContract = await opensearchService.start(startDeps);
const client = startContract.client;
expect(client.asInternalUser).toBe(mockClusterClientInstance.asInternalUser);
});
describe('#createClient', () => {
it('allows to specify config properties', async () => {
await opensearchService.setup(setupDeps);
const startContract = await opensearchService.start(startDeps);
// reset all mocks called during setup phase
MockClusterClient.mockClear();
const customConfig = { logQueries: true };
const clusterClient = startContract.createClient('custom-type', customConfig);
expect(clusterClient).toBe(mockClusterClientInstance);
expect(MockClusterClient).toHaveBeenCalledTimes(1);
expect(MockClusterClient).toHaveBeenCalledWith(
expect.objectContaining(customConfig),
expect.objectContaining({ context: ['opensearch', 'custom-type'] }),
expect.any(Function)
);
});
it('creates a new client on each call', async () => {
await opensearchService.setup(setupDeps);
const startContract = await opensearchService.start(startDeps);
// reset all mocks called during setup phase
MockClusterClient.mockClear();
const customConfig = { logQueries: true };
startContract.createClient('custom-type', customConfig);
startContract.createClient('another-type', customConfig);
expect(MockClusterClient).toHaveBeenCalledTimes(2);
});
it('falls back to opensearch default config values if property not specified', async () => {
await opensearchService.setup(setupDeps);
const startContract = await opensearchService.start(startDeps);
// reset all mocks called during setup phase
MockClusterClient.mockClear();
const customConfig = {
hosts: ['http://8.8.8.8'],
logQueries: true,
ssl: { certificate: 'certificate-value' },
};
startContract.createClient('some-custom-type', customConfig);
const config = MockClusterClient.mock.calls[0][0];
expect(config).toMatchInlineSnapshot(`
Object {
"healthCheckDelay": "PT0.01S",
"hosts": Array [
"http://8.8.8.8",
],
"logQueries": true,
"requestHeadersWhitelist": Array [

requestHeadersWhitelist: ['authorization'],

requestHeadersWhitelist: ['authorization'],
customHeaders: {},
hosts: ['http://localhost'],
...parts,
};
};
describe('ClusterClient', () => {
let logger: ReturnType<typeof loggingSystemMock.createLogger>;
let getAuthHeaders: jest.MockedFunction<GetAuthHeaders>;
let internalClient: ReturnType<typeof opensearchClientMock.createInternalClient>;
let scopedClient: ReturnType<typeof opensearchClientMock.createInternalClient>;
beforeEach(() => {
logger = loggingSystemMock.createLogger();
internalClient = opensearchClientMock.createInternalClient();
scopedClient = opensearchClientMock.createInternalClient();
getAuthHeaders = jest.fn().mockImplementation(() => ({
authorization: 'auth',
foo: 'bar',
}));
configureClientMock.mockImplementation((config, { scoped = false }) => {
return scoped ? scopedClient : internalClient;
});
});
afterEach(() => {
configureClientMock.mockReset();
});
it('creates a single internal and scoped client during initialization', () => {
const config = createConfig();
new ClusterClient(config, logger, getAuthHeaders);
expect(configureClientMock).toHaveBeenCalledTimes(2);
expect(configureClientMock).toHaveBeenCalledWith(config, { logger });
expect(configureClientMock).toHaveBeenCalledWith(config, { logger, scoped: true });
});
describe('#asInternalUser', () => {
it('returns the internal client', () => {
const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders);
expect(clusterClient.asInternalUser).toBe(internalClient);
});
});
describe('#asScoped', () => {
it('returns a scoped cluster client bound to the request', () => {
const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest();
const scopedClusterClient = clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({ headers: expect.any(Object) });
expect(scopedClusterClient.asInternalUser).toBe(clusterClient.asInternalUser);
expect(scopedClusterClient.asCurrentUser).toBe(scopedClient.child.mock.results[0].value);
});
it('returns a distinct scoped cluster client on each call', () => {
const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest();
const scopedClusterClient1 = clusterClient.asScoped(request);
const scopedClusterClient2 = clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(2);
expect(scopedClusterClient1).not.toBe(scopedClusterClient2);
expect(scopedClusterClient1.asInternalUser).toBe(scopedClusterClient2.asInternalUser);
});
it('creates a scoped client with filtered request headers', () => {
const config = createConfig({
requestHeadersWhitelist: ['foo'],
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
headers: {
foo: 'bar',
hello: 'dolly',
},
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: { ...DEFAULT_HEADERS, foo: 'bar', 'x-opaque-id': expect.any(String) },
});
});
it('creates a scoped facade with filtered auth headers', () => {
const config = createConfig({
requestHeadersWhitelist: ['authorization'],
});
getAuthHeaders.mockReturnValue({
authorization: 'auth',
other: 'nope',
});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) },
});
});
it('respects auth headers precedence', () => {
const config = createConfig({
requestHeadersWhitelist: ['authorization'],
});
getAuthHeaders.mockReturnValue({
authorization: 'auth',
other: 'nope',
});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
headers: {
authorization: 'override',
},
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) },
});
});
it('includes the `customHeaders` from the config without filtering them', () => {
const config = createConfig({
customHeaders: {
foo: 'bar',
hello: 'dolly',
},
requestHeadersWhitelist: ['authorization'],
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
...DEFAULT_HEADERS,
foo: 'bar',
hello: 'dolly',
'x-opaque-id': expect.any(String),
},
});
});
it('adds the x-opaque-id header based on the request id', () => {
const config = createConfig();
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
opensearchDashboardsRequestState: {
requestId: 'my-fake-id',
requestUuid: 'ignore-this-id',
},
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
...DEFAULT_HEADERS,
'x-opaque-id': 'my-fake-id',
},
});
});
it('respect the precedence of auth headers over config headers', () => {
const config = createConfig({
customHeaders: {
foo: 'config',
hello: 'dolly',
},
requestHeadersWhitelist: ['foo'],
});
getAuthHeaders.mockReturnValue({
foo: 'auth',
});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
...DEFAULT_HEADERS,
foo: 'auth',
hello: 'dolly',
'x-opaque-id': expect.any(String),
},
});
});
it('respect the precedence of request headers over config headers', () => {
const config = createConfig({
customHeaders: {
foo: 'config',
hello: 'dolly',
},
requestHeadersWhitelist: ['foo'],
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
headers: { foo: 'request' },
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
...DEFAULT_HEADERS,
foo: 'request',
hello: 'dolly',
'x-opaque-id': expect.any(String),
},
});
});
it('respect the precedence of config headers over default headers', () => {
const headerKey = Object.keys(DEFAULT_HEADERS)[0];
const config = createConfig({
customHeaders: {
[headerKey]: 'foo',
},
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest();
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
[headerKey]: 'foo',
'x-opaque-id': expect.any(String),
},
});
});
it('respect the precedence of request headers over default headers', () => {
const headerKey = Object.keys(DEFAULT_HEADERS)[0];
const config = createConfig({
requestHeadersWhitelist: [headerKey],
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
headers: { [headerKey]: 'foo' },
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
[headerKey]: 'foo',
'x-opaque-id': expect.any(String),
},
});
});
it('respect the precedence of x-opaque-id header over config headers', () => {
const config = createConfig({
customHeaders: {
'x-opaque-id': 'from config',
},
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = httpServerMock.createOpenSearchDashboardsRequest({
headers: { foo: 'request' },
opensearchDashboardsRequestState: {
requestId: 'from request',
requestUuid: 'ignore-this-id',
},
});
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: {
...DEFAULT_HEADERS,
'x-opaque-id': 'from request',
},
});
});
it('filter headers when called with a `FakeRequest`', () => {
const config = createConfig({
requestHeadersWhitelist: ['authorization'],
});
getAuthHeaders.mockReturnValue({});
const clusterClient = new ClusterClient(config, logger, getAuthHeaders);
const request = {
headers: {
authorization: 'auth',
hello: 'dolly',
},
};
clusterClient.asScoped(request);
expect(scopedClient.child).toHaveBeenCalledTimes(1);
expect(scopedClient.child).toHaveBeenCalledWith({
headers: { ...DEFAULT_HEADERS, authorization: 'auth' },
});
});
it('does not add auth headers when called with a `FakeRequest`', () => {
const config = createConfig({
requestHeadersWhitelist: ['authorization', 'foo'],

...this.config.requestHeadersWhitelist,
]);
} else {
scopedHeaders = filterHeaders(request?.headers ?? {}, this.config.requestHeadersWhitelist);

requestHeadersWhitelist: ['one', 'two'],
} as any;
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
jest.clearAllMocks();
});
test('creates additional OpenSearch client only once', () => {
const firstScopedClusterClient = clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { one: '1' } })
);
expect(firstScopedClusterClient).toBeDefined();
expect(mockParseOpenSearchClientConfig).toHaveBeenCalledTimes(1);
expect(mockParseOpenSearchClientConfig).toHaveBeenLastCalledWith(
mockOpenSearchConfig,
mockLogger,
{
auth: false,
ignoreCertAndKey: true,
}
);
expect(MockClient).toHaveBeenCalledTimes(1);
expect(MockClient).toHaveBeenCalledWith(mockParseOpenSearchClientConfig.mock.results[0].value);
jest.clearAllMocks();
const secondScopedClusterClient = clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { two: '2' } })
);
expect(secondScopedClusterClient).toBeDefined();
expect(secondScopedClusterClient).not.toBe(firstScopedClusterClient);
expect(mockParseOpenSearchClientConfig).not.toHaveBeenCalled();
expect(MockClient).not.toHaveBeenCalled();
});
test('properly configures `ignoreCertAndKey` for various configurations', () => {
// Config without SSL.
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
mockParseOpenSearchClientConfig.mockClear();
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } }));
expect(mockParseOpenSearchClientConfig).toHaveBeenCalledTimes(1);
expect(mockParseOpenSearchClientConfig).toHaveBeenLastCalledWith(
mockOpenSearchConfig,
mockLogger,
{
auth: false,
ignoreCertAndKey: true,
}
);
// Config ssl.alwaysPresentCertificate === false
mockOpenSearchConfig = {
...mockOpenSearchConfig,
ssl: { alwaysPresentCertificate: false },
} as any;
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
mockParseOpenSearchClientConfig.mockClear();
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } }));
expect(mockParseOpenSearchClientConfig).toHaveBeenCalledTimes(1);
expect(mockParseOpenSearchClientConfig).toHaveBeenLastCalledWith(
mockOpenSearchConfig,
mockLogger,
{
auth: false,
ignoreCertAndKey: true,
}
);
// Config ssl.alwaysPresentCertificate === true
mockOpenSearchConfig = {
...mockOpenSearchConfig,
ssl: { alwaysPresentCertificate: true },
} as any;
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
mockParseOpenSearchClientConfig.mockClear();
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } }));
expect(mockParseOpenSearchClientConfig).toHaveBeenCalledTimes(1);
expect(mockParseOpenSearchClientConfig).toHaveBeenLastCalledWith(
mockOpenSearchConfig,
mockLogger,
{
auth: false,
ignoreCertAndKey: false,
}
);
});
test('passes only filtered headers to the scoped cluster client', () => {
clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } })
);
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ one: '1', two: '2' },
expect.any(Object)
);
});
test('passes x-opaque-id header with request id', () => {
clusterClient.asScoped(
httpServerMock.createOpenSearchDashboardsRequest({
opensearchDashboardsRequestState: { requestId: 'alpha', requestUuid: 'ignore-this-id' },
})
);
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ 'x-opaque-id': 'alpha' },
expect.any(Object)
);
});
test('both scoped and internal API caller fail if cluster client is closed', async () => {
clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } })
);
clusterClient.close();
const [[internalAPICaller, scopedAPICaller]] = MockScopedClusterClient.mock.calls;
await expect(internalAPICaller('ping')).rejects.toThrowErrorMatchingInlineSnapshot(
`"Cluster client cannot be used after it has been closed."`
);
await expect(scopedAPICaller('ping', {})).rejects.toThrowErrorMatchingInlineSnapshot(
`"Cluster client cannot be used after it has been closed."`
);
});
test('does not fail when scope to not defined request', async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
clusterClient.asScoped();
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{},
undefined
);
});
test('does not fail when scope to a request without headers', async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
clusterClient.asScoped({} as any);
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{},
undefined
);
});
test('calls getAuthHeaders and filters results for a real request', async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory,
() => ({
one: '1',
three: '3',
})
);
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { two: '2' } }));
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ one: '1', two: '2' },
expect.any(Object)
);
});
test('getAuthHeaders results rewrite extends a request headers', async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory,
() => ({ one: 'foo' })
);
clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1', two: '2' } }));
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ one: 'foo', two: '2' },
expect.any(Object)
);
});
test("doesn't call getAuthHeaders for a fake request", async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory,
() => ({})
);
clusterClient.asScoped({ headers: { one: 'foo' } });
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ one: 'foo' },
undefined
);
});
test('filters a fake request headers', async () => {
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
auditTrailServiceMock.createAuditorFactory
);
clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } });
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
{ one: '1', two: '2' },
undefined
);
});
describe('Auditor', () => {
it('creates Auditor for OpenSearchDashboardsRequest', async () => {
const auditor = auditTrailServiceMock.createAuditor();
const auditorFactory = auditTrailServiceMock.createAuditorFactory();
auditorFactory.asScoped.mockReturnValue(auditor);
clusterClient = new LegacyClusterClient(
mockOpenSearchConfig,
mockLogger,
() => auditorFactory
);
clusterClient.asScoped(httpServerMock.createOpenSearchDashboardsRequest());
expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
expect.objectContaining({ 'x-opaque-id': expect.any(String) }),
auditor
);
});
it("doesn't create Auditor for a fake request", async () => {
const getAuthHeaders = jest.fn();
clusterClient = new LegacyClusterClient(mockOpenSearchConfig, mockLogger, getAuthHeaders);
clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } });
expect(getAuthHeaders).not.toHaveBeenCalled();
});
it("doesn't create Auditor when no request passed", async () => {
const getAuthHeaders = jest.fn();
clusterClient = new LegacyClusterClient(mockOpenSearchConfig, mockLogger, getAuthHeaders);
clusterClient.asScoped();
expect(getAuthHeaders).not.toHaveBeenCalled();
});
});
});
describe('#close', () => {
let mockOpenSearchClientInstance: { close: jest.Mock };
let mockScopedOpenSearchClientInstance: { close: jest.Mock };
let clusterClient: LegacyClusterClient;
beforeEach(() => {
mockOpenSearchClientInstance = { close: jest.fn() };
mockScopedOpenSearchClientInstance = { close: jest.fn() };
MockClient.mockImplementationOnce(() => mockOpenSearchClientInstance).mockImplementationOnce(
() => mockScopedOpenSearchClientInstance
);
clusterClient = new LegacyClusterClient(
{ apiVersion: 'opensearch-version', requestHeadersWhitelist: [] } as any,

...this.config.requestHeadersWhitelist,

requestHeadersWhitelist: [],
},
logger.get()
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "master",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "localhost",
"path": "/opensearch",
"port": "80",
"protocol": "http:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": false,
"sniffOnStart": false,
}
`);
});
test('parses fully specified config', () => {
const opensearchConfig: LegacyOpenSearchClientConfig = {
apiVersion: 'v7.0.0',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: [
'http://localhost/opensearch',
'http://domain.com:1234/opensearch',
'https://opensearch.local',
],
requestHeadersWhitelist: [],
username: 'opensearch',
password: 'changeme',
pingTimeout: 12345,
requestTimeout: 54321,
sniffInterval: 11223344,
ssl: {
verificationMode: 'certificate',
certificateAuthorities: ['content-of-ca-path-1', 'content-of-ca-path-2'],
certificate: 'content-of-certificate-path',
key: 'content-of-key-path',
keyPassphrase: 'key-pass',
alwaysPresentCertificate: true,
},
};
const opensearchClientConfig = parseOpenSearchClientConfig(opensearchConfig, logger.get());
// Check that original references aren't used.
for (const host of opensearchClientConfig.hosts) {
expect(opensearchConfig.customHeaders).not.toBe(host.headers);
}
expect(opensearchConfig.ssl).not.toBe(opensearchClientConfig.ssl);
expect(opensearchClientConfig).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"auth": "opensearch:changeme",
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "localhost",
"path": "/opensearch",
"port": "80",
"protocol": "http:",
"query": null,
},
Object {
"auth": "opensearch:changeme",
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "domain.com",
"path": "/opensearch",
"port": "1234",
"protocol": "http:",
"query": null,
},
Object {
"auth": "opensearch:changeme",
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"pingTimeout": 12345,
"requestTimeout": 54321,
"sniffInterval": 11223344,
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
"ca": Array [
"content-of-ca-path-1",
"content-of-ca-path-2",
],
"cert": "content-of-certificate-path",
"checkServerIdentity": [Function],
"key": "content-of-key-path",
"passphrase": "key-pass",
"rejectUnauthorized": true,
},
}
`);
});
test('parses config timeouts of moment.Duration type', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'master',
customHeaders: { xsrf: 'something' },
logQueries: false,
sniffOnStart: false,
sniffOnConnectionFault: false,
pingTimeout: duration(100, 'ms'),
requestTimeout: duration(30, 's'),
sniffInterval: duration(1, 'minute'),
hosts: ['http://localhost:9200/opensearch'],
requestHeadersWhitelist: [],
},
logger.get()
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "master",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "localhost",
"path": "/opensearch",
"port": "9200",
"protocol": "http:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"pingTimeout": 100,
"requestTimeout": 30000,
"sniffInterval": 60000,
"sniffOnConnectionFault": false,
"sniffOnStart": false,
}
`);
});
describe('#auth', () => {
test('is not set if #auth = false even if username and password are provided', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['http://user:password@localhost/opensearch', 'https://opensearch.local'],
username: 'opensearch',
password: 'changeme',
requestHeadersWhitelist: [],
},
logger.get(),
{ auth: false }
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "localhost",
"path": "/opensearch",
"port": "80",
"protocol": "http:",
"query": null,
},
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
}
`);
});
test('is not set if username is not specified', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
password: 'changeme',
},
logger.get(),
{ auth: true }
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
}
`);
});
test('is not set if password is not specified', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
username: 'opensearch',
},
logger.get(),
{ auth: true }
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
"xsrf": "something",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
}
`);
});
});
describe('#customHeaders', () => {
test('override the default headers', () => {
const headerKey = Object.keys(DEFAULT_HEADERS)[0];
const parsedConfig = parseOpenSearchClientConfig(
{
apiVersion: 'master',
customHeaders: { [headerKey]: 'foo' },
logQueries: false,
sniffOnStart: false,
sniffOnConnectionFault: false,
hosts: ['http://localhost/opensearch'],
requestHeadersWhitelist: [],
},
logger.get()
);
expect(parsedConfig.hosts[0].headers).toEqual({
[headerKey]: 'foo',
});
});
});
describe('#log', () => {
test('default logger with #logQueries = false', () => {
const parsedConfig = parseOpenSearchClientConfig(
{
apiVersion: 'master',
customHeaders: { xsrf: 'something' },
logQueries: false,
sniffOnStart: false,
sniffOnConnectionFault: false,
hosts: ['http://localhost/opensearch'],
requestHeadersWhitelist: [],
},
logger.get()
);
const Logger = new parsedConfig.log();
Logger.error('some-error');
Logger.warning('some-warning');
Logger.trace('some-trace');
Logger.info('some-info');
Logger.debug('some-debug');
expect(typeof Logger.close).toBe('function');
expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(`
Object {
"debug": Array [],
"error": Array [
Array [
"some-error",
],
],
"fatal": Array [],
"info": Array [],
"log": Array [],
"trace": Array [],
"warn": Array [
Array [
"some-warning",
],
],
}
`);
});
test('default logger with #logQueries = true', () => {
const parsedConfig = parseOpenSearchClientConfig(
{
apiVersion: 'master',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: false,
sniffOnConnectionFault: false,
hosts: ['http://localhost/opensearch'],
requestHeadersWhitelist: [],
},
logger.get()
);
const Logger = new parsedConfig.log();
Logger.error('some-error');
Logger.warning('some-warning');
Logger.trace('METHOD', { path: '/some-path' }, '?query=2', 'unknown', '304');
Logger.info('some-info');
Logger.debug('some-debug');
expect(typeof Logger.close).toBe('function');
expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(`
Object {
"debug": Array [
Array [
"304
METHOD /some-path
?query=2",
Object {
"tags": Array [
"query",
],
},
],
],
"error": Array [
Array [
"some-error",
],
],
"fatal": Array [],
"info": Array [],
"log": Array [],
"trace": Array [],
"warn": Array [
Array [
"some-warning",
],
],
}
`);
});
test('custom logger', () => {
const customLogger = jest.fn();
const parsedConfig = parseOpenSearchClientConfig(
{
apiVersion: 'master',
customHeaders: { xsrf: 'something' },
logQueries: true,
sniffOnStart: false,
sniffOnConnectionFault: false,
hosts: ['http://localhost/opensearch'],
requestHeadersWhitelist: [],
log: customLogger,
},
logger.get()
);
expect(parsedConfig.log).toBe(customLogger);
});
});
describe('#ssl', () => {
test('#verificationMode = none', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: {},
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
ssl: { verificationMode: 'none' },
},
logger.get()
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
"ca": undefined,
"rejectUnauthorized": false,
},
}
`);
});
test('#verificationMode = certificate', () => {
const clientConfig = parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: {},
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
ssl: { verificationMode: 'certificate' },
},
logger.get()
);
// `checkServerIdentity` shouldn't check hostname when verificationMode is certificate.
expect(
clientConfig.ssl!.checkServerIdentity!('right.com', { subject: { CN: 'wrong.com' } } as any)
).toBeUndefined();
expect(clientConfig).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
"ca": undefined,
"checkServerIdentity": [Function],
"rejectUnauthorized": true,
},
}
`);
});
test('#verificationMode = full', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: {},
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
ssl: { verificationMode: 'full' },
},
logger.get()
)
).toMatchInlineSnapshot(`
Object {
"apiVersion": "v7.0.0",
"hosts": Array [
Object {
"headers": Object {
"x-opensearch-product-origin": "opensearch-dashboards",
},
"host": "opensearch.local",
"path": "/",
"port": "443",
"protocol": "https:",
"query": null,
},
],
"keepAlive": true,
"log": [Function],
"sniffOnConnectionFault": true,
"sniffOnStart": true,
"ssl": Object {
"ca": undefined,
"rejectUnauthorized": true,
},
}
`);
});
test('#verificationMode is unknown', () => {
expect(() =>
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: {},
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],
ssl: { verificationMode: 'misspelled' as any },
},
logger.get()
)
).toThrowErrorMatchingInlineSnapshot(`"Unknown ssl verificationMode: misspelled"`);
});
test('#ignoreCertAndKey = true', () => {
expect(
parseOpenSearchClientConfig(
{
apiVersion: 'v7.0.0',
customHeaders: {},
logQueries: true,
sniffOnStart: true,
sniffOnConnectionFault: true,
hosts: ['https://opensearch.local'],
requestHeadersWhitelist: [],

requestHeadersWhitelist: string[];

const filteredHeaders = filterHeaders(headers, opensearchConfig.requestHeadersWhitelist);

opensearch.requestHeadersWhitelistConfigured config

requestHeadersWhitelistConfigured: boolean;

requestHeadersWhitelistConfigured: isConfigured.stringOrArray(

requestHeadersWhitelistConfigured: boolean;

server.compression.referrerWhitelist config

'--server.compression.referrerWhitelist=["some-host.com"]',

it(`uses compression when there is a whitelisted referer`, async () => {
await supertest
.get('/app/opensearch-dashboards')
.set('accept-encoding', 'gzip')
.set('referer', 'https://some-host.com')
.then((response) => {
expect(response.headers).to.have.property('content-encoding', 'gzip');
});
});
it(`doesn't use compression when there is a non-whitelisted referer`, async () => {

server.xsrf.whitelist config

xsrf: {
disableProtection: false,
whitelist: [],
},
},
});
const configAdapterWithDisabledSSL = new LegacyObjectToConfigAdapter({
server: {
name: 'opensearch-dashboards-hostname',
autoListen: true,
basePath: '/abc',
cors: false,
customResponseHeaders: { 'custom-header': 'custom-value' },
host: 'host',
maxPayloadBytes: 1000,
keepaliveTimeout: 5000,
socketTimeout: 2000,
port: 1234,
rewriteBasePath: false,
ssl: { enabled: false, certificate: 'cert', key: 'key' },
compression: { enabled: true },
someNotSupportedValue: 'val',
xsrf: {
disableProtection: false,
whitelist: [],
},

"whitelist": Array [],
},
}
`;
exports[`#get correctly handles server config.: disabled ssl 1`] = `
Object {
"autoListen": true,
"basePath": "/abc",
"compression": Object {
"enabled": true,
},
"cors": false,
"customResponseHeaders": Object {
"custom-header": "custom-value",
},
"host": "host",
"keepaliveTimeout": 5000,
"maxPayload": 1000,
"name": "opensearch-dashboards-hostname",
"port": 1234,
"rewriteBasePath": false,
"socketTimeout": 2000,
"ssl": Object {
"certificate": "cert",
"enabled": false,
"key": "key",
},
"uuid": undefined,
"xsrf": Object {
"disableProtection": false,
"whitelist": Array [],

it('logs a warning if server.xsrf.whitelist is set', () => {
const { messages } = applyCoreDeprecations({
server: { xsrf: { whitelist: ['/path'] } },
});
expect(messages).toMatchInlineSnapshot(`
Array [
"\\"server.xsrf.whitelist\\" is deprecated and has been replaced by \\"server.xsrf.allowlist\\"",
"It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. Instead, supply the \\"osd-xsrf\\" header.",

if ((settings.server?.xsrf?.whitelist ?? []).length > 0) {
log(
'It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. ' +
'Instead, supply the "osd-xsrf" header.'
);
}
return settings;
};
const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) {
log(
'You should set server.basePath along with server.rewriteBasePath. OpenSearch Dashboards ' +
'will expect that all requests start with server.basePath rather than expecting you to rewrite ' +
'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' +
'current behavior and silence this warning.'
);
}
return settings;
};
const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
const NONCE_STRING = `{nonce}`;
// Policies that should include the 'self' source
const SELF_POLICIES = Object.freeze(['script-src', 'style-src']);
const SELF_STRING = `'self'`;
const rules: string[] = get(settings, 'csp.rules');
if (rules) {
const parsed = new Map(
rules.map((ruleStr) => {
const parts = ruleStr.split(/\s+/);
return [parts[0], parts.slice(1)];
})
);
settings.csp.rules = [...parsed].map(([policy, sourceList]) => {
if (sourceList.find((source) => source.includes(NONCE_STRING))) {
log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`);
sourceList = sourceList.filter((source) => !source.includes(NONCE_STRING));
// Add 'self' if not present
if (!sourceList.find((source) => source.includes(SELF_STRING))) {
sourceList.push(SELF_STRING);
}
}
if (
SELF_POLICIES.includes(policy) &&
!sourceList.find((source) => source.includes(SELF_STRING))
) {
log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`);
sourceList.push(SELF_STRING);
}
return `${policy} ${sourceList.join(' ')}`.trim();
});
}
return settings;
};
const mapManifestServiceUrlDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
if (has(settings, 'map.manifestServiceUrl')) {
log(
'You should no longer use the map.manifestServiceUrl setting in opensearch_dashboards.yml to configure the location ' +
'of the Maps Service settings. These settings have moved to the "map.emsTileApiUrl" and ' +
'"map.emsFileApiUrl" settings instead. These settings are for development use only and should not be ' +
'modified for use in production environments.'
);
}
return settings;
};
export const coreDeprecationProvider: ConfigDeprecationProvider = ({
unusedFromRoot,
renameFromRoot,
renameFromRootWithoutMap,
}) => [
unusedFromRoot('savedObjects.indexCheckTimeout'),
unusedFromRoot('server.xsrf.token'),
unusedFromRoot('maps.manifestServiceUrl'),
unusedFromRoot('optimize.lazy'),
unusedFromRoot('optimize.lazyPort'),
unusedFromRoot('optimize.lazyHost'),
unusedFromRoot('optimize.lazyPrebuild'),
unusedFromRoot('optimize.lazyProxyTimeout'),
unusedFromRoot('optimize.enabled'),
unusedFromRoot('optimize.bundleFilter'),
unusedFromRoot('optimize.bundleDir'),
unusedFromRoot('optimize.viewCaching'),
unusedFromRoot('optimize.watch'),
unusedFromRoot('optimize.watchPort'),
unusedFromRoot('optimize.watchHost'),
unusedFromRoot('optimize.watchPrebuild'),
unusedFromRoot('optimize.watchProxyTimeout'),
unusedFromRoot('optimize.useBundleCache'),
unusedFromRoot('optimize.sourceMaps'),
unusedFromRoot('optimize.workers'),
unusedFromRoot('optimize.profile'),
unusedFromRoot('optimize.validateSyntaxOfNodeModules'),
renameFromRoot('cpu.cgroup.path.override', 'ops.cGroupOverrides.cpuPath'),
renameFromRoot('cpuacct.cgroup.path.override', 'ops.cGroupOverrides.cpuAcctPath'),
unusedFromRoot('opensearch.preserveHost'),
unusedFromRoot('opensearch.startupTimeout'),
renameFromRootWithoutMap('server.xsrf.whitelist', 'server.xsrf.allowlist'),

Other code

Comments/variables

test('excludes references and migrationVersion which are part of the blacklist', () => {

a security tool for macOS that monitors process executions and can blacklist/whitelist binaries. \

test('enables compression for whitelisted referer', async () => {
const response = await supertest(listener)
.get('/')
.set('accept-encoding', 'gzip')
.set('referer', 'http://foo:1234');
expect(response.header).toHaveProperty('content-encoding', 'gzip');
});
test('disables compression for non-whitelisted referer', async () => {

export const LICENSE_WHITELIST = [

export const DEV_ONLY_LICENSE_WHITELIST = ['MPL-2.0'];

export { LICENSE_WHITELIST, DEV_ONLY_LICENSE_WHITELIST, LICENSE_OVERRIDES } from './config';

import { LICENSE_WHITELIST, DEV_ONLY_LICENSE_WHITELIST, LICENSE_OVERRIDES } from './config';
import { assertLicensesValid } from './valid';
run(
async ({ log, flags }) => {
const packages = await getInstalledPackages({
directory: REPO_ROOT,
licenseOverrides: LICENSE_OVERRIDES,
includeDev: !!flags.dev,
});
// Assert if the found licenses in the production
// packages are valid
assertLicensesValid({
packages: packages.filter((pkg) => !pkg.isDevOnly),
validLicenses: LICENSE_WHITELIST,
});
log.success('All production dependency licenses are allowed');
// Do the same as above for the packages only used in development
// if the dev flag is found
if (flags.dev) {
assertLicensesValid({
packages: packages.filter((pkg) => pkg.isDevOnly),
validLicenses: LICENSE_WHITELIST.concat(DEV_ONLY_LICENSE_WHITELIST),

const whiteListedRules = ['backticks', 'emphasis', 'link', 'list'];
export function Content({ text }) {
return (
<Markdown
className="euiText"
markdown={text}
openLinksInNewTab={true}
whiteListedRules={whiteListedRules}

a security tool for macOS that monitors process executions and can blacklist/whitelist binaries. \

test('whiteListedRules', () => {
const component = shallow(
<Markdown markdown={markdown} whiteListedRules={['backticks', 'emphasis']} />
);
expect(component).toMatchSnapshot();
});
test('should update markdown when openLinksInNewTab prop change', () => {
const component = shallow(<Markdown markdown={markdown} openLinksInNewTab={false} />);
expect(component.render().find('a').prop('target')).not.toBe('_blank');
component.setProps({ openLinksInNewTab: true });
expect(component.render().find('a').prop('target')).toBe('_blank');
});
test('should update markdown when whiteListedRules prop change', () => {
const md = '*emphasis* `backticks`';
const component = shallow(
<Markdown markdown={md} whiteListedRules={['emphasis', 'backticks']} />
);
expect(component.render().find('em')).toHaveLength(1);
expect(component.render().find('code')).toHaveLength(1);
component.setProps({ whiteListedRules: ['backticks'] });

* whiteListedRules and openLinksInNewTab configurations.
* @param {Array of Strings} whiteListedRules - white list of markdown rules
* list of rules can be found at https://github.com/markdown-it/markdown-it/issues/361
* @param {Boolean} openLinksInNewTab
* @return {Function} Returns an Object to use with dangerouslySetInnerHTML
* with the rendered markdown HTML
*/
export const markdownFactory = memoize(
(whiteListedRules: string[] = [], openLinksInNewTab: boolean = false) => {
let markdownIt: MarkdownIt;
// It is imperative that the html config property be set to false, to mitigate XSS: the output of markdown-it is
// fed directly to the DOM via React's dangerouslySetInnerHTML below.
if (whiteListedRules && whiteListedRules.length > 0) {
markdownIt = new MarkdownIt('zero', { html: false, linkify: true });
markdownIt.enable(whiteListedRules);
} else {
markdownIt = new MarkdownIt({ html: false, linkify: true });
}
if (openLinksInNewTab) {
// All links should open in new browser tab.
// Define custom renderer to add 'target' attribute
// https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
const originalLinkRender =
markdownIt.renderer.rules.link_open ||
function (tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
markdownIt.renderer.rules.link_open = function (tokens, idx, options, env, self) {
const href = tokens[idx].attrGet('href');
const target = '_blank';
const rel = getSecureRelForTarget({ href: href === null ? undefined : href, target });
// https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/
tokens[idx].attrPush(['target', target]);
if (rel) {
tokens[idx].attrPush(['rel', rel]);
}
return originalLinkRender(tokens, idx, options, env, self);
};
}
/**
* This method is used to render markdown from the passed parameter
* into HTML. It will just return an empty string when the markdown is empty.
* @param {String} markdown - The markdown String
* @return {String} - Returns the rendered HTML as string.
*/
return (markdown: string) => {
return markdown ? markdownIt.render(markdown) : '';
};
},
(whiteListedRules: string[] = [], openLinksInNewTab: boolean = false) => {
return `${whiteListedRules.join('_')}${openLinksInNewTab}`;
}
);
export interface MarkdownProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
markdown?: string;
openLinksInNewTab?: boolean;
whiteListedRules?: string[];
}
export class Markdown extends PureComponent<MarkdownProps> {
render() {
const { className, markdown = '', openLinksInNewTab, whiteListedRules, ...rest } = this.props;
const classes = classNames('osdMarkdown__body', className);
const markdownRenderer = markdownFactory(whiteListedRules, openLinksInNewTab);

WHITE_LISTED_GROUP_BY_FIELDS = 'whiteListedGroupByFields',
/**
* Key for getting the white listed metrics from the UIRestrictions object.
*/
WHITE_LISTED_METRICS = 'whiteListedMetrics',
/**
* Key for getting the white listed Time Range modes from the UIRestrictions object.
*/
WHITE_LISTED_TIMERANGE_MODES = 'whiteListedTimerangeModes',

get whiteListedMetrics() {
return this.createUiRestriction();
}
get whiteListedGroupByFields() {
return this.createUiRestriction();
}
get whiteListedTimerangeModes() {
return this.createUiRestriction();
}
get uiRestrictions() {
return {
[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: this.whiteListedMetrics,
[RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: this.whiteListedGroupByFields,
[RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES]: this.whiteListedTimerangeModes,

whiteListedMetrics: { '*': true },
whiteListedGroupByFields: { '*': true },
whiteListedTimerangeModes: { '*': true },

@tmarkley tmarkley changed the title Change the "Blacklist / Whitelist" nomenclature Deprecate the "Blacklist / Whitelist" nomenclature Apr 21, 2022
@kavilla kavilla assigned tmarkley and unassigned boktorbb May 9, 2022
@kavilla kavilla added v2.1.0 and removed v2.0.0 labels May 16, 2022
@tmarkley tmarkley removed their assignment May 23, 2022
@tmarkley tmarkley removed their assignment Jun 20, 2022
@kavilla kavilla self-assigned this Jun 24, 2022
@kavilla kavilla linked a pull request Jun 27, 2022 that will close this issue
7 tasks
@kavilla kavilla added v2.2.0 and removed v2.1.0 labels Jun 29, 2022
@kavilla kavilla removed their assignment Jul 5, 2022
@manasvinibs manasvinibs removed their assignment Jul 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants