Skip to content

Commit

Permalink
[Multiple Datasources] Add TLS configuration for multiple data sources (
Browse files Browse the repository at this point in the history
opensearch-project#6171)

* Add TLS configuration for multiple data sources

Signed-off-by: Craig Perkins <cwperx@amazon.com>

* Add to CHANGELOG and add examples commented out in opensearch_dashboards.yml

Signed-off-by: Craig Perkins <craig5008@gmail.com>

* Add tests and replace instance of any

Signed-off-by: Craig Perkins <cwperx@amazon.com>

* Add tls config to legacy client

Signed-off-by: Craig Perkins <cwperx@amazon.com>

* Add test for certificate mode

Signed-off-by: Craig Perkins <cwperx@amazon.com>

* Respond to PR feedback

Signed-off-by: Craig Perkins <cwperx@amazon.com>

* Extract readCertificateAuthorities to util file and add more tests

Signed-off-by: Craig Perkins <cwperx@amazon.com>

---------

Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <craig5008@gmail.com>
  • Loading branch information
cwperks authored Mar 22, 2024
1 parent 0dce00a commit a9b400e
Show file tree
Hide file tree
Showing 9 changed files with 372 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Workspace] Validate if workspace exists when setup inside a workspace ([#6154](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6154))
- [Workspace] Register a workspace dropdown menu at the top of left nav bar ([#6150](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6150))
- [Multiple Datasource] Add icon in datasource table page to show the default datasource ([#6231](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6231))
- [Multiple Datasource] Add TLS configuration for multiple data sources ([#6171](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6171))

### 🐛 Bug Fixes

Expand Down
8 changes: 8 additions & 0 deletions config/opensearch_dashboards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@
# 'ff00::/8',
# ]

# Optional setting that enables you to specify a path to PEM files for the certificate
# authority for your connected datasources.
#data_source.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ]

# To disregard the validity of SSL certificates for connected data sources, change this setting's value to 'none'.
# Possible values include full, certificate and none
#data_source.ssl.verificationMode: full

# Set enabled false to hide authentication method in OpenSearch Dashboards.
# If this setting is commented then all 3 options will be available in OpenSearch Dashboards.
# Default value will be considered to True.
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/data_source/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ export const configSchema = schema.object({
defaultValue: new Array(32).fill(0),
}),
}),
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 })])
),
}),
clientPool: schema.object({
size: schema.number({ defaultValue: 5 }),
}),
Expand Down
97 changes: 90 additions & 7 deletions src/plugins/data_source/server/client/client_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
import { DataSourcePluginConfigType } from '../../config';
import { parseClientOptions } from './client_config';

const TEST_DATA_SOURCE_ENDPOINT = 'http://test.com/';
jest.mock('fs');
const mockReadFileSync: jest.Mock = jest.requireMock('fs').readFileSync;

const config = {
enabled: true,
clientPool: {
size: 5,
},
} as DataSourcePluginConfigType;
const TEST_DATA_SOURCE_ENDPOINT = 'http://test.com/';

