diff --git a/src/plugins/data_source/audit_config.ts b/src/plugins/data_source/audit_config.ts new file mode 100644 index 000000000000..d8c99fbfa846 --- /dev/null +++ b/src/plugins/data_source/audit_config.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +// eslint-disable-next-line @osd/eslint/no-restricted-paths +import { DateConversion } from '../../../src/core/server/logging/layouts/conversions'; + +const patternSchema = schema.string({ + validate: (string) => { + DateConversion.validate!(string); + }, +}); + +const patternLayout = schema.object({ + highlight: schema.maybe(schema.boolean()), + kind: schema.literal('pattern'), + pattern: schema.maybe(patternSchema), +}); + +const jsonLayout = schema.object({ + kind: schema.literal('json'), +}); + +export const fileAppenderSchema = schema.object( + { + kind: schema.literal('file'), + layout: schema.oneOf([patternLayout, jsonLayout]), + path: schema.string(), + }, + { + defaultValue: { + kind: 'file', + layout: { + kind: 'pattern', + highlight: true, + }, + path: '/tmp/opensearch-dashboards-data-source-audit.log', + }, + } +); diff --git a/src/plugins/data_source/config.ts b/src/plugins/data_source/config.ts index 95e8bc3f96bb..d7579026c6e2 100644 --- a/src/plugins/data_source/config.ts +++ b/src/plugins/data_source/config.ts @@ -4,6 +4,7 @@ */ import { schema, TypeOf } from '@osd/config-schema'; +import { fileAppenderSchema } from './audit_config'; const KEY_NAME_MIN_LENGTH: number = 1; const KEY_NAME_MAX_LENGTH: number = 100; @@ -32,6 +33,10 @@ export const configSchema = schema.object({ clientPool: schema.object({ size: schema.number({ defaultValue: 5 }), }), + audit: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + appender: fileAppenderSchema, + }), }); export type DataSourcePluginConfigType = TypeOf; diff --git a/src/plugins/data_source/server/audit/logging_auditor.ts b/src/plugins/data_source/server/audit/logging_auditor.ts new file mode 100644 index 000000000000..bc1f5bc57dcb --- /dev/null +++ b/src/plugins/data_source/server/audit/logging_auditor.ts @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuditableEvent, Auditor, Logger, OpenSearchDashboardsRequest } from 'src/core/server'; + +export class LoggingAuditor implements Auditor { + constructor( + private readonly request: OpenSearchDashboardsRequest, + private readonly logger: Logger + ) {} + + public withAuditScope(name: string) {} + + public add(event: AuditableEvent) { + const message = event.message; + const meta = { + type: event.type, + }; + this.logger.info(message, meta); + } +} diff --git a/src/plugins/data_source/server/data_source_service.ts b/src/plugins/data_source/server/data_source_service.ts index b7071962f5ab..1bab1a1d0b0b 100644 --- a/src/plugins/data_source/server/data_source_service.ts +++ b/src/plugins/data_source/server/data_source_service.ts @@ -3,9 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Logger, OpenSearchClient, SavedObjectsClientContract } from '../../../../src/core/server'; +import { + Auditor, + Logger, + OpenSearchClient, + SavedObjectsClientContract, +} from '../../../../src/core/server'; import { DataSourcePluginConfigType } from '../config'; -import { OpenSearchClientPool, configureClient } from './client'; +import { configureClient, OpenSearchClientPool } from './client'; import { CryptographyClient } from './cryptography'; export interface DataSourceServiceSetup { getDataSourceClient: ( diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts index 49d5418c86bc..0c75370af122 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -3,30 +3,39 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { first } from 'rxjs/operators'; import { OpenSearchClientError } from '@opensearch-project/opensearch/lib/errors'; -import { dataSource, credential, CredentialSavedObjectsClientWrapper } from './saved_objects'; -import { DataSourcePluginConfigType } from '../config'; +import { Observable } from 'rxjs'; +import { first, map } from 'rxjs/operators'; import { - PluginInitializerContext, + Auditor, + AuditorFactory, CoreSetup, CoreStart, - Plugin, - Logger, IContextProvider, + Logger, + LoggerContextConfigInput, + OpenSearchDashboardsRequest, + Plugin, + PluginInitializerContext, RequestHandler, } from '../../../../src/core/server'; +import { DataSourcePluginConfigType } from '../config'; +import { LoggingAuditor } from './audit/logging_auditor'; +import { CryptographyClient } from './cryptography'; import { DataSourceService, DataSourceServiceSetup } from './data_source_service'; +import { credential, CredentialSavedObjectsClientWrapper, dataSource } from './saved_objects'; import { DataSourcePluginSetup, DataSourcePluginStart } from './types'; -import { CryptographyClient } from './cryptography'; - +// eslint-disable-next-line @osd/eslint/no-restricted-paths +import { ensureRawRequest } from '../../../../src/core/server/http/router'; export class DataSourcePlugin implements Plugin { private readonly logger: Logger; private readonly dataSourceService: DataSourceService; + private readonly config$: Observable; constructor(private initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); this.dataSourceService = new DataSourceService(this.logger.get('data-source-service')); + this.config$ = this.initializerContext.config.create(); } public async setup(core: CoreSetup) { @@ -38,8 +47,7 @@ export class DataSourcePlugin implements Plugin(); - const config: DataSourcePluginConfigType = await config$.pipe(first()).toPromise(); + const config: DataSourcePluginConfigType = await this.config$.pipe(first()).toPromise(); // Fetch configs used to create credential saved objects client wrapper const { wrappingKeyName, wrappingKeyNamespace, wrappingKey } = config.encryption; @@ -63,10 +71,40 @@ export class DataSourcePlugin implements Plugin( + map((dataSourceConfig) => ({ + appenders: { + auditTrailAppender: dataSourceConfig.audit.appender, + }, + loggers: [ + { + context: 'audit', + level: dataSourceConfig.audit.enabled ? 'info' : 'off', + appenders: ['auditTrailAppender'], + }, + ], + })) + ) + ); + + const auditorFactory: AuditorFactory = { + asScoped: (request: OpenSearchDashboardsRequest) => { + return new LoggingAuditor(request, this.logger.get('audit')); + }, + }; + core.auditTrail.register(auditorFactory); + const auditTrailPromise = core.getStartServices().then(([coreStart]) => coreStart.auditTrail); + // Register data source plugin context to route handler context core.http.registerRouteHandlerContext( 'dataSource', - this.createDataSourceRouteHandlerContext(dataSourceService, cryptographyClient, this.logger) + this.createDataSourceRouteHandlerContext( + dataSourceService, + cryptographyClient, + this.logger, + auditTrailPromise + ) ); return {}; @@ -74,6 +112,7 @@ export class DataSourcePlugin implements Plugin ): IContextProvider, 'dataSource'> => { return (context, req) => { return { opensearch: { getClient: (dataSourceId: string) => { try { + const auditor = auditTrailPromise.then((auditTrail) => auditTrail.asScoped(req)); + this.logAuditMessage(auditor, dataSourceId, req); + return dataSourceService.getDataSourceClient( dataSourceId, context.core.savedObjects.client, @@ -107,4 +150,28 @@ export class DataSourcePlugin implements Plugin, + dataSourceId: string, + request: OpenSearchDashboardsRequest + ) { + const auditor = await auditorPromise; + const auditMessage = this.getAuditMessage(request, dataSourceId); + + auditor.add({ + message: auditMessage, + type: 'opensearch.dataSourceClient.fetchClient', + }); + } + + private getAuditMessage(request: OpenSearchDashboardsRequest, dataSourceId: string) { + const rawRequest = ensureRawRequest(request); + const remoteAddress = rawRequest?.info?.remoteAddress; + const xForwardFor = request.headers['x-forwarded-for']; + + return xForwardFor + ? `${remoteAddress} attempted accessing through ${xForwardFor} on data source: ${dataSourceId}` + : `${remoteAddress} attempted accessing on data source: ${dataSourceId}`; + } }