[Security Solution] Endpoint RBAC integration with AppFeatures architecture (#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_


686bc2eeaa/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)
 

4d9f0c3a6f/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](3c143293-93a2-46d9-a6a5-c7dbab26b30e)

- endpointResponseActions: 

![endpointResponseActions](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](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:


4d9f0c3a6f/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.


686bc2eeaa/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](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:


24330f2356/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>
This commit is contained in:
Sergi Massaneda 2023-06-09 12:03:21 +02:00 committed by GitHub
parent ab18045968
commit 352d7c9ea7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 408 additions and 686 deletions

View file

@ -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

View file

@ -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: '-',
@ -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: '-',

View file

@ -21,253 +21,223 @@ describe('Endpoint Authz service', () => {
let fleetAuthz: FleetAuthz;
let userRoles: string[];
const responseConsolePrivileges = CONSOLE_RESPONSE_ACTION_COMMANDS.slice().reduce<
ResponseConsoleRbacControls[]
>((acc, e) => {
const item = RESPONSE_CONSOLE_ACTION_COMMANDS_TO_RBAC_FEATURE_CONTROL[e];
if (!acc.includes(item)) {
acc.push(item);
}
return acc;
}, []);
beforeEach(() => {
licenseService = createLicenseServiceMock();
fleetAuthz = createFleetAuthzMock();
userRoles = ['superuser'];
userRoles = [];
});
describe('calculateEndpointAuthz()', () => {
describe('and `fleet.all` access is true', () => {
it.each<EndpointAuthzKeyList>([
['canAccessFleet'],
['canAccessEndpointManagement'],
['canIsolateHost'],
['canUnIsolateHost'],
['canKillProcess'],
['canSuspendProcess'],
['canGetRunningProcesses'],
])('should set `%s` to `true`', (authProperty) => {
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles)[authProperty]).toBe(
true
);
});
it('should set `canIsolateHost` to false if not proper license', () => {
licenseService.isPlatinumPlus.mockReturnValue(false);
it('should set `canIsolateHost` to false if not proper license', () => {
licenseService.isPlatinumPlus.mockReturnValue(false);
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canIsolateHost).toBe(
false
);
});
it('should set `canKillProcess` to false if not proper license', () => {
licenseService.isEnterprise.mockReturnValue(false);
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canKillProcess).toBe(
false
);
});
it('should set `canSuspendProcess` to false if not proper license', () => {
licenseService.isEnterprise.mockReturnValue(false);
expect(
calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canSuspendProcess
).toBe(false);
});
it('should set `canGetRunningProcesses` to false if not proper license', () => {
licenseService.isEnterprise.mockReturnValue(false);
expect(
calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canGetRunningProcesses
).toBe(false);
});
it('should set `canUnIsolateHost` to true even if not proper license', () => {
licenseService.isPlatinumPlus.mockReturnValue(false);
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canUnIsolateHost).toBe(
true
);
});
it(`should allow Host Isolation Exception read/delete when license is not Platinum+, but entries exist`, () => {
licenseService.isPlatinumPlus.mockReturnValue(false);
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, false, true)).toEqual(
expect.objectContaining({
canWriteHostIsolationExceptions: false,
canReadHostIsolationExceptions: true,
canDeleteHostIsolationExceptions: true,
})
);
});
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canIsolateHost).toBe(
false
);
});
describe('and `fleet.all` access is false', () => {
beforeEach(() => {
fleetAuthz.fleet.all = false;
userRoles = [];
});
it('should set `canKillProcess` to false if not proper license', () => {
licenseService.isEnterprise.mockReturnValue(false);
it.each<EndpointAuthzKeyList>([
['canAccessFleet'],
['canAccessEndpointManagement'],
['canIsolateHost'],
['canUnIsolateHost'],
['canKillProcess'],
['canSuspendProcess'],
['canGetRunningProcesses'],
])('should set `%s` to `false`', (authProperty) => {
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles)[authProperty]).toBe(
false
);
});
it('should set `canUnIsolateHost` to false when policy is also not platinum', () => {
licenseService.isPlatinumPlus.mockReturnValue(false);
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canUnIsolateHost).toBe(
false
);
});
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canKillProcess).toBe(
false
);
});
describe('and endpoint rbac is enabled', () => {
const responseConsolePrivileges = CONSOLE_RESPONSE_ACTION_COMMANDS.slice().reduce<
ResponseConsoleRbacControls[]
>((acc, e) => {
const item = RESPONSE_CONSOLE_ACTION_COMMANDS_TO_RBAC_FEATURE_CONTROL[e];
if (!acc.includes(item)) {
acc.push(item);
}
return acc;
}, []);
it('should set `canSuspendProcess` to false if not proper license', () => {
licenseService.isEnterprise.mockReturnValue(false);
beforeEach(() => {
userRoles = [];
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canSuspendProcess).toBe(
false
);
});
it('should set `canGetRunningProcesses` to false if not proper license', () => {
licenseService.isEnterprise.mockReturnValue(false);
expect(
calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canGetRunningProcesses
).toBe(false);
});
it('should set `canUnIsolateHost` to true even if not proper license', () => {
licenseService.isPlatinumPlus.mockReturnValue(false);
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canUnIsolateHost).toBe(
true
);
});
it(`should allow Host Isolation Exception read/delete when license is not Platinum+`, () => {
licenseService.isPlatinumPlus.mockReturnValue(false);
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles)).toEqual(
expect.objectContaining({
canWriteHostIsolationExceptions: false,
canAccessHostIsolationExceptions: false,
canReadHostIsolationExceptions: true,
canDeleteHostIsolationExceptions: true,
})
);
});
it('should not give canAccessFleet if `fleet.all` is false', () => {
fleetAuthz.fleet.all = false;
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canAccessFleet).toBe(
false
);
});
it('should not give canAccessEndpointManagement if not superuser', () => {
userRoles = [];
expect(
calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canAccessEndpointManagement
).toBe(false);
});
it('should give canAccessFleet if `fleet.all` is true', () => {
fleetAuthz.fleet.all = true;
expect(calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canAccessFleet).toBe(
true
);
});
it('should give canAccessEndpointManagement if superuser', () => {
userRoles = ['superuser'];
expect(
calculateEndpointAuthz(licenseService, fleetAuthz, userRoles).canAccessEndpointManagement
).toBe(true);
userRoles = [];
});
it.each<[EndpointAuthzKeyList[number], string]>([
['canWriteEndpointList', 'writeEndpointList'],
['canReadEndpointList', 'readEndpointList'],
['canWritePolicyManagement', 'writePolicyManagement'],
['canReadPolicyManagement', 'readPolicyManagement'],
['canWriteActionsLogManagement', 'writeActionsLogManagement'],
['canReadActionsLogManagement', 'readActionsLogManagement'],
['canAccessEndpointActionsLogManagement', 'readActionsLogManagement'],
['canIsolateHost', 'writeHostIsolation'],
['canUnIsolateHost', 'writeHostIsolation'],
['canKillProcess', 'writeProcessOperations'],
['canSuspendProcess', 'writeProcessOperations'],
['canGetRunningProcesses', 'writeProcessOperations'],
['canWriteExecuteOperations', 'writeExecuteOperations'],
['canWriteFileOperations', 'writeFileOperations'],
['canWriteTrustedApplications', 'writeTrustedApplications'],
['canReadTrustedApplications', 'readTrustedApplications'],
['canWriteHostIsolationExceptions', 'writeHostIsolationExceptions'],
['canAccessHostIsolationExceptions', 'accessHostIsolationExceptions'],
['canReadHostIsolationExceptions', 'readHostIsolationExceptions'],
['canDeleteHostIsolationExceptions', 'deleteHostIsolationExceptions'],
['canWriteBlocklist', 'writeBlocklist'],
['canReadBlocklist', 'readBlocklist'],
['canWriteEventFilters', 'writeEventFilters'],
['canReadEventFilters', 'readEventFilters'],
])('%s should be true if `packagePrivilege.%s` is `true`', (auth) => {
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles);
expect(authz[auth]).toBe(true);
});
it.each<[EndpointAuthzKeyList[number], string[]]>([
['canWriteEndpointList', ['writeEndpointList']],
['canReadEndpointList', ['readEndpointList']],
['canWritePolicyManagement', ['writePolicyManagement']],
['canReadPolicyManagement', ['readPolicyManagement']],
['canWriteActionsLogManagement', ['writeActionsLogManagement']],
['canReadActionsLogManagement', ['readActionsLogManagement']],
['canAccessEndpointActionsLogManagement', ['readActionsLogManagement']],
['canIsolateHost', ['writeHostIsolation']],
['canUnIsolateHost', ['writeHostIsolationRelease']],
['canKillProcess', ['writeProcessOperations']],
['canSuspendProcess', ['writeProcessOperations']],
['canGetRunningProcesses', ['writeProcessOperations']],
['canWriteExecuteOperations', ['writeExecuteOperations']],
['canWriteFileOperations', ['writeFileOperations']],
['canWriteTrustedApplications', ['writeTrustedApplications']],
['canReadTrustedApplications', ['readTrustedApplications']],
['canWriteHostIsolationExceptions', ['writeHostIsolationExceptions']],
['canAccessHostIsolationExceptions', ['accessHostIsolationExceptions']],
['canReadHostIsolationExceptions', ['readHostIsolationExceptions']],
['canDeleteHostIsolationExceptions', ['deleteHostIsolationExceptions']],
['canWriteBlocklist', ['writeBlocklist']],
['canReadBlocklist', ['readBlocklist']],
['canWriteEventFilters', ['writeEventFilters']],
['canReadEventFilters', ['readEventFilters']],
// all dependent privileges are false and so it should be false
['canAccessResponseConsole', responseConsolePrivileges],
])('%s should be false if `packagePrivilege.%s` is `false`', (auth, privileges) => {
privileges.forEach((privilege) => {
fleetAuthz.packagePrivileges!.endpoint.actions[privilege].executePackageAction = false;
});
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles);
expect(authz[auth]).toBe(false);
});
it.each<[EndpointAuthzKeyList[number], string]>([
['canWriteEndpointList', 'writeEndpointList'],
['canReadEndpointList', 'readEndpointList'],
['canWritePolicyManagement', 'writePolicyManagement'],
['canReadPolicyManagement', 'readPolicyManagement'],
['canWriteActionsLogManagement', 'writeActionsLogManagement'],
['canReadActionsLogManagement', 'readActionsLogManagement'],
['canAccessEndpointActionsLogManagement', 'readActionsLogManagement'],
['canIsolateHost', 'writeHostIsolation'],
['canUnIsolateHost', 'writeHostIsolation'],
['canKillProcess', 'writeProcessOperations'],
['canSuspendProcess', 'writeProcessOperations'],
['canGetRunningProcesses', 'writeProcessOperations'],
['canWriteExecuteOperations', 'writeExecuteOperations'],
['canWriteFileOperations', 'writeFileOperations'],
['canWriteTrustedApplications', 'writeTrustedApplications'],
['canReadTrustedApplications', 'readTrustedApplications'],
['canWriteHostIsolationExceptions', 'writeHostIsolationExceptions'],
['canReadHostIsolationExceptions', 'readHostIsolationExceptions'],
['canWriteBlocklist', 'writeBlocklist'],
['canReadBlocklist', 'readBlocklist'],
['canWriteEventFilters', 'writeEventFilters'],
['canReadEventFilters', 'readEventFilters'],
])('%s should be true if `packagePrivilege.%s` is `true`', (auth) => {
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
expect(authz[auth]).toBe(true);
});
it.each<[EndpointAuthzKeyList[number], string[]]>([
['canWriteEndpointList', ['writeEndpointList']],
['canReadEndpointList', ['writeEndpointList', 'readEndpointList']],
['canWritePolicyManagement', ['writePolicyManagement']],
['canReadPolicyManagement', ['writePolicyManagement', 'readPolicyManagement']],
['canWriteActionsLogManagement', ['writeActionsLogManagement']],
['canReadActionsLogManagement', ['writeActionsLogManagement', 'readActionsLogManagement']],
[
'canAccessEndpointActionsLogManagement',
['writeActionsLogManagement', 'readActionsLogManagement'],
],
['canIsolateHost', ['writeHostIsolation']],
['canUnIsolateHost', ['writeHostIsolation']],
['canKillProcess', ['writeProcessOperations']],
['canSuspendProcess', ['writeProcessOperations']],
['canGetRunningProcesses', ['writeProcessOperations']],
['canWriteExecuteOperations', ['writeExecuteOperations']],
['canWriteFileOperations', ['writeFileOperations']],
['canWriteTrustedApplications', ['writeTrustedApplications']],
['canReadTrustedApplications', ['writeTrustedApplications', 'readTrustedApplications']],
['canWriteHostIsolationExceptions', ['writeHostIsolationExceptions']],
[
'canReadHostIsolationExceptions',
['writeHostIsolationExceptions', 'readHostIsolationExceptions'],
],
['canWriteBlocklist', ['writeBlocklist']],
['canReadBlocklist', ['writeBlocklist', 'readBlocklist']],
['canWriteEventFilters', ['writeEventFilters']],
['canReadEventFilters', ['writeEventFilters', 'readEventFilters']],
// all dependent privileges are false and so it should be false
['canAccessResponseConsole', responseConsolePrivileges],
])('%s should be false if `packagePrivilege.%s` is `false`', (auth, privileges) => {
// read permission checks for write || read so we need to set both to false
it.each<[EndpointAuthzKeyList[number], string[]]>([
['canWriteEndpointList', ['writeEndpointList']],
['canReadEndpointList', ['readEndpointList']],
['canWritePolicyManagement', ['writePolicyManagement']],
['canReadPolicyManagement', ['readPolicyManagement']],
['canWriteActionsLogManagement', ['writeActionsLogManagement']],
['canReadActionsLogManagement', ['readActionsLogManagement']],
['canAccessEndpointActionsLogManagement', ['readActionsLogManagement']],
['canIsolateHost', ['writeHostIsolation']],
['canUnIsolateHost', ['writeHostIsolationRelease']],
['canKillProcess', ['writeProcessOperations']],
['canSuspendProcess', ['writeProcessOperations']],
['canGetRunningProcesses', ['writeProcessOperations']],
['canWriteExecuteOperations', ['writeExecuteOperations']],
['canWriteFileOperations', ['writeFileOperations']],
['canWriteTrustedApplications', ['writeTrustedApplications']],
['canReadTrustedApplications', ['readTrustedApplications']],
['canWriteHostIsolationExceptions', ['writeHostIsolationExceptions']],
['canAccessHostIsolationExceptions', ['accessHostIsolationExceptions']],
['canReadHostIsolationExceptions', ['readHostIsolationExceptions']],
['canWriteBlocklist', ['writeBlocklist']],
['canReadBlocklist', ['readBlocklist']],
['canWriteEventFilters', ['writeEventFilters']],
['canReadEventFilters', ['readEventFilters']],
// all dependent privileges are false and so it should be false
['canAccessResponseConsole', responseConsolePrivileges],
])(
'%s should be false if `packagePrivilege.%s` is `false` and user roles is undefined',
(auth, privileges) => {
privileges.forEach((privilege) => {
fleetAuthz.packagePrivileges!.endpoint.actions[privilege].executePackageAction = false;
});
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, undefined);
expect(authz[auth]).toBe(false);
});
}
);
it.each<[EndpointAuthzKeyList[number], string[]]>([
['canWriteEndpointList', ['writeEndpointList']],
['canReadEndpointList', ['writeEndpointList', 'readEndpointList']],
['canWritePolicyManagement', ['writePolicyManagement']],
['canReadPolicyManagement', ['writePolicyManagement', 'readPolicyManagement']],
['canWriteActionsLogManagement', ['writeActionsLogManagement']],
['canReadActionsLogManagement', ['writeActionsLogManagement', 'readActionsLogManagement']],
[
'canAccessEndpointActionsLogManagement',
['writeActionsLogManagement', 'readActionsLogManagement'],
],
['canIsolateHost', ['writeHostIsolation']],
['canUnIsolateHost', ['writeHostIsolation']],
['canKillProcess', ['writeProcessOperations']],
['canSuspendProcess', ['writeProcessOperations']],
['canGetRunningProcesses', ['writeProcessOperations']],
['canWriteExecuteOperations', ['writeExecuteOperations']],
['canWriteFileOperations', ['writeFileOperations']],
['canWriteTrustedApplications', ['writeTrustedApplications']],
['canReadTrustedApplications', ['writeTrustedApplications', 'readTrustedApplications']],
['canWriteHostIsolationExceptions', ['writeHostIsolationExceptions']],
[
'canReadHostIsolationExceptions',
['writeHostIsolationExceptions', 'readHostIsolationExceptions'],
],
['canWriteBlocklist', ['writeBlocklist']],
['canReadBlocklist', ['writeBlocklist', 'readBlocklist']],
['canWriteEventFilters', ['writeEventFilters']],
['canReadEventFilters', ['writeEventFilters', 'readEventFilters']],
// all dependent privileges are false and so it should be false
['canAccessResponseConsole', responseConsolePrivileges],
])(
'%s should be false if `packagePrivilege.%s` is `false` and user roles is undefined',
(auth, privileges) => {
// read permission checks for write || read so we need to set both to false
privileges.forEach((privilege) => {
fleetAuthz.packagePrivileges!.endpoint.actions[privilege].executePackageAction = false;
});
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, undefined, true);
expect(authz[auth]).toBe(false);
}
);
it.each(responseConsolePrivileges)(
'canAccessResponseConsole should be true if %s for CONSOLE privileges is true',
(responseConsolePrivilege) => {
// set all to false
responseConsolePrivileges.forEach((p) => {
fleetAuthz.packagePrivileges!.endpoint.actions[p].executePackageAction = false;
});
// set one of them to true
fleetAuthz.packagePrivileges!.endpoint.actions[
responseConsolePrivilege
].executePackageAction = true;
it.each(responseConsolePrivileges)(
'canAccessResponseConsole should be true if %s for CONSOLE privileges is true',
(responseConsolePrivilege) => {
// set all to false
responseConsolePrivileges.forEach((p) => {
fleetAuthz.packagePrivileges!.endpoint.actions[p].executePackageAction = false;
});
// set one of them to true
fleetAuthz.packagePrivileges!.endpoint.actions[
responseConsolePrivilege
].executePackageAction = true;
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles, true);
expect(authz.canAccessResponseConsole).toBe(true);
}
);
});
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles);
expect(authz.canAccessResponseConsole).toBe(true);
}
);
});
describe('getEndpointAuthzInitialState()', () => {
@ -287,7 +257,7 @@ describe('Endpoint Authz service', () => {
canWriteActionsLogManagement: false,
canReadActionsLogManagement: false,
canIsolateHost: false,
canUnIsolateHost: true,
canUnIsolateHost: false,
canKillProcess: false,
canSuspendProcess: false,
canGetRunningProcesses: false,
@ -297,6 +267,7 @@ describe('Endpoint Authz service', () => {
canWriteTrustedApplications: false,
canReadTrustedApplications: false,
canWriteHostIsolationExceptions: false,
canAccessHostIsolationExceptions: false,
canReadHostIsolationExceptions: false,
canWriteBlocklist: false,
canReadBlocklist: false,

View file

@ -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;
}
@ -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(
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,
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(
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,
@ -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),
@ -250,7 +113,8 @@ export const calculateEndpointAuthz = (
// artifacts
canWriteTrustedApplications,
canReadTrustedApplications,
canWriteHostIsolationExceptions,
canWriteHostIsolationExceptions: canWriteHostIsolationExceptions && isPlatinumPlusLicense,
canAccessHostIsolationExceptions: canAccessHostIsolationExceptions && isPlatinumPlusLicense,
canReadHostIsolationExceptions,
canDeleteHostIsolationExceptions,
canWriteBlocklist,
@ -275,7 +139,7 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => {
canWriteActionsLogManagement: false,
canReadActionsLogManagement: false,
canIsolateHost: false,
canUnIsolateHost: true,
canUnIsolateHost: false,
canKillProcess: false,
canSuspendProcess: false,
canGetRunningProcesses: false,
@ -285,6 +149,7 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => {
canWriteTrustedApplications: false,
canReadTrustedApplications: false,
canWriteHostIsolationExceptions: false,
canAccessHostIsolationExceptions: false,
canReadHostIsolationExceptions: false,
canDeleteHostIsolationExceptions: false,
canWriteBlocklist: false,

View file

@ -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,
};

View file

@ -63,6 +63,7 @@ export type ConsoleResponseActionCommands = typeof CONSOLE_RESPONSE_ACTION_COMMA
export type ResponseConsoleRbacControls =
| 'writeHostIsolation'
| 'writeHostIsolationRelease'
| 'writeProcessOperations'
| 'writeFileOperations'
| 'writeExecuteOperations';
@ -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',

View file

@ -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.

View file

@ -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 {

View file

@ -6,22 +6,18 @@
*/
import type { RenderHookResult, RenderResult } from '@testing-library/react-hooks';
import { act, renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react-hooks';
import { securityMock } from '@kbn/security-plugin/public/mocks';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { createFleetAuthzMock } from '@kbn/fleet-plugin/common/mocks';
import type { EndpointPrivileges } from '../../../../../common/endpoint/types';
import { useCurrentUser, useKibana, useHttp as _useHttp } from '../../../lib/kibana';
import { useCurrentUser, useKibana } from '../../../lib/kibana';
import { licenseService } from '../../../hooks/use_license';
import { useEndpointPrivileges } from './use_endpoint_privileges';
import { getEndpointPrivilegesInitialStateMock } from './mocks';
import { getEndpointPrivilegesInitialState } from './utils';
import { exceptionsListAllHttpMocks } from '../../../../management/mocks';
import { getDeferred } from '../../../../management/mocks/utils';
import { waitFor } from '@testing-library/react';
import type { HttpFetchOptionsWithPath, HttpSetup } from '@kbn/core-http-browser';
jest.mock('../../../lib/kibana');
jest.mock('../../../hooks/use_license', () => {
@ -38,7 +34,6 @@ jest.mock('../../../hooks/use_license', () => {
});
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
const useHttpMock = _useHttp as jest.Mock;
const licenseServiceMock = licenseService as jest.Mocked<typeof licenseService>;
describe('When using useEndpointPrivileges hook', () => {
@ -98,61 +93,4 @@ describe('When using useEndpointPrivileges hook', () => {
render();
expect(result.current).toEqual({ ...getEndpointPrivilegesInitialState(), loading: false });
});
it.each([
['HIE exist', true],
['No HIE exist', false],
])(
`should check if Host Isolation Exceptions exist when license is not Platinum+ (%s)`,
async (_, hasHIE) => {
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
const http = useKibanaMock().services.http as jest.Mocked<HttpSetup>;
const deferred = getDeferred();
const apiMock = exceptionsListAllHttpMocks(http);
useHttpMock.mockReturnValue(http);
const apiResponse = apiMock.responseProvider.exceptionsFind({
query: {},
} as HttpFetchOptionsWithPath);
apiMock.responseProvider.exceptionsFind.mockImplementation(() => {
if (hasHIE) {
return apiResponse;
}
return {
...apiResponse,
total: 0,
data: [],
};
});
// Hold on to the Host Isolation Exceptions API all
apiMock.responseProvider.exceptionsFind.mockDelay.mockReturnValue(deferred.promise);
const { rerender } = render();
expect(result.current).toEqual(getEndpointPrivilegesInitialState());
// release HIE api call
act(() => {
deferred.resolve();
});
rerender();
await waitFor(() => {
expect(apiMock.responseProvider.exceptionsFind).toHaveBeenCalled();
});
expect(result.current).toEqual(
getEndpointPrivilegesInitialStateMock({
canCreateArtifactsByPolicy: false,
canIsolateHost: false,
canAccessEndpointActionsLogManagement: false,
canWriteHostIsolationExceptions: false,
canReadHostIsolationExceptions: hasHIE,
canDeleteHostIsolationExceptions: hasHIE,
})
);
}
);
});

View file

@ -8,9 +8,7 @@
import { useEffect, useMemo, useState } from 'react';
import { isEmpty } from 'lodash';
import { useIsMounted } from '@kbn/securitysolution-hook-utils';
import { checkArtifactHasData } from '../../../../management/services/exceptions_list/check_artifact_has_data';
import { HostIsolationExceptionsApiClient } from '../../../../management/pages/host_isolation_exceptions/host_isolation_exceptions_api_client';
import { useCurrentUser, useHttp, useKibana } from '../../../lib/kibana';
import { useCurrentUser, useKibana } from '../../../lib/kibana';
import { useLicense } from '../../../hooks/use_license';
import type {
EndpointPrivileges,
@ -31,7 +29,6 @@ import { useSecuritySolutionStartDependencies } from './security_solution_start_
*/
export const useEndpointPrivileges = (): Immutable<EndpointPrivileges> => {
const isMounted = useIsMounted();
const http = useHttp();
const user = useCurrentUser();
const kibanaServices = useKibana().services;
@ -43,42 +40,21 @@ export const useEndpointPrivileges = (): Immutable<EndpointPrivileges> => {
const fleetAuthz = fleetServicesFromUseKibana?.authz ?? fleetServicesFromPluginStart?.authz;
const licenseService = useLicense();
const isPlatinumPlus = licenseService.isPlatinumPlus();
const [userRolesCheckDone, setUserRolesCheckDone] = useState<boolean>(false);
const [userRoles, setUserRoles] = useState<MaybeImmutable<string[]>>([]);
const [checkHostIsolationExceptionsDone, setCheckHostIsolationExceptionsDone] =
useState<boolean>(false);
const [hasHostIsolationExceptionsItems, setHasHostIsolationExceptionsItems] =
useState<boolean>(false);
const privileges = useMemo(() => {
const loading = !userRolesCheckDone || !user || !checkHostIsolationExceptionsDone;
const loading = !userRolesCheckDone || !user;
const privilegeList: EndpointPrivileges = Object.freeze({
loading,
...(!loading && fleetAuthz && !isEmpty(user)
? calculateEndpointAuthz(
licenseService,
fleetAuthz,
userRoles,
true,
hasHostIsolationExceptionsItems
)
? calculateEndpointAuthz(licenseService, fleetAuthz, userRoles)
: getEndpointAuthzInitialState()),
});
return privilegeList;
}, [
userRolesCheckDone,
user,
checkHostIsolationExceptionsDone,
fleetAuthz,
licenseService,
userRoles,
hasHostIsolationExceptionsItems,
]);
}, [userRolesCheckDone, user, fleetAuthz, licenseService, userRoles]);
// get user roles
useEffect(() => {
@ -90,29 +66,5 @@ export const useEndpointPrivileges = (): Immutable<EndpointPrivileges> => {
})();
}, [isMounted, user]);
// Check if Host Isolation Exceptions exist if license is not Platinum+
useEffect(() => {
if (!isPlatinumPlus) {
// Reset these back to false. Case license is changed while the user is logged in.
setHasHostIsolationExceptionsItems(false);
setCheckHostIsolationExceptionsDone(false);
checkArtifactHasData(HostIsolationExceptionsApiClient.getInstance(http))
.then((hasData) => {
if (isMounted()) {
setHasHostIsolationExceptionsItems(hasData);
}
})
.finally(() => {
if (isMounted()) {
setCheckHostIsolationExceptionsDone(true);
}
});
} else {
setHasHostIsolationExceptionsItems(true);
setCheckHostIsolationExceptionsDone(true);
}
}, [http, isMounted, isPlatinumPlus]);
return privileges;
};

View file

@ -50,6 +50,7 @@ describe('When using the release action from response actions console', () => {
endpointCapabilities: [...capabilities],
endpointPrivileges: {
...getEndpointAuthzInitialState(),
canUnIsolateHost: true,
loading: false,
},
}),

View file

@ -19,10 +19,7 @@ import { getEndpointAuthzInitialStateMock } from '../../common/endpoint/service/
import { licenseService as _licenseService } from '../common/hooks/use_license';
import type { LicenseService } from '../../common/license';
import { createLicenseServiceMock } from '../../common/license/mocks';
import type { FleetAuthz } from '@kbn/fleet-plugin/common';
import { createFleetAuthzMock } from '@kbn/fleet-plugin/common/mocks';
import type { DeepPartial } from '@kbn/utility-types';
import { merge } from 'lodash';
import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants';
jest.mock('../common/hooks/use_license');
@ -48,21 +45,17 @@ describe('links', () => {
links: links.links?.filter((link) => !excludedLinks.includes(link.id)),
});
const getPlugins = (
roles: string[],
fleetAuthzOverrides: DeepPartial<FleetAuthz> = {},
noUserAuthz: boolean = false
): StartPlugins => {
const getPlugins = (noUserAuthz: boolean = false): StartPlugins => {
return {
security: {
authc: {
getCurrentUser: noUserAuthz
? jest.fn().mockReturnValue('')
: jest.fn().mockReturnValue({ roles }),
? jest.fn().mockReturnValue(undefined)
: jest.fn().mockReturnValue([]),
},
},
fleet: {
authz: merge(createFleetAuthzMock(), fleetAuthzOverrides),
authz: createFleetAuthzMock(),
},
} as unknown as StartPlugins;
};
@ -86,15 +79,12 @@ describe('links', () => {
it('should return all links for user with all sub-feature privileges', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock());
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(links);
});
it('should not return any endpoint management link for user with all sub-feature privileges when no user authz', async () => {
const filteredLinks = await getManagementFilteredLinks(
coreMockStarted,
getPlugins([], {}, true)
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins(true));
expect(filteredLinks).toEqual(
getLinksWithout(
SecurityPageName.blocklist,
@ -113,106 +103,80 @@ describe('links', () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
canReadActionsLogManagement: false,
canDeleteHostIsolationExceptions: false,
})
);
fakeHttpServices.get.mockResolvedValue({ total: 0 });
const filteredLinks = await getManagementFilteredLinks(
coreMockStarted,
getPlugins(['superuser'])
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.responseActionsHistory));
});
});
describe('Host Isolation Exception', () => {
it('should NOT return HIE if `canReadHostIsolationExceptions` is false', async () => {
it('should return HIE if user has access permission (licensed)', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({ canReadHostIsolationExceptions: false })
getEndpointAuthzInitialStateMock({ canAccessHostIsolationExceptions: true })
);
const filteredLinks = await getManagementFilteredLinks(
coreMockStarted,
getPlugins(['superuser'])
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions));
expect(filteredLinks).toEqual(links);
expect(fakeHttpServices.get).not.toHaveBeenCalled();
});
it('should NOT return HIE if license is lower than Enterprise and NO HIE entries exist', async () => {
it('should NOT return HIE if the user has no HIE permission', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({ canReadHostIsolationExceptions: false })
getEndpointAuthzInitialStateMock({
canAccessHostIsolationExceptions: false,
canReadHostIsolationExceptions: false,
})
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions));
expect(fakeHttpServices.get).not.toHaveBeenCalled();
});
it('should NOT return HIE if user has read permission (no license) and NO HIE entries exist', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({
canAccessHostIsolationExceptions: false,
canReadHostIsolationExceptions: true,
})
);
fakeHttpServices.get.mockResolvedValue({ total: 0 });
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
const filteredLinks = await getManagementFilteredLinks(
coreMockStarted,
getPlugins([], {
packagePrivileges: {
endpoint: {
actions: {
readHostIsolationExceptions: {
executePackageAction: true,
},
},
},
},
})
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions));
expect(fakeHttpServices.get).toHaveBeenCalledWith('/api/exception_lists/items/_find', {
query: expect.objectContaining({
list_id: [ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id],
}),
});
expect(calculateEndpointAuthz as jest.Mock).toHaveBeenLastCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
false
);
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.hostIsolationExceptions));
});
it('should return HIE if license is lower than Enterprise, but HIE entries exist', async () => {
it('should return HIE if user has read permission (no license) but HIE entries exist', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(
getEndpointAuthzInitialStateMock({ canReadHostIsolationExceptions: true })
getEndpointAuthzInitialStateMock({
canAccessHostIsolationExceptions: false,
canReadHostIsolationExceptions: true,
})
);
fakeHttpServices.get.mockResolvedValue({ total: 100 });
licenseServiceMock.isPlatinumPlus.mockReturnValue(false);
const filteredLinks = await getManagementFilteredLinks(
coreMockStarted,
getPlugins([], {
packagePrivileges: {
endpoint: {
actions: {
readHostIsolationExceptions: {
executePackageAction: true,
},
},
},
},
})
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(links);
expect(fakeHttpServices.get).toHaveBeenCalledWith('/api/exception_lists/items/_find', {
query: expect.objectContaining({
list_id: [ENDPOINT_ARTIFACT_LISTS.hostIsolationExceptions.id],
}),
});
expect(calculateEndpointAuthz as jest.Mock).toHaveBeenLastCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
true
);
expect(filteredLinks).toEqual(getLinksWithout());
});
});
@ -220,7 +184,7 @@ describe('links', () => {
it('should return all links for user with all sub-feature privileges', async () => {
(calculateEndpointAuthz as jest.Mock).mockReturnValue(getEndpointAuthzInitialStateMock());
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(links);
});
@ -232,7 +196,7 @@ describe('links', () => {
})
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.trustedApps));
});
@ -244,7 +208,7 @@ describe('links', () => {
})
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.eventFilters));
});
@ -256,7 +220,7 @@ describe('links', () => {
})
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.blocklist));
});
@ -268,7 +232,7 @@ describe('links', () => {
})
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.policies));
});
@ -281,10 +245,7 @@ describe('links', () => {
canReadEndpointList: false,
})
);
const filteredLinks = await getManagementFilteredLinks(
coreMockStarted,
getPlugins(['superuser'])
);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins());
expect(filteredLinks).toEqual(getLinksWithout(SecurityPageName.endpoints));
});
});

View file

@ -8,7 +8,6 @@
import type { CoreStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { hasKibanaPrivilege } from '../../common/endpoint/service/authz/authz';
import { checkArtifactHasData } from './services/exceptions_list/check_artifact_has_data';
import {
calculateEndpointAuthz,
@ -239,35 +238,10 @@ export const getManagementFilteredLinks = async (
plugins: StartPlugins
): Promise<LinkItem> => {
const fleetAuthz = plugins.fleet?.authz;
const linksToExclude: SecurityPageName[] = [];
const currentUser = await plugins.security.authc.getCurrentUser();
const isPlatinumPlus = licenseService.isPlatinumPlus();
let hasHostIsolationExceptions: boolean = isPlatinumPlus;
// If not Platinum+ license and user has read permissions to security solution
// then check if Host Isolation Exceptions exist.
// *** IT IS IMPORTANT *** that this HTTP call only be made if the user has access to the
// Lists plugin, else non-security solution users, especially when license is not Platinum,
// may see failed HTTP requests in the browser console. This is the reason that
// `hasKibanaPrivilege()` is used below.
if (
currentUser &&
!isPlatinumPlus &&
fleetAuthz &&
hasKibanaPrivilege(
fleetAuthz,
true,
currentUser.roles.includes('superuser'),
'readHostIsolationExceptions'
)
) {
hasHostIsolationExceptions = await checkArtifactHasData(
HostIsolationExceptionsApiClient.getInstance(core.http)
);
}
const {
canReadActionsLogManagement,
canAccessHostIsolationExceptions,
canReadHostIsolationExceptions,
canReadEndpointList,
canReadTrustedApplications,
@ -276,15 +250,18 @@ export const getManagementFilteredLinks = async (
canReadPolicyManagement,
} =
fleetAuthz && currentUser
? calculateEndpointAuthz(
licenseService,
fleetAuthz,
currentUser.roles,
true,
hasHostIsolationExceptions
)
? calculateEndpointAuthz(licenseService, fleetAuthz, currentUser.roles)
: getEndpointAuthzInitialState();
const showHostIsolationExceptions =
canAccessHostIsolationExceptions || // access host isolation exceptions is a paid feature, always show the link.
// read host isolation exceptions is not a paid feature, to allow deleting exceptions after a downgrade scenario.
// however, in this situation we allow to access only when there is data, otherwise the link won't be accessible.
(canReadHostIsolationExceptions &&
(await checkArtifactHasData(HostIsolationExceptionsApiClient.getInstance(core.http))));
const linksToExclude: SecurityPageName[] = [];
if (!canReadEndpointList) {
linksToExclude.push(SecurityPageName.endpoints);
}
@ -297,7 +274,7 @@ export const getManagementFilteredLinks = async (
linksToExclude.push(SecurityPageName.responseActionsHistory);
}
if (!canReadHostIsolationExceptions) {
if (!showHostIsolationExceptions) {
linksToExclude.push(SecurityPageName.hostIsolationExceptions);
}

View file

@ -5,26 +5,28 @@
* 2.0.
*/
import { Switch, Redirect } from 'react-router-dom';
import { Switch } from 'react-router-dom';
import { Route } from '@kbn/shared-ux-router';
import React, { memo } from 'react';
import { ENDPOINTS_PATH, SecurityPageName } from '../../../../common/constants';
import { SecurityPageName } from '../../../../common/constants';
import { useLinkExists } from '../../../common/links/links';
import { MANAGEMENT_ROUTING_HOST_ISOLATION_EXCEPTIONS_PATH } from '../../common/constants';
import { NotFoundPage } from '../../../app/404';
import { HostIsolationExceptionsList } from './view/host_isolation_exceptions_list';
import { NoPrivilegesPage } from '../../../common/components/no_privileges';
/**
* Provides the routing container for the hosts related views
*/
export const HostIsolationExceptionsContainer = memo(() => {
// TODO: Probably should not silently redirect here
const canAccessHostIsolationExceptionsLink = useLinkExists(
SecurityPageName.hostIsolationExceptions
);
if (!canAccessHostIsolationExceptionsLink) {
return <Redirect to={ENDPOINTS_PATH} />;
// TODO: Render a license/productType upsell page
return (
<NoPrivilegesPage docLinkSelector={({ securitySolution }) => securitySolution.privileges} />
);
}
return (

View file

@ -15,7 +15,6 @@ import type {
FleetFromHostFileClientInterface,
} from '@kbn/fleet-plugin/server';
import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/alerting-plugin/server';
import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants';
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import {
getPackagePolicyCreateCallback,
@ -42,7 +41,6 @@ import { calculateEndpointAuthz } from '../../common/endpoint/service/authz';
import type { FeatureUsageService } from './services/feature_usage/service';
import type { ExperimentalFeatures } from '../../common/experimental_features';
import type { ActionCreateService } from './services';
import { doesArtifactHaveData } from './services';
import type { actionCreateService } from './services/actions';
export interface EndpointAppContextServiceSetupContract {
@ -162,20 +160,7 @@ export class EndpointAppContextService {
public async getEndpointAuthz(request: KibanaRequest): Promise<EndpointAuthz> {
const fleetAuthz = await this.getFleetAuthzService().fromRequest(request);
const userRoles = this.security?.authc.getCurrentUser(request)?.roles ?? [];
const isPlatinumPlus = this.getLicenseService().isPlatinumPlus();
const listClient = this.getExceptionListsClient();
const hasExceptionsListItems = !isPlatinumPlus
? await doesArtifactHaveData(listClient, ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID)
: true;
return calculateEndpointAuthz(
this.getLicenseService(),
fleetAuthz,
userRoles,
true,
hasExceptionsListItems
);
return calculateEndpointAuthz(this.getLicenseService(), fleetAuthz, userRoles);
}
public getEndpointMetadataService(): EndpointMetadataService {

View file

@ -218,10 +218,11 @@ function redirectHandler(
TypeOf<typeof NoParametersRequestSchema.body>,
SecuritySolutionRequestHandlerContext
> {
return async (_context, _req, res) => {
return async (context, _req, res) => {
const basePath = (await context.securitySolution).getServerBasePath();
return res.custom({
statusCode: 308,
headers: { location },
headers: { location: `${basePath}${location}` },
});
};
}

View file

@ -71,7 +71,7 @@ export class AppFeatures {
this.experimentalFeatures
);
const enabledSecurityAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs(
getSecurityAppFeaturesConfig()
getSecurityAppFeaturesConfig(this.experimentalFeatures)
);
this.featuresSetup.registerKibanaFeature(
this.securityFeatureConfigMerger.mergeAppFeatureConfigs(

View file

@ -124,23 +124,10 @@ export const getSecurityBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
export const getSecurityBaseKibanaSubFeatureIds = (
_: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use
): SecuritySubFeatureId[] => {
const subFeatureIds: SecuritySubFeatureId[] = [
SecuritySubFeatureId.endpointList,
SecuritySubFeatureId.trustedApplications,
SecuritySubFeatureId.hostIsolationExceptions,
SecuritySubFeatureId.blocklist,
SecuritySubFeatureId.eventFilters,
SecuritySubFeatureId.policyManagement,
SecuritySubFeatureId.responseActionsHistory,
SecuritySubFeatureId.hostIsolation,
SecuritySubFeatureId.processOperations,
SecuritySubFeatureId.fileOperations,
SecuritySubFeatureId.executeAction,
];
return subFeatureIds;
};
): SecuritySubFeatureId[] => [
SecuritySubFeatureId.hostIsolationExceptions,
SecuritySubFeatureId.hostIsolation,
];
/**
* Maps the AppFeatures keys to Kibana privileges that will be merged
@ -151,7 +138,9 @@ export const getSecurityBaseKibanaSubFeatureIds = (
* - `subFeatureIds`: the ids of the sub-features that will be added into the Security subFeatures entry.
* - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified.
*/
export const getSecurityAppFeaturesConfig = (): AppFeaturesSecurityConfig => {
export const getSecurityAppFeaturesConfig = (
_: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use
): AppFeaturesSecurityConfig => {
return {
[AppFeatureSecurityKey.advancedInsights]: {
privileges: {
@ -165,5 +154,46 @@ export const getSecurityAppFeaturesConfig = (): AppFeaturesSecurityConfig => {
},
},
},
[AppFeatureSecurityKey.endpointResponseActions]: {
subFeatureIds: [
SecuritySubFeatureId.processOperations,
SecuritySubFeatureId.fileOperations,
SecuritySubFeatureId.executeAction,
],
subFeaturesPrivileges: [
{
id: 'host_isolation_all',
api: [`${APP_ID}-writeHostIsolation`],
ui: ['writeHostIsolation'],
},
],
},
[AppFeatureSecurityKey.endpointExceptions]: {
subFeatureIds: [
SecuritySubFeatureId.trustedApplications,
SecuritySubFeatureId.blocklist,
SecuritySubFeatureId.eventFilters,
SecuritySubFeatureId.policyManagement,
SecuritySubFeatureId.endpointList,
SecuritySubFeatureId.responseActionsHistory,
],
subFeaturesPrivileges: [
{
id: 'host_isolation_exceptions_all',
api: [
`${APP_ID}-accessHostIsolationExceptions`,
`${APP_ID}-writeHostIsolationExceptions`,
],
ui: ['accessHostIsolationExceptions', 'writeHostIsolationExceptions'],
},
{
id: 'host_isolation_exceptions_read',
api: [`${APP_ID}-accessHostIsolationExceptions`],
ui: ['accessHostIsolationExceptions'],
},
],
},
};
};

View file

@ -142,7 +142,7 @@ const hostIsolationExceptionsSubFeature: SubFeatureConfig = {
'lists-all',
'lists-read',
'lists-summary',
`${APP_ID}-writeHostIsolationExceptions`,
`${APP_ID}-deleteHostIsolationExceptions`,
`${APP_ID}-readHostIsolationExceptions`,
],
id: 'host_isolation_exceptions_all',
@ -152,7 +152,7 @@ const hostIsolationExceptionsSubFeature: SubFeatureConfig = {
all: [EXCEPTION_LIST_NAMESPACE_AGNOSTIC],
read: [],
},
ui: ['writeHostIsolationExceptions', 'readHostIsolationExceptions'],
ui: ['readHostIsolationExceptions', 'deleteHostIsolationExceptions'],
},
{
api: ['lists-read', 'lists-summary', `${APP_ID}-readHostIsolationExceptions`],
@ -396,7 +396,7 @@ const hostIsolationSubFeature: SubFeatureConfig = {
groupType: 'mutually_exclusive',
privileges: [
{
api: [`${APP_ID}-writeHostIsolation`],
api: [`${APP_ID}-writeHostIsolationRelease`],
id: 'host_isolation_all',
includeIn: 'none',
name: 'All',
@ -404,7 +404,7 @@ const hostIsolationSubFeature: SubFeatureConfig = {
all: [],
read: [],
},
ui: ['writeHostIsolation'],
ui: ['writeHostIsolationRelease'],
},
],
},

View file

@ -109,6 +109,7 @@ const createSecuritySolutionRequestContextMock = (
return {
core,
getServerBasePath: jest.fn(() => ''),
getEndpointAuthz: jest.fn(async () =>
getEndpointAuthzInitialStateMock(overrides.endpointAuthz)
),

View file

@ -77,6 +77,8 @@ export class RequestContextFactory implements IRequestContextFactory {
return {
core: coreContext,
getServerBasePath: () => core.http.basePath.serverBasePath,
getEndpointAuthz: async (): Promise<Immutable<EndpointAuthz>> => {
if (!endpointAuthz) {
// eslint-disable-next-line require-atomic-updates

View file

@ -31,6 +31,7 @@ export { AppClient };
export interface SecuritySolutionApiRequestHandlerContext {
core: CoreRequestHandlerContext;
getServerBasePath: () => string;
getEndpointAuthz: () => Promise<Immutable<EndpointAuthz>>;
getConfig: () => ConfigType;
getFrameworkRequest: () => FrameworkRequest;

View file

@ -18,8 +18,8 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
complete: [AppFeatureKey.advancedInsights, AppFeatureKey.casesConnectors],
},
endpoint: {
essentials: [],
complete: [],
essentials: [AppFeatureKey.endpointExceptions],
complete: [AppFeatureKey.endpointResponseActions],
},
cloud: {
essentials: [],