mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:  - endpointResponseActions:  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.  ### 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.  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:
parent
ab18045968
commit
352d7c9ea7
23 changed files with 408 additions and 686 deletions
|
@ -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
|
||||
|
|
|
@ -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: '-',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -50,6 +50,7 @@ describe('When using the release action from response actions console', () => {
|
|||
endpointCapabilities: [...capabilities],
|
||||
endpointPrivileges: {
|
||||
...getEndpointAuthzInitialState(),
|
||||
canUnIsolateHost: true,
|
||||
loading: false,
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}` },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ export class AppFeatures {
|
|||
this.experimentalFeatures
|
||||
);
|
||||
const enabledSecurityAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs(
|
||||
getSecurityAppFeaturesConfig()
|
||||
getSecurityAppFeaturesConfig(this.experimentalFeatures)
|
||||
);
|
||||
this.featuresSetup.registerKibanaFeature(
|
||||
this.securityFeatureConfigMerger.mergeAppFeatureConfigs(
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -109,6 +109,7 @@ const createSecuritySolutionRequestContextMock = (
|
|||
|
||||
return {
|
||||
core,
|
||||
getServerBasePath: jest.fn(() => ''),
|
||||
getEndpointAuthz: jest.fn(async () =>
|
||||
getEndpointAuthzInitialStateMock(overrides.endpointAuthz)
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,6 +31,7 @@ export { AppClient };
|
|||
|
||||
export interface SecuritySolutionApiRequestHandlerContext {
|
||||
core: CoreRequestHandlerContext;
|
||||
getServerBasePath: () => string;
|
||||
getEndpointAuthz: () => Promise<Immutable<EndpointAuthz>>;
|
||||
getConfig: () => ConfigType;
|
||||
getFrameworkRequest: () => FrameworkRequest;
|
||||
|
|
|
@ -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: [],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue