-
Notifications
You must be signed in to change notification settings - Fork 905
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
Comments
Resolved in #1467 |
Instances of
|
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( |
OpenSearch-Dashboards/src/core/server/http/http_config.ts
Lines 120 to 121 in 376ba8c
if (!rawConfig.compression.enabled && rawConfig.compression.referrerWhitelist) { | |
return 'cannot use [compression.referrerWhitelist] when [compression.enabled] is set to false'; |
OpenSearch-Dashboards/src/core/server/http/http_server.test.ts
Lines 857 to 862 in 376ba8c
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; |
OpenSearch-Dashboards/src/core/server/http/__snapshots__/http_config.test.ts.snap
Lines 103 to 116 in 376ba8c
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
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.mock.ts
Line 68 in 376ba8c
referrerWhitelistConfigured: false, |
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.test.ts
Line 120 in 376ba8c
"referrerWhitelistConfigured": false, |
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 201 in 376ba8c
referrerWhitelistConfigured: isConfigured.array(http.compression.referrerWhitelist), |
referrerWhitelistConfigured: boolean; |
Line 75 in 376ba8c
referrerWhitelistConfigured: { type: 'boolean' }, |
http.xsrf.whitelist
config
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 205 in 376ba8c
whitelistConfigured: isConfigured.array(http.xsrf.whitelist), |
whitelist: [], |
OpenSearch-Dashboards/src/core/server/http/http_config.test.ts
Lines 179 to 187 in 376ba8c
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"` |
whitelist: schema.arrayOf( |
public xsrf: { disableProtection: boolean; whitelist: string[] }; |
OpenSearch-Dashboards/src/core/server/http/lifecycle_handlers.test.ts
Lines 76 to 170 in 376ba8c
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 }, |
OpenSearch-Dashboards/src/core/server/http/lifecycle_handlers.ts
Lines 43 to 48 in 376ba8c
const { whitelist, disableProtection } = config.xsrf; | |
return (request, response, toolkit) => { | |
if ( | |
disableProtection || | |
whitelist.includes(request.route.path) || |
whitelist: [], |
OpenSearch-Dashboards/src/core/server/http/__snapshots__/http_config.test.ts.snap
Line 86 in 376ba8c
"whitelist": Array [], |
OpenSearch-Dashboards/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
Lines 50 to 249 in 376ba8c
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; |
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.mock.ts
Line 113 in 376ba8c
whitelistConfigured: false, |
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.test.ts
Line 171 in 376ba8c
"whitelistConfigured": false, |
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 205 in 376ba8c
whitelistConfigured: isConfigured.array(http.xsrf.whitelist), |
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 205 in 376ba8c
whitelistConfigured: isConfigured.array(http.xsrf.whitelist), |
whitelistConfigured: boolean; |
Line 79 in 376ba8c
whitelistConfigured: { type: 'boolean' }, |
opensearch.requestHeadersWhitelist
config
OpenSearch-Dashboards/config/opensearch_dashboards.yml
Lines 90 to 93 in 376ba8c
#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. |
OpenSearch-Dashboards/packages/osd-apm-config-loader/__fixtures__/en_var_ref_config.yml
Line 5 in 376ba8c
requestHeadersWhitelist: ["${OSD_ENV_VAR1}", "${OSD_ENV_VAR2}"] |
Line 100 in 376ba8c
"requestHeadersWhitelist": Array [ |
requestHeadersWhitelist: ["${OSD_ENV_VAR1}", "${OSD_ENV_VAR2}"] |
OpenSearch-Dashboards/packages/osd-config/src/raw/__snapshots__/read_config.test.ts.snap
Line 100 in 376ba8c
"requestHeadersWhitelist": Array [ |
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[]; |
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 176 in 376ba8c
opensearchConfig.requestHeadersWhitelist, |
OpenSearch-Dashboards/src/core/server/opensearch/opensearch_config.test.ts
Lines 88 to 444 in 376ba8c
"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\\"", |
OpenSearch-Dashboards/src/core/server/opensearch/opensearch_config.ts
Lines 77 to 317 in 376ba8c
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]; |
OpenSearch-Dashboards/src/core/server/opensearch/opensearch_service.test.ts
Lines 153 to 344 in 376ba8c
"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' |
OpenSearch-Dashboards/src/core/server/opensearch/client/cluster_client.test.ts
Lines 46 to 391 in 376ba8c
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'], |
OpenSearch-Dashboards/src/core/server/opensearch/client/cluster_client.ts
Lines 118 to 121 in 376ba8c
...this.config.requestHeadersWhitelist, | |
]); | |
} else { | |
scopedHeaders = filterHeaders(request?.headers ?? {}, this.config.requestHeadersWhitelist); |
OpenSearch-Dashboards/src/core/server/opensearch/legacy/cluster_client.test.ts
Lines 262 to 578 in 376ba8c
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, |
OpenSearch-Dashboards/src/core/server/opensearch/legacy/opensearch_client_config.test.ts
Lines 51 to 662 in 376ba8c
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: [], |
OpenSearch-Dashboards/src/core/server/opensearch/legacy/opensearch_client_config.ts
Line 55 in 376ba8c
| 'requestHeadersWhitelist' |
requestHeadersWhitelist: string[]; |
OpenSearch-Dashboards/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
Line 86 in 376ba8c
const filteredHeaders = filterHeaders(headers, opensearchConfig.requestHeadersWhitelist); |
OpenSearch-Dashboards/src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts
Line 45 in 376ba8c
requestHeadersWhitelist: [], |
Line 56 in 376ba8c
requestHeadersWhitelist: [], |
opensearch.requestHeadersWhitelistConfigured
config
requestHeadersWhitelistConfigured: boolean; |
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.mock.ts
Line 48 in 376ba8c
requestHeadersWhitelistConfigured: false, |
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.test.ts
Line 185 in 376ba8c
"requestHeadersWhitelistConfigured": false, |
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 175 in 376ba8c
requestHeadersWhitelistConfigured: isConfigured.stringOrArray( |
requestHeadersWhitelistConfigured: boolean; |
Line 48 in 376ba8c
requestHeadersWhitelistConfigured: { type: 'boolean' }, |
server.compression.referrerWhitelist
config
'server.compression.referrerWhitelist', |
'--server.compression.referrerWhitelist=["some-host.com"]', |
OpenSearch-Dashboards/test/api_integration/apis/core/index.js
Lines 47 to 57 in 376ba8c
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
OpenSearch-Dashboards/packages/osd-config/src/legacy/legacy_object_to_config_adapter.test.ts
Lines 108 to 134 in 376ba8c
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: [], | |
}, |
Lines 29 to 60 in 376ba8c
"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 [], |
OpenSearch-Dashboards/src/core/server/config/deprecation/core_deprecations.test.ts
Lines 95 to 102 in 376ba8c
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.", |
OpenSearch-Dashboards/src/core/server/config/deprecation/core_deprecations.ts
Lines 53 to 158 in 376ba8c
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
OpenSearch-Dashboards/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.test.ts
Line 182 in 376ba8c
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. \ |
# expect the ones in the whitelist |
OpenSearch-Dashboards/src/core/server/http/http_server.test.ts
Lines 872 to 881 in 376ba8c
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), |
OpenSearch-Dashboards/src/plugins/home/public/application/components/tutorial/content.js
Lines 35 to 43 in 376ba8c
const whiteListedRules = ['backticks', 'emphasis', 'link', 'list']; | |
export function Content({ text }) { | |
return ( | |
<Markdown | |
className="euiText" | |
markdown={text} | |
openLinksInNewTab={true} | |
whiteListedRules={whiteListedRules} |
Line 8 in 376ba8c
whiteListedRules={ |
a security tool for macOS that monitors process executions and can blacklist/whitelist binaries. \ |
OpenSearch-Dashboards/src/plugins/opensearch_dashboards_react/public/markdown/markdown.test.tsx
Lines 89 to 110 in 376ba8c
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'] }); |
OpenSearch-Dashboards/src/plugins/opensearch_dashboards_react/public/markdown/markdown.tsx
Lines 40 to 110 in 376ba8c
* 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); |
Line 27 in 376ba8c
exports[`props whiteListedRules 1`] = ` |
OpenSearch-Dashboards/src/plugins/vis_type_timeseries/common/ui_restrictions.ts
Lines 42 to 52 in 376ba8c
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', |
Lines 52 to 68 in 376ba8c
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, |
Lines 53 to 55 in 376ba8c
whiteListedMetrics: { '*': true }, | |
whiteListedGroupByFields: { '*': true }, | |
whiteListedTimerangeModes: { '*': true }, |
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:
Tasks
http.compression.referrerWhitelist
in favor ofhttp.compression.referrerAllowlist
( Unknown configuration key(s)/ does not exists)http.compression.referrerWhitelistConfigured
in favor ofhttp.compression.referrerAllowlistConfigured
( Unknown configuration key(s)/ does not exists)http.xsrf.whitelist
in favor ofhttp.xsrf.allowlist
( Unknown configuration key(s)/ does not exists)http.xsrf.whitelistConfigured
in favor ofhttp.xsrf.allowlistConfigured
opensearch.requestHeadersWhitelist
in favor ofopensearch.requestHeadersAllowlist
opensearch.requestHeadersWhitelistConfigured
in favor ofopensearch.requestHeadersAllowlistConfigured
server.compression.referrerWhitelist
in favor ofserver.compression.referrerAllowlist
server.xsrf.whitelist
in favor ofserver.xsrf.allowlist
blacklist
withdenylist
in comments and local variableswhitelist
withallowlist
in comments and local variablesNon-inclusive instances of
blacklist
andwhitelist
APIs, configuration
http.compression.referrerWhitelist
configOpenSearch-Dashboards/src/core/server/http/http_config.test.ts
Lines 235 to 268 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/http_config.ts
Line 80 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/http_config.ts
Lines 120 to 121 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/http_server.test.ts
Lines 857 to 862 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/http_server.ts
Line 285 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/__snapshots__/http_config.test.ts.snap
Lines 103 to 116 in 376ba8c
http.compression.referrerWhitelistConfigured
configOpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.mock.ts
Line 68 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.test.ts
Line 120 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 201 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/types.ts
Line 110 in 376ba8c
OpenSearch-Dashboards/src/plugins/opensearch_dashboards_usage_collection/server/collectors/core/core_usage_collector.ts
Line 75 in 376ba8c
http.xsrf.whitelist
configOpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 205 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/cookie_session_storage.test.ts
Line 75 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/http_config.test.ts
Lines 179 to 187 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/http_config.ts
Line 96 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/http_config.ts
Line 156 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/lifecycle_handlers.test.ts
Lines 76 to 170 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/lifecycle_handlers.ts
Lines 43 to 48 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/test_utils.ts
Line 57 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/__snapshots__/http_config.test.ts.snap
Line 86 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/integration_tests/lifecycle_handlers.test.ts
Lines 50 to 249 in 376ba8c
http.xsrf.whitelistConfigured
configOpenSearch-Dashboards/src/core/server/server.api.md
Line 419 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.mock.ts
Line 113 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.test.ts
Line 171 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 205 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 205 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/types.ts
Line 114 in 376ba8c
OpenSearch-Dashboards/src/plugins/opensearch_dashboards_usage_collection/server/collectors/core/core_usage_collector.ts
Line 79 in 376ba8c
opensearch.requestHeadersWhitelist
configOpenSearch-Dashboards/config/opensearch_dashboards.yml
Lines 90 to 93 in 376ba8c
OpenSearch-Dashboards/packages/osd-apm-config-loader/__fixtures__/en_var_ref_config.yml
Line 5 in 376ba8c
OpenSearch-Dashboards/packages/osd-apm-config-loader/src/utils/__snapshots__/read_config.test.ts.snap
Line 100 in 376ba8c
OpenSearch-Dashboards/packages/osd-config/__fixtures__/en_var_ref_config.yml
Line 5 in 376ba8c
OpenSearch-Dashboards/packages/osd-config/src/raw/__snapshots__/read_config.test.ts.snap
Line 100 in 376ba8c
OpenSearch-Dashboards/src/core/server/server.api.md
Line 353 in 376ba8c
OpenSearch-Dashboards/src/core/server/server.api.md
Line 415 in 376ba8c
OpenSearch-Dashboards/src/core/server/server.api.md
Line 1231 in 376ba8c
OpenSearch-Dashboards/src/core/server/server.api.md
Line 1424 in 376ba8c
OpenSearch-Dashboards/src/core/server/server.api.md
Line 1443 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 176 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/opensearch_config.test.ts
Lines 88 to 444 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/opensearch_config.ts
Lines 77 to 317 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/opensearch_service.test.ts
Lines 153 to 344 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/client/client_config.test.ts
Line 42 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/client/client_config.ts
Line 50 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/client/cluster_client.test.ts
Lines 46 to 391 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/client/cluster_client.ts
Lines 118 to 121 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/legacy/cluster_client.test.ts
Lines 262 to 578 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/legacy/cluster_client.ts
Line 229 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/legacy/opensearch_client_config.test.ts
Lines 51 to 662 in 376ba8c
OpenSearch-Dashboards/src/core/server/opensearch/legacy/opensearch_client_config.ts
Line 55 in 376ba8c
OpenSearch-Dashboards/src/plugins/console/server/types.ts
Line 47 in 376ba8c
OpenSearch-Dashboards/src/plugins/console/server/routes/api/console/proxy/create_handler.ts
Line 86 in 376ba8c
OpenSearch-Dashboards/src/plugins/console/server/routes/api/console/proxy/tests/mocks.ts
Line 45 in 376ba8c
OpenSearch-Dashboards/src/plugins/console/server/routes/api/console/proxy/tests/proxy_fallback.test.ts
Line 56 in 376ba8c
opensearch.requestHeadersWhitelistConfigured
configOpenSearch-Dashboards/src/core/server/server.api.md
Line 448 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.mock.ts
Line 48 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.test.ts
Line 185 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/core_usage_data_service.ts
Line 175 in 376ba8c
OpenSearch-Dashboards/src/core/server/core_usage_data/types.ts
Line 83 in 376ba8c
OpenSearch-Dashboards/src/plugins/opensearch_dashboards_usage_collection/server/collectors/core/core_usage_collector.ts
Line 48 in 376ba8c
server.compression.referrerWhitelist
configOpenSearch-Dashboards/src/core/server/config/deprecation/core_deprecations.ts
Line 160 in 376ba8c
OpenSearch-Dashboards/test/api_integration/config.js
Line 51 in 376ba8c
OpenSearch-Dashboards/test/api_integration/apis/core/index.js
Lines 47 to 57 in 376ba8c
server.xsrf.whitelist
configOpenSearch-Dashboards/packages/osd-config/src/legacy/legacy_object_to_config_adapter.test.ts
Lines 108 to 134 in 376ba8c
OpenSearch-Dashboards/packages/osd-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap
Lines 29 to 60 in 376ba8c
OpenSearch-Dashboards/src/core/server/config/deprecation/core_deprecations.test.ts
Lines 95 to 102 in 376ba8c
OpenSearch-Dashboards/src/core/server/config/deprecation/core_deprecations.ts
Lines 53 to 158 in 376ba8c
Other code
Comments/variables
OpenSearch-Dashboards/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.test.ts
Line 182 in 376ba8c
OpenSearch-Dashboards/src/plugins/home/server/tutorials/santa_logs/index.ts
Line 55 in 376ba8c
OpenSearch-Dashboards/packages/opensearch-safer-lodash-set/scripts/update.sh
Line 25 in 376ba8c
OpenSearch-Dashboards/src/core/server/http/http_server.test.ts
Lines 872 to 881 in 376ba8c
OpenSearch-Dashboards/src/dev/license_checker/config.ts
Line 33 in 376ba8c
OpenSearch-Dashboards/src/dev/license_checker/config.ts
Line 89 in 376ba8c
OpenSearch-Dashboards/src/dev/license_checker/index.ts
Line 31 in 376ba8c
OpenSearch-Dashboards/src/dev/license_checker/run_check_licenses_cli.ts
Lines 35 to 59 in 376ba8c
OpenSearch-Dashboards/src/plugins/home/public/application/components/tutorial/content.js
Lines 35 to 43 in 376ba8c
OpenSearch-Dashboards/src/plugins/home/public/application/components/tutorial/__snapshots__/content.test.js.snap
Line 8 in 376ba8c
OpenSearch-Dashboards/src/plugins/home/server/tutorials/santa_logs/index.ts
Line 55 in 376ba8c
OpenSearch-Dashboards/src/plugins/opensearch_dashboards_react/public/markdown/markdown.test.tsx
Lines 89 to 110 in 376ba8c
OpenSearch-Dashboards/src/plugins/opensearch_dashboards_react/public/markdown/markdown.tsx
Lines 40 to 110 in 376ba8c
OpenSearch-Dashboards/src/plugins/opensearch_dashboards_react/public/markdown/__snapshots__/markdown.test.tsx.snap
Line 27 in 376ba8c
OpenSearch-Dashboards/src/plugins/vis_type_timeseries/common/ui_restrictions.ts
Lines 42 to 52 in 376ba8c
OpenSearch-Dashboards/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js
Lines 52 to 68 in 376ba8c
OpenSearch-Dashboards/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js
Lines 53 to 55 in 376ba8c
The text was updated successfully, but these errors were encountered: