Skip to content

Commit

Permalink
[Security Solution] Endpoint RBAC integration with AppFeatures archit…
Browse files Browse the repository at this point in the history
…ecture (#158646)

# Summary

This PR adapts the endpoint RBAC to the new Serverless PLI features
architecture.
The changes are the following:

## App Features

### New appFeatures keys for endpoint

The `endpointExceptions` PLI has been added to the _Endpoint Essentials_
product tier and `endpointResponseActions` to the _Endpoint Complete_


https://github.com/elastic/kibana/blob/686bc2eeaaa87841383af5aa1959c2758e633b20/x-pack/plugins/serverless_security/common/pli/pli_config.ts#L20-L23

### Endpoint appFeatures capabilities config

The features configuration for each appFeature (PLI) has been added.
They will be configured within the Security Kibana features only when
the appFeature is enabled by the selected Security product type. (Note
that all of them will be always added in regular ESS deployments, only
in Serverless we'll have different product types)
 

https://github.com/elastic/kibana/blob/4d9f0c3a6f0c926a733b0fc113dfc20fe1223e07/x-pack/plugins/security_solution/server/lib/app_features/security_kibana_features.ts#L170-L198

These are the capabilities that seemed relevant to me for each PLI, but
I don't have enough expertise in Endpoint operations to know for sure
what Kibana sub-features and capabilities need to be included in each
appFeature. The PLIs are in a private spreadsheet with the following
descriptions.
- endpointExceptions: 

![endpointExceptions](https://github.com/machadoum/kibana/assets/17747913/3c143293-93a2-46d9-a6a5-c7dbab26b30e)

- endpointResponseActions: 

![endpointResponseActions](https://github.com/machadoum/kibana/assets/17747913/12a644bd-5ad7-475e-850a-29ca89572027)

I'll need Endpoint team members to confirm there's no missing or wrong
capability in each appFeature config.

### Host isolation capabilities

It is important to mention that in the configuration above, to have some
capabilities available we are adding some sub-features directly using
the `subFeatureIds` entry, but for host_isolation capabilities, we are
doing it in a slightly different way, using the `subFeaturesPrivileges`,
this way the privileges are added to existing subFeatures.
 
The reason is we need to have the _write_ (isolate operation) only in
payment product types, but the _read_ and _delete_ (release operation)
capabilities should be always available, to allow releasing previously
isolated hosts after a product downgrade.

To do this we always include the `host_isolation_all` and
`host_isolation_exceptions_all` subFeatures in the base configuration,
but they only contain _read_ and _delete_ capabilities by default, only
when the product tier allows the proper appFeatures the _write_
capability is added to the same subFeatures privileges.


## Endpoint Authz module

### Remove "superuser" specific check
This specific check:
```
  // user is superuser, always return true
  if (isSuperuser) {
    return true;
  }
```
Has been removed, this has no behavioral impact, superuser has all
capabilities enabled anyway.

### Remove usage of `endpointRbacEnabled` and `endpointRbacV1Enabled`
experimental flags

They are already enabled by default. superuser will still have the
authorization to access all the features. The only change is the
endpoint sub-features will always be visible in the Kibana Privilege
section of the Role management page, they were hidden when these
experimental flags were disabled.

![Role Security
sub-features](https://github.com/elastic/kibana/assets/17747913/98a9dcd8-0f03-439a-a924-a5175c59d2d5)

### Remove double _write_ check for _read_ authorizations:
We were doing unnecessary checks for the _write_ capabilities in the
_read_ authorizations, like: ```
const canReadEndpointList = canWriteEndpointList ||
hasKibanaPrivilege(fleetAuthz, 'readEndpointList');
```. Sub-features already add _read_ and _write_ capabilities on the
`all` privilege, so these double checks were unnecessary.

### Extract `hasHostIsolationExceptionsItems` flag

This flag was used to grant _read_ and _delete_ authorization for Host
Isolation Exceptions (HIE) when there is data, basically turning them
free features when there is data to perform the actions. This is needed
to allow users to remove HIE after a license downgrade scenario, which
is good.
However, we needed to do this API call from outside the auth module, in
every place we needed to call `calculateEndpointAuthz`, and we were also
adding the responsibility to do some auth-specific logic with licenses
outside the auth module, which is not good.
In addition, it is not very consistent to make authorization depend on
the existence of data to perform an action. Authorization should be
based only on the role capabilities and tiers/licenses, if some parts of
the application want to show/hide stuff depending on the data, that's
not the auth module's responsibility.
I checked all the places where we use the HIE _read_ and _delete_
authorizations, and the only place where we really need them to be
denied (when there is no data) is in the _links_, we need to remove the
HIE link from the app in this situation.
So, this PR moves the data check to the links.ts module, making the
_read_ and _delete_ permissions always granted without a license (they
will still be useless without data), the same way the `canUnIsolateHost`
authorization works. And then doing the async data check to remove the
HIE link in the _management/links.ts_ module itself, only in the last
case where we really need to know it:


https://github.com/elastic/kibana/blob/4d9f0c3a6f0c926a733b0fc113dfc20fe1223e07/x-pack/plugins/security_solution/public/management/links.ts#L257-L262

This flag extraction is unrelated to the integration of the new
architecture, I included it only to extract complexity from the _authz_
module and simplify its usage, but this change can be rolled back if we
consider it.

# Testing

- To start the application in ESS (non-serverless) mode, run it normally
with `yarn start`. Everything should keep working as usual with all
features available and capabilities should only be restricted by the
user role.

- To start the application in Serverless mode run with `yarn
serverless-security`. It sets a random root path, so access the main URL
at "http://localhost:5601/" to be redirected.
By default the "Endpoint Complete" product line is selected in the
_serverless.security.yml_ config, so everything should be available as
in ESS with the default config.


https://github.com/elastic/kibana/blob/686bc2eeaaa87841383af5aa1959c2758e633b20/config/serverless.security.yml#L11-L15

Once in Serverless mode, in order to see the difference between product
types, we can change the _Endpoint_ `product_tier` to `essentials`, as
per the pli_config, this change should remove all the capabilities
included by the `endpointResponseActions` appFeatures config.
To check how the application behaves without the `endpointExceptions`
PLI, we can remove the _Endpoint_ `product_line` entirely from the
product array, leaving the _Security_ `product_line` alone.

# Next steps

## Upselling page

The product upselling page has not been registered for endpoint pages in
this PR, so when any of these pages are unauthorized because of the
serverless product tier, and they are accessed directly by URL they
still show the `Privileges required` screen.


![Privileges_required_page](https://github.com/machadoum/kibana/assets/17747913/675076c3-3c97-4347-bc0a-90845607b50f)

This is arguably not entirely correct. However, an upselling page can be
registered to display a "Buy a higher tier" message when the privilege
is denied because of the product type, if it is unauthorized because of
the user role the "Privileges required" page will still show.
I did not include the endpoint upselling page in this PR to keep it
simple, but the registry is already implemented in the main proposal, we
can define and register them in a follow-up PR.

## Superuser role in authz module

Almost all "superuser" role conditionals have been removed from the
Endpoint authz module, but there is only one check left here:


https://github.com/elastic/kibana/blob/24330f2356537771c55b07efc3144e56ce199332/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts#L85

This `canAccessEndpointManagement` flag looks deprecated, and it seems
to be used incorrectly in the few places where it is checked. If we
could fix the places that it is used, checking the proper authz flag, we
could definitively remove the `userRoles` parameter from the
`calculateEndpointAuthz` function, this will have an impact in the
different places where this function is called since they will no longer
need any async logic.

---------

Co-authored-by: Pablo Neves Machado <pablo.nevesmachado@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 9, 2023
1 parent ab18045 commit 352d7c9
Show file tree
Hide file tree
Showing 23 changed files with 396 additions and 674 deletions.
6 changes: 5 additions & 1 deletion config/serverless.security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ xpack.uptime.enabled: false

## Enable the Serverless Security plugin
xpack.serverless.security.enabled: true
xpack.serverless.security.productTypes: [{ product_line: 'security', product_tier: 'complete' }]
xpack.serverless.security.productTypes:
[
{ product_line: 'security', product_tier: 'complete' },
{ product_line: 'endpoint', product_tier: 'complete' },
]

## Set the home route
uiSettings.overrides.defaultRoute: /app/security/get_started
Expand Down
18 changes: 18 additions & 0 deletions x-pack/plugins/fleet/common/constants/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ export const ENDPOINT_PRIVILEGES: Record<string, PrivilegeMapObject> = deepFreez
privilegeType: 'api',
privilegeName: 'readHostIsolationExceptions',
},
accessHostIsolationExceptions: {
appId: DEFAULT_APP_CATEGORIES.security.id,
privilegeSplit: '-',
privilegeType: 'api',
privilegeName: 'accessHostIsolationExceptions',
},
deleteHostIsolationExceptions: {
appId: DEFAULT_APP_CATEGORIES.security.id,
privilegeSplit: '-',
privilegeType: 'api',
privilegeName: 'deleteHostIsolationExceptions',
},
writeBlocklist: {
appId: DEFAULT_APP_CATEGORIES.security.id,
privilegeSplit: '-',
Expand Down Expand Up @@ -126,6 +138,12 @@ export const ENDPOINT_PRIVILEGES: Record<string, PrivilegeMapObject> = deepFreez
privilegeType: 'api',
privilegeName: 'writeHostIsolation',
},
writeHostIsolationRelease: {
appId: DEFAULT_APP_CATEGORIES.security.id,
privilegeSplit: '-',
privilegeType: 'api',
privilegeName: 'writeHostIsolationRelease',
},
writeProcessOperations: {
appId: DEFAULT_APP_CATEGORIES.security.id,
privilegeSplit: '-',
Expand Down

Large diffs are not rendered by default.

205 changes: 35 additions & 170 deletions x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,12 @@ import type { MaybeImmutable } from '../../types';
* level, use `calculateEndpointAuthz()`
*
* @param fleetAuthz
* @param isEndpointRbacEnabled
* @param isSuperuser
* @param privilege
*/
export function hasKibanaPrivilege(
fleetAuthz: FleetAuthz,
isEndpointRbacEnabled: boolean,
isSuperuser: boolean = false,
privilege: keyof typeof ENDPOINT_PRIVILEGES
): boolean {
// user is superuser, always return true
if (isSuperuser) {
return true;
}

// not superuser and FF not enabled, no access
if (!isEndpointRbacEnabled) {
return false;
}

// FF enabled, access based on privileges
return fleetAuthz.packagePrivileges?.endpoint?.actions[privilege].executePackageAction ?? false;
}

Expand All @@ -50,181 +35,58 @@ export function hasKibanaPrivilege(
* @param licenseService
* @param fleetAuthz
* @param userRoles
* @param isEndpointRbacEnabled
* @param permissions
* @param hasHostIsolationExceptionsItems if set to `true`, then Host Isolation Exceptions related authz properties
* may be adjusted to account for a license downgrade scenario
*/

// eslint-disable-next-line complexity
export const calculateEndpointAuthz = (
licenseService: LicenseService,
fleetAuthz: FleetAuthz,
userRoles: MaybeImmutable<string[]> = [],
isEndpointRbacEnabled: boolean = false,
hasHostIsolationExceptionsItems: boolean = false
userRoles: MaybeImmutable<string[]> = []
): EndpointAuthz => {
const isPlatinumPlusLicense = licenseService.isPlatinumPlus();
const isEnterpriseLicense = licenseService.isEnterprise();
const hasEndpointManagementAccess = userRoles.includes('superuser');

const canWriteSecuritySolution = hasKibanaPrivilege(
fleetAuthz,
true,
hasEndpointManagementAccess,
'writeSecuritySolution'
);
const canReadSecuritySolution =
canWriteSecuritySolution ||
hasKibanaPrivilege(fleetAuthz, true, hasEndpointManagementAccess, 'readSecuritySolution');
const canWriteEndpointList = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writeEndpointList'
);
const canReadEndpointList =
canWriteEndpointList ||
hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'readEndpointList'
);
const canWritePolicyManagement = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writePolicyManagement'
);
const canReadPolicyManagement =
canWritePolicyManagement ||
hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'readPolicyManagement'
);
const canWriteActionsLogManagement = hasKibanaPrivilege(
const canWriteSecuritySolution = hasKibanaPrivilege(fleetAuthz, 'writeSecuritySolution');
const canReadSecuritySolution = hasKibanaPrivilege(fleetAuthz, 'readSecuritySolution');
const canWriteEndpointList = hasKibanaPrivilege(fleetAuthz, 'writeEndpointList');
const canReadEndpointList = hasKibanaPrivilege(fleetAuthz, 'readEndpointList');
const canWritePolicyManagement = hasKibanaPrivilege(fleetAuthz, 'writePolicyManagement');
const canReadPolicyManagement = hasKibanaPrivilege(fleetAuthz, 'readPolicyManagement');
const canWriteActionsLogManagement = hasKibanaPrivilege(fleetAuthz, 'writeActionsLogManagement');
const canReadActionsLogManagement = hasKibanaPrivilege(fleetAuthz, 'readActionsLogManagement');
const canIsolateHost = hasKibanaPrivilege(fleetAuthz, 'writeHostIsolation');
const canUnIsolateHost = hasKibanaPrivilege(fleetAuthz, 'writeHostIsolationRelease');
const canWriteProcessOperations = hasKibanaPrivilege(fleetAuthz, 'writeProcessOperations');
const canWriteTrustedApplications = hasKibanaPrivilege(fleetAuthz, 'writeTrustedApplications');
const canReadTrustedApplications = hasKibanaPrivilege(fleetAuthz, 'readTrustedApplications');
const canWriteHostIsolationExceptions = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writeActionsLogManagement'
);
const canReadActionsLogManagement =
canWriteActionsLogManagement ||
hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'readActionsLogManagement'
);
const canIsolateHost = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writeHostIsolation'
);
const canWriteProcessOperations = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writeProcessOperations'
);
const canWriteTrustedApplications = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writeTrustedApplications'
);
const canReadTrustedApplications =
canWriteTrustedApplications ||
hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'readTrustedApplications'
);

