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

[RAC] [RBAC] adds function to get alerts-as-data index name #6

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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,40 @@ export class AlertsClient {
this.ruleDataService = ruleDataService;
}

public getFullAssetName() {
return this.ruleDataService?.getFullAssetName();
}

/**
* we are "hard coding" this string similar to how rule registry is doing it
* OUTDATED: 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 async getAlertsIndex(featureIds: string[]) {
dhurley14 marked this conversation as resolved.
Show resolved Hide resolved
return this.authorization.getAuthorizedAlertsIndices(
featureIds.length !== 0 ? featureIds : ['apm', 'siem']
);
// const indexExists = await this.ruleDataService?.assertFullAssetNameExists(assetName);
// this.logger.debug(`DOES THE INDEX EXIST? ${JSON.stringify(indexExists)}`);
// const fullAssetName = this.ruleDataService?.getFullAssetName(assetName);
// if (indexExists) {
// return fullAssetName;
// }
// const errorMessage = `Missing index alias ${fullAssetName} not found for given asset name ${assetName}`;
// this.logger.error(errorMessage);
// throw new Error(errorMessage);
}

private async fetchAlert({ id, assetName }: GetAlertParams): Promise<ParsedTechnicalFields> {
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 +112,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 +153,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 +169,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
Loading