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

[Response Ops][RAM] Add RBAC Support for Multi-Consumer Rule Creation in O11Y and Stack Management #162605

Merged
merged 33 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
44bb149
Initial commit
JiaweiWu Jul 27, 2023
6823a67
Merge branch 'o11y-rbac-rule-feature-branch' into rbac-fixes
JiaweiWu Jul 27, 2023
7450abc
Consolidate alert authorization error message and fix some e2e tests
JiaweiWu Jul 27, 2023
ab9afa6
Fix tests
JiaweiWu Jul 31, 2023
aadad7c
Fix tests
JiaweiWu Jul 31, 2023
9e1887a
Fix unit tests
JiaweiWu Jul 31, 2023
679f1b5
Fix serverless tests
JiaweiWu Jul 31, 2023
40d28fe
More test fixes
JiaweiWu Jul 31, 2023
09ae685
Write unit tests and fix serverless tests
JiaweiWu Aug 1, 2023
a763eac
fix rule flyout logic
JiaweiWu Aug 2, 2023
71f6160
fix types
JiaweiWu Aug 2, 2023
935456e
Fix some bugs
JiaweiWu Aug 2, 2023
4ef3118
O11Y E2E tests
JiaweiWu Aug 3, 2023
4fc4040
Fix merge conflict
JiaweiWu Aug 3, 2023
527f980
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Aug 3, 2023
7a5883e
Fix tests and fix bug involving discover
JiaweiWu Aug 3, 2023
67bb1a9
Use better constants
JiaweiWu Aug 3, 2023
c1d7b25
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Aug 3, 2023
f07eb53
Design changes
JiaweiWu Aug 8, 2023
8c065ed
Fix telemetry test
JiaweiWu Aug 9, 2023
d78d108
Merge branch 'main' into rbac-fixes
JiaweiWu Aug 9, 2023
01ecdc8
Merge branch 'main' into rbac-fixes
JiaweiWu Aug 9, 2023
5cadacd
Fix e2e tests
JiaweiWu Aug 9, 2023
ed1deb0
Address comments
JiaweiWu Aug 9, 2023
a2f07fa
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Aug 9, 2023
28ab550
Fix types
JiaweiWu Aug 10, 2023
b3b7093
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Aug 10, 2023
e350f14
Address comments
JiaweiWu Aug 16, 2023
0a5db9f
Rename rule types variable
JiaweiWu Aug 16, 2023
0dc9c2a
Fix merge conflict
JiaweiWu Aug 16, 2023
0dc5599
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Aug 16, 2023
3bbeb44
Fix types
JiaweiWu Aug 16, 2023
7be9ddc
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Aug 16, 2023
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 @@ -249,7 +249,7 @@ describe('AlertingAuthorization', () => {
entity: AlertingAuthorizationEntity.Rule,
});

expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(0);
expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1);
});

test('is a no-op when the security license is disabled', async () => {
Expand All @@ -271,7 +271,7 @@ describe('AlertingAuthorization', () => {
entity: AlertingAuthorizationEntity.Rule,
});

expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(0);
expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1);
});

test('ensures the user has privileges to execute rules for the specified rule type and operation without consumer when producer and consumer are the same', async () => {
Expand Down Expand Up @@ -304,7 +304,7 @@ describe('AlertingAuthorization', () => {

expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType');

expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2);
expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1);
expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
'myType',
'myApp',
Expand Down Expand Up @@ -346,7 +346,7 @@ describe('AlertingAuthorization', () => {

expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType');

expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2);
expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1);
expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
'myType',
'myApp',
Expand Down Expand Up @@ -388,13 +388,7 @@ describe('AlertingAuthorization', () => {

expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType');

expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2);
expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
'myType',
'alerts',
'rule',
'create'
);
expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1);
expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
'myType',
'myApp',
Expand Down Expand Up @@ -436,13 +430,7 @@ describe('AlertingAuthorization', () => {

expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType');

expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2);
expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
'myType',
'alerts',
'alert',
'update'
);
expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1);
expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
'myType',
'myApp',
Expand Down Expand Up @@ -484,24 +472,15 @@ describe('AlertingAuthorization', () => {

expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType');

expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2);
expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
'myType',
'myApp',
'rule',
'create'
);
expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1);
expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
'myType',
'myOtherApp',
'rule',
'create'
);
expect(checkPrivileges).toHaveBeenCalledWith({
kibana: [
mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create'),
mockAuthorizationAction('myType', 'myApp', 'rule', 'create'),
],
kibana: [mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create')],
});
});

