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

[RBAC][RAC] - PR cleanup and tests #5

Merged
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
400 changes: 0 additions & 400 deletions x-pack/plugins/rule_registry/server/alert_data_client/alert_client.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { PublicMethodsOf } from '@kbn/utility-types';
import { AlertsClient } from './alerts_client';

type Schema = PublicMethodsOf<AlertsClient>;
export type AlertsClientMock = jest.Mocked<Schema>;

const createAlertsClientMock = () => {
const mocked: AlertsClientMock = {
get: jest.fn(),
getAlertsIndex: jest.fn(),
update: jest.fn(),
};
return mocked;
};

export const alertsClientMock: {
create: () => AlertsClientMock;
} = {
create: createAlertsClientMock,
};
193 changes: 193 additions & 0 deletions x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { PublicMethodsOf } from '@kbn/utility-types';
import { AlertTypeParams } from '../../../alerting/server';
import {
ReadOperations,
AlertingAuthorization,
WriteOperations,
AlertingAuthorizationEntity,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../alerting/server/authorization';
import { Logger, ElasticsearchClient } from '../../../../../src/core/server';
import { alertAuditEvent, AlertAuditAction } from './audit_events';
import { RuleDataPluginService } from '../rule_data_plugin_service';
import { AuditLogger } from '../../../security/server';
import { ParsedTechnicalFields } from '../../common/parse_technical_fields';

export interface ConstructorOptions {
logger: Logger;
authorization: PublicMethodsOf<AlertingAuthorization>;
auditLogger?: AuditLogger;
esClient: ElasticsearchClient;
ruleDataService: RuleDataPluginService;
}

export interface UpdateOptions<Params extends AlertTypeParams> {
id: string;
data: {
status: string;
};
// observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191
assetName: string;
}

interface GetAlertParams {
id: string;
// observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191
assetName: string;
}

export class AlertsClient {
private readonly logger: Logger;
private readonly auditLogger?: AuditLogger;
private readonly authorization: PublicMethodsOf<AlertingAuthorization>;
private readonly esClient: ElasticsearchClient;
private readonly ruleDataService: RuleDataPluginService;

constructor({
auditLogger,
authorization,
logger,
esClient,
ruleDataService,
}: ConstructorOptions) {
this.logger = logger;
this.authorization = authorization;
this.esClient = esClient;
this.auditLogger = auditLogger;
this.ruleDataService = ruleDataService;
}

/**
* we are "hard coding" this string similar to how rule registry is doing it
* x-pack/plugins/apm/server/plugin.ts:191
*/
public getAlertsIndex(assetName: string) {
return this.ruleDataService?.getFullAssetName(assetName);
}

private async fetchAlert({ id, assetName }: GetAlertParams): Promise<ParsedTechnicalFields> {
try {
const result = await this.esClient.get<ParsedTechnicalFields>({
index: this.getAlertsIndex(assetName),
id,
});

if (
result == null ||
result.body == null ||
result.body._source == null ||
result.body._source['rule.id'] == null ||
result.body._source['kibana.rac.alert.owner'] == null
) {
const errorMessage = `[rac] - Unable to retrieve alert details for alert with id of "${id}".`;
this.logger.debug(errorMessage);
throw new Error(errorMessage);
}

return result.body._source;
} catch (error) {
const errorMessage = `[rac] - Unable to retrieve alert with id of "${id}".`;
this.logger.debug(errorMessage);
throw error;
}
}

public async get({ id, assetName }: GetAlertParams): Promise<ParsedTechnicalFields> {
try {
// first search for the alert by id, then use the alert info to check if user has access to it
const alert = await this.fetchAlert({
id,
assetName,
});

// this.authorization leverages the alerting plugin's authorization
// client exposed to us for reuse
await this.authorization.ensureAuthorized({
ruleTypeId: alert['rule.id'],
consumer: alert['kibana.rac.alert.owner'],
operation: ReadOperations.Get,
entity: AlertingAuthorizationEntity.Alert,
});

this.auditLogger?.log(
alertAuditEvent({
action: AlertAuditAction.GET,
id,
})
);

return alert;
} catch (error) {
this.logger.debug(`[rac] - Error fetching alert with id of "${id}"`);
this.auditLogger?.log(
alertAuditEvent({
action: AlertAuditAction.GET,
id,
error,
})
);
throw error;
}
}

public async update<Params extends AlertTypeParams = never>({
id,
data,
assetName,
}: UpdateOptions<Params>): Promise<ParsedTechnicalFields | null | undefined> {
try {
// TODO: use MGET
const alert = await this.fetchAlert({
id,
assetName,
});

await this.authorization.ensureAuthorized({
ruleTypeId: alert['rule.id'],
consumer: alert['kibana.rac.alert.owner'],
operation: WriteOperations.Update,
entity: AlertingAuthorizationEntity.Alert,
});

const index = this.getAlertsIndex(assetName);

const updateParameters = {
id,
index,
body: {
doc: {
'kibana.rac.alert.status': data.status,
},
},
};

const res = await this.esClient.update<ParsedTechnicalFields, unknown, unknown, unknown>(
updateParameters
);

this.auditLogger?.log(
alertAuditEvent({
action: AlertAuditAction.UPDATE,
id,
})
);

return res.body.get?._source;
} catch (error) {
this.auditLogger?.log(
alertAuditEvent({
action: AlertAuditAction.UPDATE,
id,
error,
})
);
throw error;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { Request } from '@hapi/hapi';

import { AlertsClientFactory, AlertsClientFactoryProps } from './alerts_client_factory';
import { ElasticsearchClient, KibanaRequest } from 'src/core/server';
import { loggingSystemMock } from 'src/core/server/mocks';
import { securityMock } from '../../../security/server/mocks';
import { AuditLogger } from '../../../security/server';
import { ruleDataPluginServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock';
import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock';
import { RuleDataPluginServiceConstructorOptions } from '../rule_data_plugin_service';

jest.mock('./alerts_client');

const securityPluginSetup = securityMock.createSetup();
const ruleDataServiceMock = ruleDataPluginServiceMock.create(
{} as RuleDataPluginServiceConstructorOptions
);
const alertingAuthMock = alertingAuthorizationMock.create();

const alertsClientFactoryParams: jest.Mocked<AlertsClientFactoryProps> = {
logger: loggingSystemMock.create().get(),
getAlertingAuthorization: (_: KibanaRequest) => alertingAuthMock,
securityPluginSetup,
esClient: {} as ElasticsearchClient,
ruleDataService: ruleDataServiceMock,
};

const fakeRequest = ({
app: {},
headers: {},
getBasePath: () => '',
path: '/',
route: { settings: {} },
url: {
href: '/',
},
raw: {
req: {
url: '/',
},
},
} as unknown) as Request;

const auditLogger = {
log: jest.fn(),
} as jest.Mocked<AuditLogger>;

beforeEach(() => {
jest.resetAllMocks();

securityPluginSetup.audit.asScoped.mockReturnValue(auditLogger);
});

test('creates an alerts client with proper constructor arguments', async () => {
const factory = new AlertsClientFactory();
factory.initialize({ ...alertsClientFactoryParams });
const request = KibanaRequest.from(fakeRequest);
await factory.create(request);

expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({
authorization: alertingAuthMock,
logger: alertsClientFactoryParams.logger,
auditLogger,
esClient: {},
ruleDataService: ruleDataServiceMock,
});
});

test('throws an error if already initialized', () => {
const factory = new AlertsClientFactory();
factory.initialize({ ...alertsClientFactoryParams });

expect(() =>
factory.initialize({ ...alertsClientFactoryParams })
).toThrowErrorMatchingInlineSnapshot(`"AlertsClientFactory (RAC) already initialized"`);
});

test('throws an error if ruleDataService not available', () => {
const factory = new AlertsClientFactory();

expect(() =>
factory.initialize({
...alertsClientFactoryParams,
ruleDataService: null,
})
).toThrowErrorMatchingInlineSnapshot(`"Rule registry data service required for alerts client"`);
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,57 +10,54 @@ import { PublicMethodsOf } from '@kbn/utility-types';
import { SecurityPluginSetup } from '../../../security/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertingAuthorization } from '../../../alerting/server/authorization';
import { AlertsClient } from './alert_client';
import { RacAuthorizationAuditLogger } from './audit_logger';
import { AlertsClient } from './alerts_client';
import { RuleDataPluginService } from '../rule_data_plugin_service';

export interface RacClientFactoryOpts {
export interface AlertsClientFactoryProps {
logger: Logger;
getSpaceId: (request: KibanaRequest) => string | undefined;
esClient: ElasticsearchClient;
getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf<AlertingAuthorization>;
securityPluginSetup: SecurityPluginSetup | undefined;
ruleDataService: RuleDataPluginService | undefined;
ruleDataService: RuleDataPluginService | null;
}

export class AlertsClientFactory {
private isInitialized = false;
private logger!: Logger;
private getSpaceId!: (request: KibanaRequest) => string | undefined;
private esClient!: ElasticsearchClient;
private getAlertingAuthorization!: (
request: KibanaRequest
) => PublicMethodsOf<AlertingAuthorization>;
private securityPluginSetup!: SecurityPluginSetup | undefined;
private ruleDataService!: RuleDataPluginService | undefined;
private ruleDataService!: RuleDataPluginService;

public initialize(options: RacClientFactoryOpts) {
public initialize(options: AlertsClientFactoryProps) {
/**
* This should be called by the plugin's start() method.
*/
if (this.isInitialized) {
throw new Error('AlertsClientFactory (RAC) already initialized');
}

if (options.ruleDataService == null) {
throw new Error('Rule registry data service required for alerts client');
}

this.getAlertingAuthorization = options.getAlertingAuthorization;
this.isInitialized = true;
this.logger = options.logger;
this.getSpaceId = options.getSpaceId;
this.esClient = options.esClient;
this.securityPluginSetup = options.securityPluginSetup;
this.ruleDataService = options.ruleDataService;
}

public async create(request: KibanaRequest, index: string): Promise<AlertsClient> {
public async create(request: KibanaRequest): Promise<AlertsClient> {
const { securityPluginSetup, getAlertingAuthorization, logger } = this;
const spaceId = this.getSpaceId(request);

return new AlertsClient({
spaceId,
logger,
index,
authorization: getAlertingAuthorization(request),
auditLogger: new RacAuthorizationAuditLogger(securityPluginSetup?.audit.asScoped(request)),
auditLogger: securityPluginSetup?.audit.asScoped(request),
esClient: this.esClient,
ruleDataService: this.ruleDataService!,
});
Expand Down
Loading