describe('parseClientOptions', () => {
test('include the ssl client configs as defaults', () => {
const config = {
enabled: true,
clientPool: {
size: 5,
},
} as DataSourcePluginConfigType;

expect(parseClientOptions(config, TEST_DATA_SOURCE_ENDPOINT)).toEqual(
expect.objectContaining({
node: TEST_DATA_SOURCE_ENDPOINT,
Expand All @@ -26,4 +29,84 @@ describe('parseClientOptions', () => {
})
);
});

test('test ssl config with verification mode set to none', () => {
const config = {
enabled: true,
ssl: {
verificationMode: 'none',
},
clientPool: {
size: 5,
},
} as DataSourcePluginConfigType;
expect(parseClientOptions(config, TEST_DATA_SOURCE_ENDPOINT)).toEqual(
expect.objectContaining({
node: TEST_DATA_SOURCE_ENDPOINT,
ssl: {
requestCert: true,
rejectUnauthorized: false,
ca: [],
},
})
);
});

test('test ssl config with verification mode set to certificate', () => {
const config = {
enabled: true,
ssl: {
verificationMode: 'certificate',
certificateAuthorities: ['some-path'],
},
clientPool: {
size: 5,
},
} as DataSourcePluginConfigType;
mockReadFileSync.mockReset();
mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
const parsedConfig = parseClientOptions(config, TEST_DATA_SOURCE_ENDPOINT);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
mockReadFileSync.mockClear();
expect(parsedConfig).toEqual(
expect.objectContaining({
node: TEST_DATA_SOURCE_ENDPOINT,
ssl: {
requestCert: true,
rejectUnauthorized: true,
checkServerIdentity: expect.any(Function),
ca: ['content-of-some-path'],
},
})
);
expect(parsedConfig.ssl?.checkServerIdentity()).toBeUndefined();
});

test('test ssl config with verification mode set to full', () => {
const config = {
enabled: true,
ssl: {
verificationMode: 'full',
certificateAuthorities: ['some-path'],
},
clientPool: {
size: 5,
},
} as DataSourcePluginConfigType;
mockReadFileSync.mockReset();
mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
const parsedConfig = parseClientOptions(config, TEST_DATA_SOURCE_ENDPOINT);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
mockReadFileSync.mockClear();
expect(parsedConfig).toEqual(
expect.objectContaining({
node: TEST_DATA_SOURCE_ENDPOINT,
ssl: {
requestCert: true,
rejectUnauthorized: true,
ca: ['content-of-some-path'],
},
})
);
});
});
46 changes: 42 additions & 4 deletions src/plugins/data_source/server/client/client_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@
*/

import { ClientOptions } from '@opensearch-project/opensearch';
import { checkServerIdentity } from 'tls';
import { DataSourcePluginConfigType } from '../../config';
import { readCertificateAuthorities } from '../util/tls_settings_provider';

/** @internal */
type DataSourceSSLConfigOptions = Partial<{
requestCert: boolean;
rejectUnauthorized: boolean;
checkServerIdentity: typeof checkServerIdentity;
ca: string[];
}>;

/**
* Parse the client options from given data source config and endpoint
Expand All @@ -18,12 +28,40 @@ export function parseClientOptions(
endpoint: string,
registeredSchema: any[]
): ClientOptions {
const sslConfig: DataSourceSSLConfigOptions = {
requestCert: true,
rejectUnauthorized: true,
};

if (config.ssl) {
const verificationMode = config.ssl.verificationMode;
switch (verificationMode) {
case 'none':
sslConfig.rejectUnauthorized = false;
break;
case 'certificate':
sslConfig.rejectUnauthorized = true;

// by default, NodeJS is checking the server identify
sslConfig.checkServerIdentity = () => undefined;
break;
case 'full':
sslConfig.rejectUnauthorized = true;
break;
default:
throw new Error(`Unknown ssl verificationMode: ${verificationMode}`);
}

const { certificateAuthorities } = readCertificateAuthorities(
config.ssl?.certificateAuthorities
);

sslConfig.ca = certificateAuthorities || [];
}

const clientOptions: ClientOptions = {
node: endpoint,
ssl: {
requestCert: true,
rejectUnauthorized: true,
},
ssl: sslConfig,
plugins: registeredSchema,
};

Expand Down
94 changes: 87 additions & 7 deletions src/plugins/data_source/server/legacy/client_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
import { DataSourcePluginConfigType } from '../../config';
import { parseClientOptions } from './client_config';

const TEST_DATA_SOURCE_ENDPOINT = 'http://test.com/';
jest.mock('fs');
const mockReadFileSync: jest.Mock = jest.requireMock('fs').readFileSync;

const config = {
enabled: true,
clientPool: {
size: 5,
},
} as DataSourcePluginConfigType;
const TEST_DATA_SOURCE_ENDPOINT = 'http://test.com/';

describe('parseClientOptions', () => {
test('include the ssl client configs as defaults', () => {
const config = {
enabled: true,
clientPool: {
size: 5,
},
} as DataSourcePluginConfigType;

expect(parseClientOptions(config, TEST_DATA_SOURCE_ENDPOINT)).toEqual(
expect.objectContaining({
host: TEST_DATA_SOURCE_ENDPOINT,
Expand All @@ -25,4 +28,81 @@ describe('parseClientOptions', () => {
})
);
});

test('test ssl config with verification mode set to none', () => {
const config = {
enabled: true,
ssl: {
verificationMode: 'none',
},
clientPool: {
size: 5,
},
} as DataSourcePluginConfigType;
expect(parseClientOptions(config, TEST_DATA_SOURCE_ENDPOINT)).toEqual(
expect.objectContaining({
host: TEST_DATA_SOURCE_ENDPOINT,
ssl: {
rejectUnauthorized: false,
ca: [],
},
})
);
});

test('test ssl config with verification mode set to certificate', () => {
const config = {
enabled: true,
ssl: {
verificationMode: 'certificate',
certificateAuthorities: ['some-path'],
},
clientPool: {
size: 5,
},
} as DataSourcePluginConfigType;
mockReadFileSync.mockReset();
mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
const parsedConfig = parseClientOptions(config, TEST_DATA_SOURCE_ENDPOINT);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
mockReadFileSync.mockClear();
expect(parsedConfig).toEqual(
expect.objectContaining({
host: TEST_DATA_SOURCE_ENDPOINT,
ssl: {
rejectUnauthorized: true,
checkServerIdentity: expect.any(Function),
ca: ['content-of-some-path'],
},
})
);
expect(parsedConfig.ssl?.checkServerIdentity()).toBeUndefined();
});

test('test ssl config with verification mode set to full', () => {
const config = {
enabled: true,
ssl: {
verificationMode: 'full',
certificateAuthorities: ['some-path'],
},
clientPool: {
size: 5,
},
} as DataSourcePluginConfigType;
mockReadFileSync.mockReset();
mockReadFileSync.mockImplementation((path: string) => `content-of-${path}`);
const parsedConfig = parseClientOptions(config, TEST_DATA_SOURCE_ENDPOINT);
expect(mockReadFileSync).toHaveBeenCalledTimes(1);
mockReadFileSync.mockClear();
expect(parsedConfig).toEqual(
expect.objectContaining({
host: TEST_DATA_SOURCE_ENDPOINT,
ssl: {
rejectUnauthorized: true,
ca: ['content-of-some-path'],
},
})
);
});
});
44 changes: 41 additions & 3 deletions src/plugins/data_source/server/legacy/client_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@
*/

import { ConfigOptions } from 'elasticsearch';
import { checkServerIdentity } from 'tls';
import { DataSourcePluginConfigType } from '../../config';
import { readCertificateAuthorities } from '../util/tls_settings_provider';

/** @internal */
type LegacyDataSourceSSLConfigOptions = Partial<{
requestCert: boolean;
rejectUnauthorized: boolean;
checkServerIdentity: typeof checkServerIdentity;
ca: string[];
}>;

/**
* Parse the client options from given data source config and endpoint
Expand All @@ -18,11 +28,39 @@ export function parseClientOptions(
endpoint: string,
registeredSchema: any[]
): ConfigOptions {
const sslConfig: LegacyDataSourceSSLConfigOptions = {
rejectUnauthorized: true,
};

if (config.ssl) {
const verificationMode = config.ssl.verificationMode;
switch (verificationMode) {
case 'none':
sslConfig.rejectUnauthorized = false;
break;
case 'certificate':
sslConfig.rejectUnauthorized = true;

// by default, NodeJS is checking the server identify
sslConfig.checkServerIdentity = () => undefined;
break;
case 'full':
sslConfig.rejectUnauthorized = true;
break;
default:
throw new Error(`Unknown ssl verificationMode: ${verificationMode}`);
}

const { certificateAuthorities } = readCertificateAuthorities(
config.ssl?.certificateAuthorities
);

sslConfig.ca = certificateAuthorities || [];
}

const configOptions: ConfigOptions = {
host: endpoint,
ssl: {
rejectUnauthorized: true,
},
ssl: sslConfig,
plugins: registeredSchema,
};

Expand Down
Loading

0 comments on commit a9b400e

Please sign in to comment.