const hasWriteHostIsolationExceptionsPermission = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writeHostIsolationExceptions'
);
const canWriteHostIsolationExceptions =
hasWriteHostIsolationExceptionsPermission && isPlatinumPlusLicense;

const hasReadHostIsolationExceptionsPermission =
hasWriteHostIsolationExceptionsPermission ||
hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'readHostIsolationExceptions'
);
// Calculate the Host Isolation Exceptions Authz. Some of these authz properties could be
// set to `true` in cases where license was downgraded, but entries still exist.
const canReadHostIsolationExceptions =
canWriteHostIsolationExceptions ||
(hasReadHostIsolationExceptionsPermission &&
// We still allow `read` if not Platinum license, but entries exists for HIE
(isPlatinumPlusLicense || hasHostIsolationExceptionsItems));

const canDeleteHostIsolationExceptions =
canWriteHostIsolationExceptions ||
// Should be able to delete if host isolation exceptions exists and license is not platinum+
(hasWriteHostIsolationExceptionsPermission &&
!isPlatinumPlusLicense &&
hasHostIsolationExceptionsItems);

const canWriteBlocklist = hasKibanaPrivilege(
const canReadHostIsolationExceptions = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writeBlocklist'
'readHostIsolationExceptions'
);
const canReadBlocklist =
canWriteBlocklist ||
hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'readBlocklist'
);
const canWriteEventFilters = hasKibanaPrivilege(
const canAccessHostIsolationExceptions = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writeEventFilters'
'accessHostIsolationExceptions'
);
const canReadEventFilters =
canWriteEventFilters ||
hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'readEventFilters'
);
const canWriteFileOperations = hasKibanaPrivilege(
const canDeleteHostIsolationExceptions = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writeFileOperations'
'deleteHostIsolationExceptions'
);
const canWriteBlocklist = hasKibanaPrivilege(fleetAuthz, 'writeBlocklist');
const canReadBlocklist = hasKibanaPrivilege(fleetAuthz, 'readBlocklist');
const canWriteEventFilters = hasKibanaPrivilege(fleetAuthz, 'writeEventFilters');
const canReadEventFilters = hasKibanaPrivilege(fleetAuthz, 'readEventFilters');
const canWriteFileOperations = hasKibanaPrivilege(fleetAuthz, 'writeFileOperations');

