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

adds an 'owner' field to the siem-signals mapping, working authz get for security solution, need to work through rule registry changes #98746

Closed
wants to merge 16 commits into from
Closed
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
28 changes: 28 additions & 0 deletions x-pack/plugins/features/common/feature_kibana_privileges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,34 @@ export interface FeatureKibanaPrivileges {
*/
read?: readonly string[];
};

/**
* Solutions should specify owners of alerts here which will provide the solution read / write access to those alerts.
*/
rac?: {
/**
* List of owners of alerts which users should have full read/write access to when granted this privilege.
* @example
* ```ts
* {
* all: ['securitySolution']
* }
* ```
*/
all?: readonly string[];

/**
* List of owners of alerts which users should have read-only access to when granted this privilege.
* @example
* ```ts
* {
* read: ['securitySolution', 'observability']
* }
* ```
*/
read?: readonly string[];
};

/**
* If your feature requires access to specific saved objects, then specify your access needs here.
*/
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/features/common/kibana_feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export interface KibanaFeatureConfig {
*/
alerting?: readonly string[];

/**
* If your feature grants access to specific alerts, you can specify them here to control visibility based on the current space.
*/
rac?: readonly string[];

/**
* Feature privilege definition.
*
Expand Down Expand Up @@ -191,6 +196,10 @@ export class KibanaFeature {
return this.config.reserved;
}

public get rac() {
return this.config.rac;
}

public toRaw() {
return { ...this.config } as KibanaFeatureConfig;
}
Expand Down
30 changes: 29 additions & 1 deletion x-pack/plugins/features/server/feature_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const managementSchema = Joi.object().pattern(
);
const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex));
const alertingSchema = Joi.array().items(Joi.string());
const racSchema = Joi.array().items(Joi.string());

const appCategorySchema = Joi.object({
id: Joi.string().required(),
Expand All @@ -56,6 +57,10 @@ const kibanaPrivilegeSchema = Joi.object({
all: Joi.array().items(Joi.string()).required(),
read: Joi.array().items(Joi.string()).required(),
}).required(),
rac: Joi.object({
all: racSchema,
read: racSchema,
}),
ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(),
});

Expand Down Expand Up @@ -113,6 +118,7 @@ const kibanaFeatureSchema = Joi.object({
management: managementSchema,
catalogue: catalogueSchema,
alerting: alertingSchema,
rac: racSchema,
privileges: Joi.object({
all: kibanaPrivilegeSchema,
read: kibanaPrivilegeSchema,
Expand Down Expand Up @@ -161,7 +167,7 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) {
throw validateResult.error;
}
// the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid.
const { app = [], management = {}, catalogue = [], alerting = [] } = feature;
const { app = [], management = {}, catalogue = [], alerting = [], rac = [] } = feature;

const unseenApps = new Set(app);

Expand All @@ -176,6 +182,8 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) {

const unseenAlertTypes = new Set(alerting);

const unseenRacTypes = new Set(rac);

function validateAppEntry(privilegeId: string, entry: readonly string[] = []) {
entry.forEach((privilegeApp) => unseenApps.delete(privilegeApp));

Expand Down Expand Up @@ -219,6 +227,23 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) {
}
}

function validateRacEntry(privilegeId: string, entry: FeatureKibanaPrivileges['rac']) {
const all = entry?.all ?? [];
const read = entry?.read ?? [];

all.forEach((privilegeOwner) => unseenRacTypes.delete(privilegeOwner));
read.forEach((privilegeOwner) => unseenRacTypes.delete(privilegeOwner));

const unknownRacEntries = difference([...all, ...read], rac);
if (unknownRacEntries.length > 0) {
throw new Error(
`Feature privilege ${
feature.id
}.${privilegeId} has unknown rac entries: ${unknownRacEntries.join(', ')}`
);
}
}

function validateManagementEntry(
privilegeId: string,
managementEntry: Record<string, readonly string[]> = {}
Expand Down Expand Up @@ -280,6 +305,8 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) {

validateManagementEntry(privilegeId, privilegeDefinition.management);
validateAlertingEntry(privilegeId, privilegeDefinition.alerting);

// validateRacEntry(privilegeId, privilegeDefinition.rac);
});

const subFeatureEntries = feature.subFeatures ?? [];
Expand All @@ -290,6 +317,7 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) {
validateCatalogueEntry(subFeaturePrivilege.id, subFeaturePrivilege.catalogue);
validateManagementEntry(subFeaturePrivilege.id, subFeaturePrivilege.management);
validateAlertingEntry(subFeaturePrivilege.id, subFeaturePrivilege.alerting);
// validateRacEntry(subFeaturePrivilege.id, subFeaturePrivilege.rac);
});
});
});
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/monitoring/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"requiredPlugins": [
"licensing",
"features",
"ruleRegistry",
"data",
"navigation",
"kibanaLegacy",
Expand Down
36 changes: 35 additions & 1 deletion x-pack/plugins/monitoring/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,18 @@ export class MonitoringPlugin
logger: this.log,
});
initInfraSource(config, plugins.infra);
router.get({ path: '/monitoring-myfakepath', validate: false }, async (context, req, res) => {
try {
const racClient = await context.ruleRegistry?.getRacClient();
const thing = await racClient?.get({ id: 'hello world', owner: 'observability' });
console.error('THE THING!!!', JSON.stringify(thing.body, null, 2));
return res.ok({ body: { success: true } });
} catch (err) {
console.error('monitoring route threw an error');
console.error(err);
return res.notFound({ body: { message: err.message } });
}
});
}

return {
Expand Down Expand Up @@ -244,8 +256,30 @@ export class MonitoringPlugin
}),
category: DEFAULT_APP_CATEGORIES.management,
app: ['monitoring', 'kibana'],
rac: ['observability'],
catalogue: ['monitoring'],
privileges: null,
privileges: {
all: {
rac: {
all: ['observability'],
},
savedObject: {
all: [],
read: [],
},
ui: ['show', 'save', 'alerting:show', 'alerting:save'],
},
read: {
rac: {
all: ['observability'],
},
savedObject: {
all: [],
read: [],
},
ui: ['show', 'save', 'alerting:show', 'alerting:save'],
},
},
alerting: ALERTS,
reserved: {
description: i18n.translate('xpack.monitoring.feature.reserved.description', {
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/monitoring/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
ActionsApiRequestHandlerContext,
} from '../../actions/server';
import type { AlertingApiRequestHandlerContext } from '../../alerting/server';
import { RacApiRequestHandlerContext } from '../../rule_registry/server';
import {
PluginStartContract as AlertingPluginStartContract,
PluginSetupContract as AlertingPluginSetupContract,
Expand Down Expand Up @@ -57,6 +58,7 @@ export interface PluginsSetup {
export interface RequestHandlerContextMonitoringPlugin extends RequestHandlerContext {
actions?: ActionsApiRequestHandlerContext;
alerting?: AlertingApiRequestHandlerContext;
ruleRegistry?: RacApiRequestHandlerContext;
}

export interface PluginsStart {
Expand Down
115 changes: 115 additions & 0 deletions x-pack/plugins/rule_registry/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# RAC

The RAC plugin provides a common place to register rules with alerting. You can:

- Register types of rules
- Perform CRUD actions on rules
- Perform CRUD actions on alerts produced by rules

----

Table of Contents

- [Rule Registry](#rule-registry)
- [Role Based Access-Control](#rbac)

## Rule Registry
The rule registry plugin aims to make it easy for rule type producers to have their rules produce the data that they need to build rich experiences on top of a unified experience, without the risk of mapping conflicts.

A rule registry creates a template, an ILM policy, and an alias. The template mappings can be configured. It also injects a client scoped to these indices.
Expand Down Expand Up @@ -66,3 +82,102 @@ The following fields are available in the root rule registry:
- `kibana.rac.alert.severity.value`: the severity of the alert, as a numerical value, which allows sorting.

This list is not final - just a start. Field names might change or moved to a scoped registry. If we implement log and sequence based rule types the list of fields will grow. If a rule type needs additional fields, the recommendation would be to have the field in its own registry first (or in its producer’s registry), and if usage is more broadly adopted, it can be moved to the root registry.

## Role Based Access-Control

Rules registered through the rule registry produce `alerts` that are indexed into the `.alerts` index. Using the `producer` defined in the rule registry, these alerts inheret the `producer` property which is used in the auth to determine whether a user has access to these alerts and what operations they can perform on them.

Users will need to be granted access to these `alerts`. When registering a feature in Kibana you can specify multiple types of privileges which are granted to users when they're assigned certain roles. Assuming your feature generates `alerts`, you'll want to control which roles have all/read privileges for these alerts that are scoped to your feature. For example, the `security_solution` plugin allows users to create rules that generate `alerts`, so does `observability`. The `security_solution` plugin only wants to grant it's users access to `alerts` belonging to `security_solution`. However, a user may have access to numerous `alerts` like `['security_solution', 'observability']`.

You can control all of these abilities by assigning privileges to Alerts from within your own feature, for example:

```typescript
features.registerKibanaFeature({
id: 'my-application-id',
name: 'My Application',
app: [],
privileges: {
all: {
alerts: {
all: [
// grant `all` over our own types
'my-application-id.my-feature',
'my-application-id.my-restricted-alert-type',
// grant `all` over the built-in IndexThreshold
'.index-threshold',
// grant `all` over Uptime's TLS AlertType
'xpack.uptime.alerts.actionGroups.tls'
],
},
},
read: {
alerts: {
read: [
// grant `read` over our own type
'my-application-id.my-feature',
// grant `read` over the built-in IndexThreshold
'.index-threshold',
// grant `read` over Uptime's TLS AlertType
'xpack.uptime.alerts.actionGroups.tls'
],
},
},
},
});
```

In this example we can see the following:
- Our feature grants any user who's assigned the `all` role in our feature the `all` role in the Alerting framework over every alert of the `my-application-id.my-alert-type` type which is created _inside_ the feature. What that means is that this privilege will allow the user to execute any of the `all` operations (listed below) on these alerts as long as their `consumer` is `my-application-id`. Below that you'll notice we've done the same with the `read` role, which is grants the Alerting Framework's `read` role privileges over these very same alerts.
- In addition, our feature grants the same privileges over any alert of type `my-application-id.my-restricted-alert-type`, which is another hypothetical alertType registered by this feature. It's worth noting though that this type has been omitted from the `read` role. What this means is that only users with the `all` role will be able to interact with alerts of this type.
- Next, lets look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over alerts of these types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in type, provided by the _Built-In Alerts_ feature, and `xpack.uptime.alerts.actionGroups.tls` is an AlertType provided by the _Uptime_ feature. Specifying these type here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use them (with `all` and `read` applied), but that isn't enough. Using another feature's AlertType is only possible if both the producer of the AlertType, and the consumer of the AlertType, explicitly grant privileges to do so. In this case, the _Built-In Alerts_ & _Uptime_ features would have to explicitly add these privileges to a role and this role would have to be granted to this user.

It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example:

```typescript
features.registerKibanaFeature({
id: 'my-application-id',
name: 'My Application',
app: [],
privileges: {
all: {
app: ['my-application-id', 'kibana'],
savedObject: {
all: [],
read: [],
},
ui: [],
api: [],
},
read: {
app: ['lens', 'kibana'],
alerting: {
all: [
'my-application-id.my-alert-type'
],
read: [
'my-application-id.my-restricted-alert-type'
],
},
savedObject: {
all: [],
read: [],
},
ui: [],
api: [],
},
},
});
```

In the above example, you note that instead of denying users with the `read` role any access to the `my-application-id.my-restricted-alert-type` type, we've decided that these users _should_ be granted `read` privileges over the _resitricted_ AlertType.
As part of that same change, we also decided that not only should they be allowed to `read` the _restricted_ AlertType, but actually, despite having `read` privileges to the feature as a whole, we do actually want to allow them to create our basic 'my-application-id.my-alert-type' AlertType, as we consider it an extension of _reading_ data in our feature, rather than _writing_ it.

### `read` privileges vs. `all` privileges
When a user is granted the `read` role in for Alerts, they will be able to execute the following api calls:
- `get`
- `find`

When a user is granted the `all` role in the Alerting Framework, they will be able to execute all of the `read` privileged api calls, but in addition they'll be granted the following calls:
- `update`

Attempting to execute any operation the user isn't privileged to execute will result in an Authorization error thrown by the AlertsClient.
4 changes: 3 additions & 1 deletion x-pack/plugins/rule_registry/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
],
"requiredPlugins": [
"alerting",
"triggersActionsUi"
"triggersActionsUi",
"features",
"security"
],
"server": true,
"ui": true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 { RacAuthorizationAuditLogger } from './audit_logger';

const createRacAuthorizationAuditLoggerMock = () => {
const mocked = ({
getAuthorizationMessage: jest.fn(),
racAuthorizationFailure: jest.fn(),
racUnscopedAuthorizationFailure: jest.fn(),
racAuthorizationSuccess: jest.fn(),
} as unknown) as jest.Mocked<RacAuthorizationAuditLogger>;
return mocked;
};

export const alertsAuthorizationAuditLoggerMock: {
create: () => jest.Mocked<RacAuthorizationAuditLogger>;
} = {
create: createRacAuthorizationAuditLoggerMock,
};
Loading