Expand Down Expand Up @@ -535,24 +514,57 @@ describe('AlertingAuthorization', () => {

expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType');

expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2);
expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1);
expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
'myType',
'myApp',
'myOtherApp',
'alert',
'update'
);
expect(checkPrivileges).toHaveBeenCalledWith({
kibana: [mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'update')],
});
});

test('ensures the producer is used for authorization if the consumer is `alerts`', async () => {
const { authorization } = mockSecurity();
const checkPrivileges: jest.MockedFunction<
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
> = jest.fn();
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
checkPrivileges.mockResolvedValueOnce({
username: 'some-user',
hasAllRequested: true,
privileges: { kibana: [] },
});

const alertAuthorization = new AlertingAuthorization({
request,
authorization,
ruleTypeRegistry,
features,
getSpace,
getSpaceId,
});

await alertAuthorization.ensureAuthorized({
ruleTypeId: 'myType',
consumer: 'alerts',
operation: WriteOperations.Create,
entity: AlertingAuthorizationEntity.Rule,
});

expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType');

expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1);
expect(authorization.actions.alerting.get).toHaveBeenCalledWith(
'myType',
'myOtherApp',
'alert',
'update'
'myApp',
'rule',
'create'
);
expect(checkPrivileges).toHaveBeenCalledWith({
kibana: [
mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'update'),
mockAuthorizationAction('myType', 'myApp', 'alert', 'update'),
],
kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create')],
});
});

Expand Down Expand Up @@ -596,7 +608,7 @@ describe('AlertingAuthorization', () => {
entity: AlertingAuthorizationEntity.Rule,
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unauthorized to create a \\"myType\\" rule for \\"myOtherApp\\""`
`"Unauthorized by \\"myOtherApp\\" to create \\"myType\\" rule"`
);
});

Expand Down Expand Up @@ -644,7 +656,7 @@ describe('AlertingAuthorization', () => {
entity: AlertingAuthorizationEntity.Alert,
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unauthorized to update a \\"myType\\" alert for \\"myAppRulesOnly\\""`
`"Unauthorized by \\"myAppRulesOnly\\" to update \\"myType\\" alert"`
);
});

Expand Down Expand Up @@ -688,7 +700,7 @@ describe('AlertingAuthorization', () => {
entity: AlertingAuthorizationEntity.Alert,
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unauthorized to update a \\"myType\\" alert by \\"myApp\\""`
`"Unauthorized by \\"myOtherApp\\" to update \\"myType\\" alert"`
);
});