const canWriteExecuteOperations = hasKibanaPrivilege(
fleetAuthz,
isEndpointRbacEnabled,
hasEndpointManagementAccess,
'writeExecuteOperations'
);
const canWriteExecuteOperations = hasKibanaPrivilege(fleetAuthz, 'writeExecuteOperations');

return {
canWriteSecuritySolution,
canReadSecuritySolution,
canAccessFleet: fleetAuthz?.fleet.all ?? userRoles.includes('superuser'),
canAccessEndpointManagement: hasEndpointManagementAccess,
canAccessFleet: fleetAuthz?.fleet.all ?? false,
canAccessEndpointManagement: hasEndpointManagementAccess, // TODO: is this one deprecated? it is the only place we need to check for superuser.
canCreateArtifactsByPolicy: isPlatinumPlusLicense,
canWriteEndpointList,
canReadEndpointList,
Expand All @@ -235,13 +97,14 @@ export const calculateEndpointAuthz = (
canAccessEndpointActionsLogManagement: canReadActionsLogManagement && isPlatinumPlusLicense,
// Response Actions
canIsolateHost: canIsolateHost && isPlatinumPlusLicense,
canUnIsolateHost: canIsolateHost,
canUnIsolateHost,
canKillProcess: canWriteProcessOperations && isEnterpriseLicense,
canSuspendProcess: canWriteProcessOperations && isEnterpriseLicense,
canGetRunningProcesses: canWriteProcessOperations && isEnterpriseLicense,
canAccessResponseConsole:
isEnterpriseLicense &&
(canIsolateHost ||
canUnIsolateHost ||
canWriteProcessOperations ||
canWriteFileOperations ||
canWriteExecuteOperations),
Expand All @@ -250,7 +113,8 @@ export const calculateEndpointAuthz = (
// artifacts
canWriteTrustedApplications,
canReadTrustedApplications,
canWriteHostIsolationExceptions,
canWriteHostIsolationExceptions: canWriteHostIsolationExceptions && isPlatinumPlusLicense,
canAccessHostIsolationExceptions: canAccessHostIsolationExceptions && isPlatinumPlusLicense,
canReadHostIsolationExceptions,
canDeleteHostIsolationExceptions,
canWriteBlocklist,
Expand All @@ -275,7 +139,7 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => {
canWriteActionsLogManagement: false,
canReadActionsLogManagement: false,
canIsolateHost: false,
canUnIsolateHost: true,
canUnIsolateHost: false,
canKillProcess: false,
canSuspendProcess: false,
canGetRunningProcesses: false,
Expand All @@ -285,6 +149,7 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => {
canWriteTrustedApplications: false,
canReadTrustedApplications: false,
canWriteHostIsolationExceptions: false,
canAccessHostIsolationExceptions: false,
canReadHostIsolationExceptions: false,
canDeleteHostIsolationExceptions: false,
canWriteBlocklist: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export const getEndpointAuthzInitialStateMock = (

return mockPrivileges;
}, {} as EndpointAuthz),
// this one is currently treated special in that everyone can un-isolate
canUnIsolateHost: true,
...overrides,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type ConsoleResponseActionCommands = typeof CONSOLE_RESPONSE_ACTION_COMMA

export type ResponseConsoleRbacControls =
| 'writeHostIsolation'
| 'writeHostIsolationRelease'
| 'writeProcessOperations'
| 'writeFileOperations'
| 'writeExecuteOperations';
Expand All @@ -75,7 +76,7 @@ export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_RBAC_FEATURE_CONTROL: Record<
ResponseConsoleRbacControls
> = Object.freeze({
isolate: 'writeHostIsolation',
release: 'writeHostIsolation',
release: 'writeHostIsolationRelease',
'kill-process': 'writeProcessOperations',
'suspend-process': 'writeProcessOperations',
processes: 'writeProcessOperations',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export interface EndpointAuthz {
canWriteHostIsolationExceptions: boolean;
/** if user has read permissions for host isolation exceptions */
canReadHostIsolationExceptions: boolean;
/**
* if user has permissions to access host isolation exceptions. This could be set to false, while
* `canReadHostIsolationExceptions` is true in cases where the license might have been downgraded.
* It is used to show the UI elements that allow users to navigate to the host isolation exceptions.
*/
canAccessHostIsolationExceptions: boolean;
/**
* if user has permissions to delete host isolation exceptions. This could be set to true, while
* `canWriteHostIsolationExceptions` is false in cases where the license might have been downgraded.
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/security_solution/common/types/app_features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ export enum AppFeatureSecurityKey {
* Enables Advanced Insights (Entity Risk, GenAI)
*/
advancedInsights = 'advanced_insights',
/**
* Enables Endpoint Response Actions like isolate host, trusted apps, blocklist, etc.
*/
endpointResponseActions = 'endpoint_response_actions',
/**
* Enables Endpoint Exceptions like isolate host, trusted apps, blocklist, etc.
*/
endpointExceptions = 'endpoint_exceptions',
}

export enum AppFeatureCasesKey {
Expand Down
Loading

0 comments on commit 352d7c9

Please sign in to comment.