Skip to content

Commit

Permalink
[RAC] [RBAC] adds function to get alerts-as-data index name (#6)
Browse files Browse the repository at this point in the history
* WIP - test script and route in rule registry to pull index name. I need to test out adding this route within the APM and sec sol plugins specifically and see if they spit back the same .alerts index but with the appropriate asset name despite not providing one.

WIP - DO NOT DELETE THIS CODE

minor cleanup

updates client to require passing in index name, which is now available through the alerts as data client function getAlertsIndex

fix types

* remove outdated comment
  • Loading branch information
dhurley14 committed Jun 8, 2021
1 parent 4d4920e commit c3008a8
Show file tree
Hide file tree
Showing 19 changed files with 308 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const createAlertingAuthorizationMock = () => {
ensureAuthorized: jest.fn(),
filterByRuleTypeAuthorization: jest.fn(),
getFindAuthorizationFilter: jest.fn(),
getAuthorizedAlertsIndices: jest.fn(),
};
return mocked;
};
Expand Down
124 changes: 90 additions & 34 deletions x-pack/plugins/alerting/server/authorization/alerting_authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export class AlertingAuthorization {
private readonly featuresIds: Promise<Set<string>>;
private readonly allPossibleConsumers: Promise<AuthorizedConsumers>;
private readonly exemptConsumerIds: string[];
// to be used when building the alerts as data index name
// private readonly spaceId: Promise<string | undefined>;

constructor({
alertTypeRegistry,
Expand Down Expand Up @@ -124,6 +126,8 @@ export class AlertingAuthorization {
return new Set();
});

// this.spaceId = getSpace(request).then((maybeSpace) => maybeSpace?.id ?? undefined);

this.allPossibleConsumers = this.featuresIds.then((featuresIds) =>
featuresIds.size
? asAuthorizedConsumers([...this.exemptConsumerIds, ...featuresIds], {
Expand All @@ -138,6 +142,41 @@ export class AlertingAuthorization {
return this.authorization?.mode?.useRbacForRequest(this.request) ?? false;
}

public async getAuthorizedAlertsIndices(featureIds: string[]): Promise<string[] | undefined> {
const augmentedRuleTypes = await this.augmentRuleTypesWithAuthorization(
this.alertTypeRegistry.list(),
[ReadOperations.Find, ReadOperations.Get, WriteOperations.Update],
AlertingAuthorizationEntity.Alert,
new Set(featureIds)
);

const arrayOfAuthorizedRuleTypes = Array.from(augmentedRuleTypes.authorizedRuleTypes);

// As long as the user can read a minimum of one type of rule type produced by the provided feature,
// the user should be provided that features' alerts index.
// Limiting which alerts that user can read on that index will be done via the findAuthorizationFilter
const authorizedFeatures = arrayOfAuthorizedRuleTypes.reduce(
(acc, ruleType) => acc.add(ruleType.producer),
new Set<string>()
);

// when we add the spaceId to the index name, uncomment this line
// const spaceName = await this.spaceName;

const toReturn = Array.from(authorizedFeatures).flatMap((feature) => {
switch (feature) {
case 'apm':
return '.alerts-observability-apm';
case 'siem':
return ['.alerts-security-solution', '.siem-signals'];
default:
return [];
}
});

return toReturn;
}

public async ensureAuthorized({ ruleTypeId, consumer, operation, entity }: EnsureAuthorizedOpts) {
const { authorization } = this;

Expand Down Expand Up @@ -339,13 +378,15 @@ export class AlertingAuthorization {
private async augmentRuleTypesWithAuthorization(
ruleTypes: Set<RegistryAlertType>,
operations: Array<ReadOperations | WriteOperations>,
authorizationEntity: AlertingAuthorizationEntity
authorizationEntity: AlertingAuthorizationEntity,
featuresIds?: Set<string>
): Promise<{
username?: string;
hasAllRequested: boolean;
authorizedRuleTypes: Set<RegistryAlertTypeWithAuth>;
unauthorizedRuleTypes: Set<RegistryAlertTypeWithAuth> | undefined;
}> {
const featuresIds = await this.featuresIds;
const fIds = featuresIds ?? (await this.featuresIds);
if (this.authorization && this.shouldCheckAuthorization()) {
const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(
this.request
Expand All @@ -363,7 +404,7 @@ export class AlertingAuthorization {
// as we can't ask ES for the user's individual privileges we need to ask for each feature
// and ruleType in the system whether this user has this privilege
for (const ruleType of ruleTypesWithAuthorization) {
for (const feature of featuresIds) {
for (const feature of fIds) {
for (const operation of operations) {
privilegeToRuleType.set(
this.authorization!.actions.alerting.get(
Expand All @@ -382,47 +423,62 @@ export class AlertingAuthorization {
kibana: [...privilegeToRuleType.keys()],
});

let authorizedRuleTypes;
let unauthorizedRuleTypes;
if (hasAllRequested) {
authorizedRuleTypes = this.augmentWithAuthorizedConsumers(
ruleTypes,
await this.allPossibleConsumers
);
} else {
[authorizedRuleTypes, unauthorizedRuleTypes] = privileges.kibana.reduce(
([authzRuleTypes, unauthzRuleTypes], { authorized, privilege }) => {
if (authorized && privilegeToRuleType.has(privilege)) {
const [
ruleType,
feature,
hasPrivileges,
isAuthorizedAtProducerLevel,
] = privilegeToRuleType.get(privilege)!;
ruleType.authorizedConsumers[feature] = mergeHasPrivileges(
hasPrivileges,
ruleType.authorizedConsumers[feature]
);

if (isAuthorizedAtProducerLevel && this.exemptConsumerIds.length > 0) {
// granting privileges under the producer automatically authorized exempt consumer IDs as well
this.exemptConsumerIds.forEach((exemptId: string) => {
ruleType.authorizedConsumers[exemptId] = mergeHasPrivileges(
hasPrivileges,
ruleType.authorizedConsumers[exemptId]
);
});
}
authzRuleTypes.add(ruleType);
} else if (!authorized) {
const [ruleType, , , ,] = privilegeToRuleType.get(privilege)!;
unauthzRuleTypes.add(ruleType);
}
return [authzRuleTypes, unauthzRuleTypes];
},
[new Set<RegistryAlertTypeWithAuth>(), new Set<RegistryAlertTypeWithAuth>()]
);
}

return {
username,
hasAllRequested,
authorizedRuleTypes: hasAllRequested
? // has access to all features
this.augmentWithAuthorizedConsumers(ruleTypes, await this.allPossibleConsumers)
: // only has some of the required privileges
privileges.kibana.reduce((authorizedRuleTypes, { authorized, privilege }) => {
if (authorized && privilegeToRuleType.has(privilege)) {
const [
ruleType,
feature,
hasPrivileges,
isAuthorizedAtProducerLevel,
] = privilegeToRuleType.get(privilege)!;
ruleType.authorizedConsumers[feature] = mergeHasPrivileges(
hasPrivileges,
ruleType.authorizedConsumers[feature]
);

if (isAuthorizedAtProducerLevel && this.exemptConsumerIds.length > 0) {
// granting privileges under the producer automatically authorized exempt consumer IDs as well
this.exemptConsumerIds.forEach((exemptId: string) => {
ruleType.authorizedConsumers[exemptId] = mergeHasPrivileges(
hasPrivileges,
ruleType.authorizedConsumers[exemptId]
);
});
}
authorizedRuleTypes.add(ruleType);
}
return authorizedRuleTypes;
}, new Set<RegistryAlertTypeWithAuth>()),
authorizedRuleTypes,
unauthorizedRuleTypes,
};
} else {
return {
hasAllRequested: true,
authorizedRuleTypes: this.augmentWithAuthorizedConsumers(
new Set([...ruleTypes].filter((ruleType) => featuresIds.has(ruleType.producer))),
new Set([...ruleTypes].filter((ruleType) => fIds.has(ruleType.producer))),
await this.allPossibleConsumers
),
unauthorizedRuleTypes: undefined,
};
}
}
Expand Down
23 changes: 23 additions & 0 deletions x-pack/plugins/apm/server/routes/settings/apm_indices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getApmIndexSettings,
} from '../../lib/settings/apm_indices/get_apm_indices';
import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices';
// import { APM_SERVER_FEATURE_ID } from '../../../common/alert_types';

// get list of apm indices and values
const apmIndexSettingsRoute = createApmServerRoute({
Expand Down Expand Up @@ -63,7 +64,29 @@ const saveApmIndicesRoute = createApmServerRoute({
},
});

// const getApmAlertsAsDataIndexRoute = createApmServerRoute({
// endpoint: 'GET /api/apm/settings/apm-alerts-as-data-indices',
// options: { tags: ['access:apm'] },
// handler: async (resources) => {
// const { context } = resources;
// console.error(context);
// const alertsAsDataClient = await context.rac?.getAlertsClient();
// if (alertsAsDataClient == null) {
// throw new Error('Missing alerts as data client');
// }
// const res = await alertsAsDataClient.getAlertsIndex([
// APM_SERVER_FEATURE_ID,
// 'siem',
// ]);
// console.error('RESPONSE', JSON.stringify(res, null, 2));
// return res[0];
// // return alertsAsDataClient.getFullAssetName();
// // return ruleDataClient.getIndexName();
// },
// });

export const apmIndicesRouteRepository = createApmServerRouteRepository()
.add(apmIndexSettingsRoute)
.add(apmIndicesRoute)
.add(saveApmIndicesRoute);
// .add(getApmAlertsAsDataIndexRoute);
2 changes: 2 additions & 0 deletions x-pack/plugins/apm/server/routes/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import {
} from 'src/core/server';
import { RuleDataClient } from '../../../rule_registry/server';
import { AlertingApiRequestHandlerContext } from '../../../alerting/server';
import type { RacApiRequestHandlerContext } from '../../../rule_registry/server';
import { LicensingApiRequestHandlerContext } from '../../../licensing/server';
import { APMConfig } from '..';
import { APMPluginDependencies } from '../types';

export interface ApmPluginRequestHandlerContext extends RequestHandlerContext {
licensing: LicensingApiRequestHandlerContext;
alerting: AlertingApiRequestHandlerContext;
rac: RacApiRequestHandlerContext;
}

export type InspectResponse = Array<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const createAlertsClientMock = () => {
get: jest.fn(),
getAlertsIndex: jest.fn(),
update: jest.fn(),
getFullAssetName: jest.fn(),
};
return mocked;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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 { OWNER, RULE_ID } from '../../common/technical_rule_data_field_names';
import { ParsedTechnicalFields } from '../../common/parse_technical_fields';

export interface ConstructorOptions {
Expand All @@ -33,13 +34,13 @@ export interface UpdateOptions<Params extends AlertTypeParams> {
status: string;
};
// observability-apm see here: x-pack/plugins/apm/server/plugin.ts:191
assetName: string;
indexName: string;
}

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

export class AlertsClient {
Expand All @@ -63,27 +64,27 @@ export class AlertsClient {
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);
public getFullAssetName() {
return this.ruleDataService?.getFullAssetName();
}

private async fetchAlert({ id, assetName }: GetAlertParams): Promise<ParsedTechnicalFields> {
public async getAlertsIndex(featureIds: string[]) {
return this.authorization.getAuthorizedAlertsIndices(
featureIds.length !== 0 ? featureIds : ['apm', 'siem']
);
}

private async fetchAlert({ id, indexName }: GetAlertParams): Promise<ParsedTechnicalFields> {
try {
const result = await this.esClient.get<ParsedTechnicalFields>({
index: this.getAlertsIndex(assetName),
index: indexName,
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
result.body._source[RULE_ID] == null ||
result.body._source[OWNER] == null
) {
const errorMessage = `[rac] - Unable to retrieve alert details for alert with id of "${id}".`;
this.logger.debug(errorMessage);
Expand All @@ -98,12 +99,12 @@ export class AlertsClient {
}
}

public async get({ id, assetName }: GetAlertParams): Promise<ParsedTechnicalFields> {
public async get({ id, indexName }: 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,
indexName,
});

// this.authorization leverages the alerting plugin's authorization
Expand Down Expand Up @@ -139,13 +140,13 @@ export class AlertsClient {
public async update<Params extends AlertTypeParams = never>({
id,
data,
assetName,
indexName,
}: UpdateOptions<Params>): Promise<ParsedTechnicalFields | null | undefined> {
try {
// TODO: use MGET
const alert = await this.fetchAlert({
id,
assetName,
indexName,
});

await this.authorization.ensureAuthorized({
Expand All @@ -155,11 +156,9 @@ export class AlertsClient {
entity: AlertingAuthorizationEntity.Alert,
});

const index = this.getAlertsIndex(assetName);

const updateParameters = {
id,
index,
index: indexName,
body: {
doc: {
'kibana.rac.alert.status': data.status,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('get()', () => {
},
})
);
const result = await alertsClient.get({ id: '1', assetName: 'observability-apm' });
const result = await alertsClient.get({ id: '1', indexName: '.alerts-observability-apm' });
expect(result).toMatchInlineSnapshot(`
Object {
"kibana.rac.alert.owner": "apm",
Expand Down Expand Up @@ -88,7 +88,7 @@ describe('get()', () => {
esClientMock.get.mockRejectedValue(error);

await expect(
alertsClient.get({ id: '1', assetName: 'observability-apm' })
alertsClient.get({ id: '1', indexName: '.alerts-observability-apm' })
).rejects.toThrowErrorMatchingInlineSnapshot(`"something when wrong"`);
expect(auditLogger.log).toHaveBeenCalledWith({
error: { code: 'Error', message: 'something when wrong' },
Expand Down Expand Up @@ -119,7 +119,7 @@ describe('get()', () => {

test('returns alert if user is authorized to read alert under the consumer', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
const result = await alertsClient.get({ id: '1', assetName: 'observability-apm' });
const result = await alertsClient.get({ id: '1', indexName: '.alerts-observability-apm' });

expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({
entity: 'alert',
Expand All @@ -144,7 +144,7 @@ describe('get()', () => {
);

await expect(
alertsClient.get({ id: '1', assetName: 'observability-apm' })
alertsClient.get({ id: '1', indexName: '.alerts-observability-apm' })
).rejects.toMatchInlineSnapshot(
`[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]`
);
Expand Down
Loading

0 comments on commit c3008a8

Please sign in to comment.