Expand Down Expand Up @@ -732,7 +744,7 @@ describe('AlertingAuthorization', () => {
entity: AlertingAuthorizationEntity.Alert,
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""`
`"Unauthorized by \\"myOtherApp\\" to create \\"myType\\" alert"`
);
});
});
Expand Down Expand Up @@ -950,7 +962,7 @@ describe('AlertingAuthorization', () => {
expect(() => {
ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'alert');
}).toThrowErrorMatchingInlineSnapshot(
`"Unauthorized to find a \\"myAppAlertType\\" alert for \\"myOtherApp\\""`
`"Unauthorized by \\"myOtherApp\\" to find \\"myAppAlertType\\" alert"`
);
});
test('creates an `ensureRuleTypeIsAuthorized` function which is no-op if type is authorized', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import Boom from '@hapi/boom';
import { map, mapValues, fromPairs, has } from 'lodash';
import { fromPairs, has } from 'lodash';
import { KibanaRequest } from '@kbn/core/server';
import { JsonObject } from '@kbn/utility-types';
import { KueryNode } from '@kbn/es-query';
Expand Down Expand Up @@ -169,42 +169,24 @@ export class AlertingAuthorization {
);
}

public async ensureAuthorized({ ruleTypeId, consumer, operation, entity }: EnsureAuthorizedOpts) {
public async ensureAuthorized({
ruleTypeId,
consumer: legacyConsumer,
operation,
entity,
}: EnsureAuthorizedOpts) {
const { authorization } = this;
const ruleType = this.ruleTypeRegistry.get(ruleTypeId);
// We have some rules with consumer of "alerts" which indirectly means the same as
// a consumer of the rule type producer. Let's simplify the code and set it accordingly
const consumer = legacyConsumer === ALERTS_FEATURE_ID ? ruleType.producer : legacyConsumer;

const isAvailableConsumer = has(await this.allPossibleConsumers, consumer);
if (authorization && this.shouldCheckAuthorization()) {
const ruleType = this.ruleTypeRegistry.get(ruleTypeId);
const requiredPrivilegesByScope = {
consumer: authorization.actions.alerting.get(ruleTypeId, consumer, entity, operation),
producer: authorization.actions.alerting.get(
ruleTypeId,
ruleType.producer,
entity,
operation
),
};

// Skip authorizing consumer if consumer is the Rules Management consumer (`alerts`)
// This means that rules and their derivative alerts created in the Rules Management UI
// will only be subject to checking if user has access to the rule producer.
const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID;

const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request);
const { hasAllRequested, privileges } = await checkPrivileges({
kibana:
shouldAuthorizeConsumer && consumer !== ruleType.producer
? [
// check for access at consumer level
requiredPrivilegesByScope.consumer,
// check for access at producer level
requiredPrivilegesByScope.producer,
]
: [
// skip consumer privilege checks under `alerts` as all rule types can
// be created under `alerts` if you have producer level privileges
requiredPrivilegesByScope.producer,
],

const { hasAllRequested } = await checkPrivileges({
kibana: [authorization.actions.alerting.get(ruleTypeId, consumer, entity, operation)],
});

if (!isAvailableConsumer) {
Expand All @@ -215,40 +197,14 @@ export class AlertingAuthorization {
* as Privileged.
* This check will ensure we don't accidentally let these through
*/
throw Boom.forbidden(
getUnauthorizedMessage(ruleTypeId, ScopeType.Consumer, consumer, operation, entity)
);
throw Boom.forbidden(getUnauthorizedMessage(ruleTypeId, legacyConsumer, operation, entity));
}

if (!hasAllRequested) {
const authorizedPrivileges = map(
privileges.kibana.filter((privilege) => privilege.authorized),
'privilege'
);
const unauthorizedScopes = mapValues(
requiredPrivilegesByScope,
(privilege) => !authorizedPrivileges.includes(privilege)
);

const [unauthorizedScopeType, unauthorizedScope] =
shouldAuthorizeConsumer && unauthorizedScopes.consumer
? [ScopeType.Consumer, consumer]
: [ScopeType.Producer, ruleType.producer];

throw Boom.forbidden(
getUnauthorizedMessage(
ruleTypeId,
unauthorizedScopeType,
unauthorizedScope,
operation,
entity
)
);
throw Boom.forbidden(getUnauthorizedMessage(ruleTypeId, consumer, operation, entity));
}
} else if (!isAvailableConsumer) {
throw Boom.forbidden(
getUnauthorizedMessage(ruleTypeId, ScopeType.Consumer, consumer, operation, entity)
);
throw Boom.forbidden(getUnauthorizedMessage(ruleTypeId, consumer, operation, entity));
}
}

Expand Down Expand Up @@ -300,13 +256,7 @@ export class AlertingAuthorization {
ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => {
if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}/${authType}`)) {
throw Boom.forbidden(
getUnauthorizedMessage(
ruleTypeId,
ScopeType.Consumer,
consumer,
'find',
authorizationEntity
)
getUnauthorizedMessage(ruleTypeId, consumer, 'find', authorizationEntity)
);
} else {
if (authorizedEntries.has(ruleTypeId)) {
Expand Down Expand Up @@ -460,19 +410,11 @@ function asAuthorizedConsumers(
return fromPairs(consumers.map((feature) => [feature, hasPrivileges]));
}

enum ScopeType {
Consumer,
Producer,
}

function getUnauthorizedMessage(
alertTypeId: string,
scopeType: ScopeType,
scope: string,
operation: string,
entity: string
): string {
return `Unauthorized to ${operation} a "${alertTypeId}" ${entity} ${
scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"`
}`;
return `Unauthorized by "${scope}" to ${operation} "${alertTypeId}" ${entity}`;
}
Loading