Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Multiple DataSource] Refactor test connection to support SigV4 auth type #3456

Merged
merged 2 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Add disablePrototypePoisoningProtection configuration to prevent JS client from erroring when cluster utilizes JS reserved words ([#2992](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2992))
- [Multiple DataSource] Add support for SigV4 authentication ([#3058](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3058))
- Make build scripts find and use the latest version of Node.js that satisfies `engines.node` ([#3467](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3467))
- [Multiple DataSource] Refactor test connection to support SigV4 auth type ([#3456](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3456))

### 🐛 Bug Fixes

Expand Down
68 changes: 63 additions & 5 deletions src/plugins/data_source/server/client/configure_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import { SavedObjectsClientContract } from '../../../../core/server';
import { loggingSystemMock, savedObjectsClientMock } from '../../../../core/server/mocks';
import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common';
import { DataSourceAttributes, AuthType } from '../../common/data_sources/types';
import {
DataSourceAttributes,
AuthType,
UsernamePasswordTypedContent,
SigV4Content,
} from '../../common/data_sources/types';
import { DataSourcePluginConfigType } from '../../config';
import { ClientMock, parseClientOptionsMock } from './configure_client.test.mocks';
import { OpenSearchClientPoolSetup } from './client_pool';
Expand All @@ -31,6 +36,8 @@ describe('configureClient', () => {
let dataSourceAttr: DataSourceAttributes;
let dsClient: ReturnType<typeof opensearchClientMock.createInternalClient>;
let dataSourceClientParams: DataSourceClientParams;
let usernamePasswordAuthContent: UsernamePasswordTypedContent;
let sigV4AuthContent: SigV4Content;

beforeEach(() => {
dsClient = opensearchClientMock.createInternalClient();
Expand All @@ -51,15 +58,24 @@ describe('configureClient', () => {
rejectUnauthorized: true,
},
} as ClientOptions;

usernamePasswordAuthContent = {
username: 'username',
password: 'password',
};

sigV4AuthContent = {
region: 'us-east-1',
accessKey: 'accessKey',
secretKey: 'secretKey',
};

dataSourceAttr = {
title: 'title',
endpoint: 'http://localhost',
auth: {
type: AuthType.UsernamePasswordType,
credentials: {
username: 'username',
password: 'password',
},
credentials: usernamePasswordAuthContent,
},
} as DataSourceAttributes;

Expand Down Expand Up @@ -126,6 +142,48 @@ describe('configureClient', () => {
expect(client).toBe(dsClient.child.mock.results[0].value);
});

test('configure client with auth.type == sigv4, will first call decodeAndDecrypt()', async () => {
savedObjectsMock.get.mockReset().mockResolvedValueOnce({
id: DATA_SOURCE_ID,
type: DATA_SOURCE_SAVED_OBJECT_TYPE,
attributes: {
...dataSourceAttr,
auth: {
type: AuthType.SigV4,
credentials: sigV4AuthContent,
},
},
references: [],
});

const decodeAndDecryptSpy = jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({
decryptedText: 'accessKey',
encryptionContext: { endpoint: 'http://localhost' },
});
await configureClient(dataSourceClientParams, clientPoolSetup, config, logger);

expect(ClientMock).toHaveBeenCalledTimes(1);
expect(savedObjectsMock.get).toHaveBeenCalledTimes(1);
expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(2);
});

test('configure test client for non-exist datasource should not call saved object api, nor decode any credential', async () => {
const decodeAndDecryptSpy = jest.spyOn(cryptographyMock, 'decodeAndDecrypt').mockResolvedValue({
decryptedText: 'password',
encryptionContext: { endpoint: 'http://localhost' },
});
const testClientParams: DataSourceClientParams = {
...dataSourceClientParams,
testClientDataSourceAttr: dataSourceAttr,
dataSourceId: undefined,
};
await configureClient(testClientParams, clientPoolSetup, config, logger);

expect(ClientMock).toHaveBeenCalledTimes(1);
expect(savedObjectsMock.get).not.toHaveBeenCalled();
expect(decodeAndDecryptSpy).not.toHaveBeenCalled();
});

test('configure client with auth.type == username_password and password contaminated', async () => {
const decodeAndDecryptSpy = jest
.spyOn(cryptographyMock, 'decodeAndDecrypt')
Expand Down
71 changes: 27 additions & 44 deletions src/plugins/data_source/server/client/configure_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,35 @@ import {
} from './configure_client_utils';

export const configureClient = async (
{ dataSourceId, savedObjects, cryptography }: DataSourceClientParams,
{ dataSourceId, savedObjects, cryptography, testClientDataSourceAttr }: DataSourceClientParams,
openSearchClientPoolSetup: OpenSearchClientPoolSetup,
config: DataSourcePluginConfigType,
logger: Logger
): Promise<Client> => {
let dataSource;
let requireDecryption = true;

try {
const dataSource = await getDataSource(dataSourceId!, savedObjects);
// configure test client
if (testClientDataSourceAttr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using testClientDataSourceAttr as condition seems a bit confuse to me, Wondering if we should pass an explicit flag from the browser side to indicate testing client instead of using the existence of some params. like isPreFlightClient=true

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am removing all the separate functions to getTestClient, so that we can reuse the same logic for both test client and non-test client. We still need to keep the testClientDataSourceAttr even tho we add a flag.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I am fine to merge them, and testClientDataSourceAttr is still needed, but in future the api could accept other dataSourceAttr but not necessarily for test client.

const {
auth: { type, credentials },
} = testClientDataSourceAttr;
// handle test connection case when changing non-credential field of existing data source
if (
dataSourceId &&
((type === AuthType.UsernamePasswordType && !credentials?.password) ||
(type === AuthType.SigV4 && !credentials?.accessKey && !credentials?.secretKey))
Comment on lines +49 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, how about give variable to these conditions for readability?
e.g. const validSigV4 = type === AuthType.SigV4 && !credentials?.accessKey && !credentials?.secretKey

Also we don't check if region is not empty?

) {
dataSource = await getDataSource(dataSourceId, savedObjects);
} else {
dataSource = testClientDataSourceAttr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comment here? explain which case will enter this block

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's comment in line 7

// handle test connection case when changing non-credential field of existing data source

requireDecryption = false;
}
} else {
dataSource = await getDataSource(dataSourceId!, savedObjects);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me add more comments

}

const rootClient = getRootClient(
dataSource,
openSearchClientPoolSetup.getClientFromPool,
Expand All @@ -48,7 +70,8 @@ export const configureClient = async (
config,
cryptography,
rootClient,
dataSourceId
dataSourceId,
requireDecryption
);
} catch (error: any) {
logger.error(
Expand All @@ -59,46 +82,6 @@ export const configureClient = async (
}
};

export const configureTestClient = async (
{ savedObjects, cryptography, dataSourceId }: DataSourceClientParams,
dataSourceAttr: DataSourceAttributes,
openSearchClientPoolSetup: OpenSearchClientPoolSetup,
config: DataSourcePluginConfigType,
logger: Logger
): Promise<Client> => {
try {
const {
auth: { type, credentials },
} = dataSourceAttr;
let requireDecryption = false;

const rootClient = getRootClient(
dataSourceAttr,
openSearchClientPoolSetup.getClientFromPool,
dataSourceId
) as Client;

if (type === AuthType.UsernamePasswordType && !credentials?.password && dataSourceId) {
dataSourceAttr = await getDataSource(dataSourceId, savedObjects);
requireDecryption = true;
}

return getQueryClient(
dataSourceAttr,
openSearchClientPoolSetup.addClientToPool,
config,
cryptography,
rootClient,
dataSourceId,
requireDecryption
);
} catch (error: any) {
logger.error(`Failed to get test client. ${error}: ${error.stack}`);
// Re-throw as DataSourceError
throw createDataSourceError(error);
}
};

/**
* Create a child client object with given auth info.
*
Expand All @@ -108,7 +91,7 @@ export const configureTestClient = async (
* @param config data source config
* @param addClientToPool function to add client to client pool
* @param dataSourceId id of data source saved Object
* @param requireDecryption boolean
* @param requireDecryption false when creating test client before data source exists
* @returns Promise of query client
*/
const getQueryClient = async (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ export const getRootClient = (
): Client | LegacyClient | undefined => {
const {
auth: { type },
lastUpdatedTime,
} = dataSourceAttr;
let cachedClient;
const cacheKey = generateCacheKey(dataSourceAttr, dataSourceId);
const cachedClient = getClientFromPool(cacheKey, type);

// return undefined when building SigV4 test client with new credentials
if (type === AuthType.SigV4) {
cachedClient = dataSourceId && lastUpdatedTime ? getClientFromPool(cacheKey, type) : undefined;
} else {
cachedClient = getClientFromPool(cacheKey, type);
}

return cachedClient;
};
Expand Down
23 changes: 2 additions & 21 deletions src/plugins/data_source/server/data_source_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { DataSourcePluginConfigType } from '../config';
import { OpenSearchClientPool } from './client';
import { configureLegacyClient } from './legacy';
import { DataSourceClientParams } from './types';
import { DataSourceAttributes } from '../common/data_sources';
import { configureTestClient, configureClient } from './client/configure_client';
import { configureClient } from './client/configure_client';
export interface DataSourceServiceSetup {
getDataSourceClient: (params: DataSourceClientParams) => Promise<OpenSearchClient>;

Expand All @@ -22,11 +21,6 @@ export interface DataSourceServiceSetup {
options?: LegacyCallAPIOptions
) => Promise<unknown>;
};

getTestingClient: (
params: DataSourceClientParams,
dataSource: DataSourceAttributes
) => Promise<OpenSearchClient>;
}
export class DataSourceService {
private readonly openSearchClientPool: OpenSearchClientPool;
Expand All @@ -49,19 +43,6 @@ export class DataSourceService {
return configureClient(params, opensearchClientPoolSetup, config, this.logger);
};

const getTestingClient = async (
params: DataSourceClientParams,
dataSource: DataSourceAttributes
): Promise<OpenSearchClient> => {
return configureTestClient(
params,
dataSource,
opensearchClientPoolSetup,
config,
this.logger
);
};

const getDataSourceLegacyClient = (params: DataSourceClientParams) => {
return {
callAPI: (
Expand All @@ -79,7 +60,7 @@ export class DataSourceService {
};
};

return { getDataSourceClient, getDataSourceLegacyClient, getTestingClient };
return { getDataSourceClient, getDataSourceLegacyClient };
}

start() {}
Expand Down
20 changes: 13 additions & 7 deletions src/plugins/data_source/server/routes/test_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { schema } from '@osd/config-schema';
import { IRouter, OpenSearchClient } from 'opensearch-dashboards/server';
import { DataSourceAttributes } from '../../common/data_sources';
import { AuthType, DataSourceAttributes } from '../../common/data_sources';
import { DataSourceConnectionValidator } from './data_source_connection_validator';
import { DataSourceServiceSetup } from '../data_source_service';
import { CryptographyServiceSetup } from '../cryptography_service';
Expand All @@ -26,14 +26,20 @@ export const registerTestConnectionRoute = (
auth: schema.maybe(
schema.object({
type: schema.oneOf([
schema.literal('username_password'),
schema.literal('no_auth'),
schema.literal(AuthType.UsernamePasswordType),
schema.literal(AuthType.NoAuth),
schema.literal(AuthType.SigV4),
]),
credentials: schema.oneOf([
schema.object({
username: schema.string(),
password: schema.string(),
}),
schema.object({
region: schema.string(),
accessKey: schema.string(),
secretKey: schema.string(),
}),
schema.literal(null),
]),
})
Expand All @@ -46,13 +52,13 @@ export const registerTestConnectionRoute = (
const { dataSourceAttr, id: dataSourceId } = request.body;

try {
const dataSourceClient: OpenSearchClient = await dataSourceServiceSetup.getTestingClient(
const dataSourceClient: OpenSearchClient = await dataSourceServiceSetup.getDataSourceClient(
{
dataSourceId,
savedObjects: context.core.savedObjects.client,
cryptography,
},
dataSourceAttr as DataSourceAttributes
dataSourceId,
testClientDataSourceAttr: dataSourceAttr as DataSourceAttributes,
}
);
const dsValidator = new DataSourceConnectionValidator(dataSourceClient);

Expand Down
9 changes: 6 additions & 3 deletions src/plugins/data_source/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
OpenSearchClient,
SavedObjectsClientContract,
} from 'src/core/server';
import { DataSourceAttributes } from '../common/data_sources';

import { CryptographyServiceSetup } from './cryptography_service';
import { DataSourceError } from './lib/error';
Expand All @@ -19,11 +20,13 @@ export interface LegacyClientCallAPIParams {
}

export interface DataSourceClientParams {
// id is optional when creating test client
dataSourceId?: string;
// this saved objects client is used to fetch data source on behalf of users, caller should pass scoped saved objects client
// to fetch data source on behalf of users, caller should pass scoped saved objects client
savedObjects: SavedObjectsClientContract;
cryptography: CryptographyServiceSetup;
// optional when creating test client, required for normal client
dataSourceId?: string;
// required when creating test client
testClientDataSourceAttr?: DataSourceAttributes;
}

export interface DataSourcePluginRequestContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,8 +581,7 @@ export class CreateDataSourceForm extends React.Component<
<EuiButton
type="submit"
fill={false}
// TODO: remove the type check when working on https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3418
disabled={!this.isFormValid() || this.state.auth.type === AuthType.SigV4}
disabled={!this.isFormValid()}
onClick={this.onClickTestConnection}
data-test-subj="createDataSourceTestConnectionButton"
>
Expand Down
Loading