Skip to content

Commit

Permalink
[RAC] [RBAC] MVP RBAC for alerts as data (#100705)
Browse files Browse the repository at this point in the history
An MVP of the RBAC work required for the "alerts as data" effort. An example of the existing implementation for alerts would be that of the security solution. The security solution stores its alerts generated from rules in a single data index - .siem-signals. In order to gain or restrict access to alerts, users do so by following the Elasticsearch privilege architecture. A user would need to go into the Kibana role access UI and give explicit read/write/manage permissions for the index itself.

Kibana as a whole is moving away from this model and instead having all user interactions run through the Kibana privilege model. When solutions use saved objects, this authentication layer is abstracted away for them. Because we have chosen to use data indices for alerts, we cannot rely on this abstracted out layer that saved objects provide - we need to provide our own RBAC! Instead of giving users explicit permission to an alerts index, users are instead given access to features. They don't need to know anything about indices, that work we do under the covers now.

Co-authored-by: Yara Tercero <[email protected]>
Co-authored-by: Yara Tercero <[email protected]>
  • Loading branch information
3 people authored Jul 8, 2021
1 parent e9ec16e commit c77c7fb
Show file tree
Hide file tree
Showing 118 changed files with 5,700 additions and 106 deletions.
3 changes: 3 additions & 0 deletions packages/kbn-rule-data-utils/src/technical_field_names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const RULE_NAME = 'rule.name' as const;
const RULE_CATEGORY = 'rule.category' as const;
const TAGS = 'tags' as const;
const PRODUCER = `${ALERT_NAMESPACE}.producer` as const;
const OWNER = `${ALERT_NAMESPACE}.owner` as const;
const ALERT_ID = `${ALERT_NAMESPACE}.id` as const;
const ALERT_UUID = `${ALERT_NAMESPACE}.uuid` as const;
const ALERT_START = `${ALERT_NAMESPACE}.start` as const;
Expand All @@ -40,6 +41,7 @@ const fields = {
RULE_CATEGORY,
TAGS,
PRODUCER,
OWNER,
ALERT_ID,
ALERT_UUID,
ALERT_START,
Expand All @@ -62,6 +64,7 @@ export {
RULE_CATEGORY,
TAGS,
PRODUCER,
OWNER,
ALERT_ID,
ALERT_UUID,
ALERT_START,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

// Similar to the src/core/server/saved_objects/version/decode_version.ts
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/kbn-securitysolution-es-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@

export * from './bad_request_error';
export * from './create_boostrap_index';
export * from './decode_version';
export * from './delete_all_index';
export * from './delete_policy';
export * from './delete_template';
export * from './elasticsearch_client';
export * from './encode_hit_version';
export * from './get_index_aliases';
export * from './get_index_count';
export * from './get_index_exists';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ const createAlertingAuthorizationMock = () => {
ensureAuthorized: jest.fn(),
filterByRuleTypeAuthorization: jest.fn(),
getFindAuthorizationFilter: jest.fn(),
getAugmentedRuleTypesWithAuthorization: jest.fn(),
};
return mocked;
};

export const alertingAuthorizationMock: {
create: () => AlertingAuthorizationMock;
create: () => jest.Mocked<PublicMethodsOf<AlertingAuthorization>>;
} = {
create: createAlertingAuthorizationMock,
};
Original file line number Diff line number Diff line change
Expand Up @@ -1944,4 +1944,184 @@ describe('AlertingAuthorization', () => {
`);
});
});

describe('getAugmentedRuleTypesWithAuthorization', () => {
const myOtherAppAlertType: RegistryAlertType = {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
recoveryActionGroup: RecoveredActionGroup,
id: 'myOtherAppAlertType',
name: 'myOtherAppAlertType',
producer: 'alerts',
enabledInLicense: true,
isExportable: true,
};
const myAppAlertType: RegistryAlertType = {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
recoveryActionGroup: RecoveredActionGroup,
id: 'myAppAlertType',
name: 'myAppAlertType',
producer: 'myApp',
enabledInLicense: true,
isExportable: true,
};
const mySecondAppAlertType: RegistryAlertType = {
actionGroups: [],
actionVariables: undefined,
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
recoveryActionGroup: RecoveredActionGroup,
id: 'mySecondAppAlertType',
name: 'mySecondAppAlertType',
producer: 'myApp',
enabledInLicense: true,
isExportable: true,
};
const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]);

test('it returns authorized rule types given a set of feature ids', async () => {
const { authorization } = mockSecurity();
const checkPrivileges: jest.MockedFunction<
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
> = jest.fn();
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
checkPrivileges.mockResolvedValueOnce({
username: 'some-user',
hasAllRequested: false,
privileges: {
kibana: [
{
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'),
authorized: true,
},
],
},
});
const alertAuthorization = new AlertingAuthorization({
request,
authorization,
alertTypeRegistry,
features,
auditLogger,
getSpace,
exemptConsumerIds,
});
alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);

await expect(
alertAuthorization.getAugmentedRuleTypesWithAuthorization(
['myApp'],
[ReadOperations.Find, ReadOperations.Get, WriteOperations.Update],
AlertingAuthorizationEntity.Alert
)
).resolves.toMatchInlineSnapshot(`
Object {
"authorizedRuleTypes": Set {
Object {
"actionGroups": Array [],
"actionVariables": undefined,
"authorizedConsumers": Object {
"myApp": Object {
"all": false,
"read": true,
},
},
"defaultActionGroupId": "default",
"enabledInLicense": true,
"id": "myOtherAppAlertType",
"isExportable": true,
"minimumLicenseRequired": "basic",
"name": "myOtherAppAlertType",
"producer": "alerts",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
},
"hasAllRequested": false,
"username": "some-user",
}
`);
});

test('it returns all authorized if user has read, get and update alert privileges', async () => {
const { authorization } = mockSecurity();
const checkPrivileges: jest.MockedFunction<
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
> = jest.fn();
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
checkPrivileges.mockResolvedValueOnce({
username: 'some-user',
hasAllRequested: false,
privileges: {
kibana: [
{
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'),
authorized: true,
},
{
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'get'),
authorized: true,
},
{
privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'update'),
authorized: true,
},
],
},
});
const alertAuthorization = new AlertingAuthorization({
request,
authorization,
alertTypeRegistry,
features,
auditLogger,
getSpace,
exemptConsumerIds,
});
alertTypeRegistry.list.mockReturnValue(setOfAlertTypes);

await expect(
alertAuthorization.getAugmentedRuleTypesWithAuthorization(
['myApp'],
[ReadOperations.Find, ReadOperations.Get, WriteOperations.Update],
AlertingAuthorizationEntity.Alert
)
).resolves.toMatchInlineSnapshot(`
Object {
"authorizedRuleTypes": Set {
Object {
"actionGroups": Array [],
"actionVariables": undefined,
"authorizedConsumers": Object {
"myApp": Object {
"all": true,
"read": true,
},
},
"defaultActionGroupId": "default",
"enabledInLicense": true,
"id": "myOtherAppAlertType",
"isExportable": true,
"minimumLicenseRequired": "basic",
"name": "myOtherAppAlertType",
"producer": "alerts",
"recoveryActionGroup": Object {
"id": "recovered",
"name": "Recovered",
},
},
},
"hasAllRequested": false,
"username": "some-user",
}
`);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -124,20 +124,41 @@ export class AlertingAuthorization {
return new Set();
});

this.allPossibleConsumers = this.featuresIds.then((featuresIds) =>
featuresIds.size
this.allPossibleConsumers = this.featuresIds.then((featuresIds) => {
return featuresIds.size
? asAuthorizedConsumers([...this.exemptConsumerIds, ...featuresIds], {
read: true,
all: true,
})
: {}
);
: {};
});
}

private shouldCheckAuthorization(): boolean {
return this.authorization?.mode?.useRbacForRequest(this.request) ?? false;
}

/*
* This method exposes the private 'augmentRuleTypesWithAuthorization' to be
* used by the RAC/Alerts client
*/
public async getAugmentedRuleTypesWithAuthorization(
featureIds: readonly string[],
operations: Array<ReadOperations | WriteOperations>,
authorizationEntity: AlertingAuthorizationEntity
): Promise<{
username?: string;
hasAllRequested: boolean;
authorizedRuleTypes: Set<RegistryAlertTypeWithAuth>;
}> {
return this.augmentRuleTypesWithAuthorization(
this.alertTypeRegistry.list(),
operations,
authorizationEntity,
new Set(featureIds)
);
}

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

Expand Down Expand Up @@ -339,13 +360,14 @@ 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>;
}> {
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 +385,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 Down Expand Up @@ -420,7 +442,7 @@ export class AlertingAuthorization {
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
),
};
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/alerting/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export { FindResult } from './alerts_client';
export { PublicAlertInstance as AlertInstance } from './alert_instance';
export { parseDuration } from './lib';
export { getEsErrorMessage } from './lib/errors';
export {
ReadOperations,
AlertingAuthorizationFilterType,
AlertingAuthorization,
WriteOperations,
AlertingAuthorizationEntity,
} from './authorization';

export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext);

Expand Down
Loading

0 comments on commit c77c7fb

Please sign in to comment.