Skip to content

Commit

Permalink
[Security Solution] Prevent superuser access PLI gated APIs (elastic#…
Browse files Browse the repository at this point in the history
…176165)

## Summary

This PR solves an issue with `superuser` (or any `*`) role and PLI
(product level item) control.

Elasticsearch _has_privileges_ API always returns _true_ on any
privilege for `superuser` role, even if the privilege has never been
registered (more context
[here](elastic/elasticsearch#33928 (comment))),
causing superuser to be able to access product-restricted APIs (e.g.
Routes that should only be available on _complete_ tier, are also
available on _essentials_ tier).

## Solution

We have the registered AppFeatures configuration locally, so we can
solve the problem by checking that the action privilege exists and has
been registered in the AppFeatures service, before doing any call to ES
_hasPrivileges_ API for RBAC.

### Changes

- AppFeatures service now stores a Set with all the (`api` and `ui`)
actions registered.
- Endpoint authz checks the actions against AppFeatures before checking
RBAC. Only for server-side.
- Route `access:` tag control has been extended to check actions against
AppFeatures for _securitySolution_ prefixed actions.
- New `securitySolutionAppFeature:` route tag control for non-RBAC
product feature checks. (This is not being used yet, but it will be
needed)

### Behavior change

- UI: no change, everything should keep working the same way.
- API: routes associated with higher product tier features (such as
endpoint or entity analytics) won't be accessible for the
superuser/admin role when running on lower product tiers, like _security
essentials_.

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
semd and kibanamachine authored Feb 15, 2024
1 parent 54634d7 commit 858347a
Show file tree
Hide file tree
Showing 21 changed files with 1,061 additions and 331 deletions.
6 changes: 4 additions & 2 deletions .buildkite/ftr_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -527,10 +527,12 @@ enabled:
- x-pack/test/security_solution_api_integration/test_suites/detections_response/telemetry/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/user_roles/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/detections_response/user_roles/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/genai/invoke_ai/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/genai/invoke_ai/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/basic_license_essentials_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/basic_license_essentials_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/exception_lists_items/trial_license_complete_tier/configs/ess.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/exception_lists_items/trial_license_complete_tier/configs/serverless.config.ts
- x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/lists_items/trial_license_complete_tier/configs/ess.config.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { ENDPOINT_PRIVILEGES, FleetAuthz } from '@kbn/fleet-plugin/common';

import { omit } from 'lodash';
import type { AppFeaturesService } from '../../../../server/lib/app_features_service';
import { RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ } from '../response_actions/constants';
import type { LicenseService } from '../../../license';
import type { EndpointAuthz } from '../../types/authz';
Expand All @@ -21,20 +22,28 @@ import type { MaybeImmutable } from '../../types';
* level, use `calculateEndpointAuthz()`
*
* @param fleetAuthz
* @param privilege
* @param appFeatureService
*/
export function hasKibanaPrivilege(
fleetAuthz: FleetAuthz,
privilege: keyof typeof ENDPOINT_PRIVILEGES
): boolean {
return fleetAuthz.packagePrivileges?.endpoint?.actions[privilege].executePackageAction ?? false;
}

export function hasEndpointExceptionsPrivilege(
fleetAuthz: FleetAuthz,
privilege: 'showEndpointExceptions' | 'crudEndpointExceptions'
): boolean {
return fleetAuthz.endpointExceptionsPrivileges?.actions[privilege] ?? false;
function hasAuthFactory(fleetAuthz: FleetAuthz, appFeatureService?: AppFeaturesService) {
return function hasAuth(
privilege: keyof typeof ENDPOINT_PRIVILEGES,
{ action }: { action?: string } = {}
): boolean {
// Product features control
if (appFeatureService) {
// Only server side has to check this, to prevent "superuser" role from being allowed to use product gated APIs.
// UI side does not need to check this. Capabilities list is correct for superuser.
const actionToCheck = action ?? appFeatureService.getApiActionName(privilege);
if (!appFeatureService.isActionRegistered(actionToCheck)) {
return false;
}
}
// Role access control
if (privilege === 'showEndpointExceptions' || privilege === 'crudEndpointExceptions') {
return fleetAuthz.endpointExceptionsPrivileges?.actions[privilege] ?? false;
}
return fleetAuthz.packagePrivileges?.endpoint?.actions[privilege].executePackageAction ?? false;
};
}

/**
Expand All @@ -48,57 +57,42 @@ export function hasEndpointExceptionsPrivilege(
export const calculateEndpointAuthz = (
licenseService: LicenseService,
fleetAuthz: FleetAuthz,
userRoles: MaybeImmutable<string[]> = []
userRoles: MaybeImmutable<string[]> = [],
appFeaturesService?: AppFeaturesService // only exists on the server side
): EndpointAuthz => {
const hasAuth = hasAuthFactory(fleetAuthz, appFeaturesService);

const isPlatinumPlusLicense = licenseService.isPlatinumPlus();
const isEnterpriseLicense = licenseService.isEnterprise();
const hasEndpointManagementAccess = userRoles.includes('superuser');

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,
'writeHostIsolationExceptions'
);
const canReadHostIsolationExceptions = hasKibanaPrivilege(
fleetAuthz,
'readHostIsolationExceptions'
);
const canAccessHostIsolationExceptions = hasKibanaPrivilege(
fleetAuthz,
'accessHostIsolationExceptions'
);
const canDeleteHostIsolationExceptions = hasKibanaPrivilege(
fleetAuthz,
'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 canWriteSecuritySolution = hasAuth('writeSecuritySolution', { action: 'ui:crud' });
const canReadSecuritySolution = hasAuth('readSecuritySolution', { action: 'ui:show' });
const canWriteEndpointList = hasAuth('writeEndpointList');
const canReadEndpointList = hasAuth('readEndpointList');
const canWritePolicyManagement = hasAuth('writePolicyManagement');
const canReadPolicyManagement = hasAuth('readPolicyManagement');
const canWriteActionsLogManagement = hasAuth('writeActionsLogManagement');
const canReadActionsLogManagement = hasAuth('readActionsLogManagement');
const canIsolateHost = hasAuth('writeHostIsolation');
const canUnIsolateHost = hasAuth('writeHostIsolationRelease');
const canWriteProcessOperations = hasAuth('writeProcessOperations');
const canWriteTrustedApplications = hasAuth('writeTrustedApplications');
const canReadTrustedApplications = hasAuth('readTrustedApplications');
const canWriteHostIsolationExceptions = hasAuth('writeHostIsolationExceptions');
const canReadHostIsolationExceptions = hasAuth('readHostIsolationExceptions');
const canAccessHostIsolationExceptions = hasAuth('accessHostIsolationExceptions');
const canDeleteHostIsolationExceptions = hasAuth('deleteHostIsolationExceptions');
const canWriteBlocklist = hasAuth('writeBlocklist');
const canReadBlocklist = hasAuth('readBlocklist');
const canWriteEventFilters = hasAuth('writeEventFilters');
const canReadEventFilters = hasAuth('readEventFilters');
const canWriteFileOperations = hasAuth('writeFileOperations');

const canWriteExecuteOperations = hasKibanaPrivilege(fleetAuthz, 'writeExecuteOperations');
const canWriteExecuteOperations = hasAuth('writeExecuteOperations');

const canReadEndpointExceptions = hasEndpointExceptionsPrivilege(
fleetAuthz,
'showEndpointExceptions'
);
const canWriteEndpointExceptions = hasEndpointExceptionsPrivilege(
fleetAuthz,
'crudEndpointExceptions'
);
const canReadEndpointExceptions = hasAuth('showEndpointExceptions');
const canWriteEndpointExceptions = hasAuth('crudEndpointExceptions');

const authz: EndpointAuthz = {
canWriteSecuritySolution,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,48 +20,75 @@ describe(
},
},
() => {
const allPages = getEndpointManagementPageList();
const deniedPages = allPages.filter(({ id }) => {
return id !== 'endpointList' && id !== 'policyList';
});
const allowedPages = allPages.filter(({ id }) => {
return id === 'endpointList' || id === 'policyList';
});
let username: string;
let password: string;

beforeEach(() => {
login(ROLE.endpoint_operations_analyst).then((response) => {
username = response.username;
password = response.password;
describe('Endpoint Operations Analyst', () => {
const allPages = getEndpointManagementPageList();
const deniedPages = allPages.filter(({ id }) => {
return id !== 'endpointList' && id !== 'policyList';
});
});
const allowedPages = allPages.filter(({ id }) => {
return id === 'endpointList' || id === 'policyList';
});
let username: string;
let password: string;

for (const { url, title, pageTestSubj } of allowedPages) {
it(`should allow access to ${title}`, () => {
cy.visit(url);
cy.getByTestSubj(pageTestSubj).should('exist');
beforeEach(() => {
login(ROLE.endpoint_operations_analyst).then((response) => {
username = response.username;
password = response.password;
});
});
}

for (const { url, title } of deniedPages) {
it(`should not allow access to ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('exist');
for (const { url, title, pageTestSubj } of allowedPages) {
it(`should allow access to ${title}`, () => {
cy.visit(url);
cy.getByTestSubj(pageTestSubj).should('exist');
});
}

for (const { url, title } of deniedPages) {
it(`should not allow access to ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('exist');
});
}

// No access to response actions (except `unisolate`)
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter(
(apiName) => apiName !== 'unisolate'
)) {
it(`should not allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('none', actionName, username, password);
});
}

it('should have access to `unisolate` api', () => {
ensureResponseActionAuthzAccess('all', 'unisolate', username, password);
});
}
});

// No access to response actions (except `unisolate`)
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter(
(apiName) => apiName !== 'unisolate'
)) {
it(`should not allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('none', actionName, username, password);
describe('Elastic superuser', () => {
let username: string;
let password: string;

beforeEach(() => {
login(ROLE.elastic_serverless).then((response) => {
username = response.username;
password = response.password;
});
});
}

it('should have access to `unisolate` api', () => {
ensureResponseActionAuthzAccess('all', 'unisolate', username, password);
// No access to response actions (except `unisolate`)
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES.filter(
(apiName) => apiName !== 'unisolate'
)) {
it(`should not allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('none', actionName, username, password);
});
}

it('should have access to `unisolate` api', () => {
ensureResponseActionAuthzAccess('all', 'unisolate', username, password);
});
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,58 @@ describe(
},
},
() => {
const allPages = getEndpointManagementPageList();
let username: string;
let password: string;

beforeEach(() => {
login(ROLE.endpoint_operations_analyst).then((response) => {
username = response.username;
password = response.password;
describe('Endpoint Operations Analyst', () => {
const allPages = getEndpointManagementPageList();
let username: string;
let password: string;

beforeEach(() => {
login(ROLE.endpoint_operations_analyst).then((response) => {
username = response.username;
password = response.password;
});
});
});

for (const { url, title, pageTestSubj } of allPages) {
it(`should allow access to ${title}`, () => {
cy.visit(url);
cy.getByTestSubj(pageTestSubj).should('exist');
for (const { url, title, pageTestSubj } of allPages) {
it(`should allow access to ${title}`, () => {
cy.visit(url);
cy.getByTestSubj(pageTestSubj).should('exist');
});
}

for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
it(`should allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('all', actionName, username, password);
});
}

it(`should have access to Fleet`, () => {
visitFleetAgentList();
getFleetAgentListTable().should('exist');
});
}
});

describe('Elastic superuser', () => {
let username: string;
let password: string;

for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
it(`should allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('all', actionName, username, password);
beforeEach(() => {
login(ROLE.elastic_serverless).then((response) => {
username = response.username;
password = response.password;
});
});
}

it(`should have access to Fleet`, () => {
visitFleetAgentList();
getFleetAgentListTable().should('exist');
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
it(`should allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('all', actionName, username, password);
});
}

it(`should have access to Fleet`, () => {
visitFleetAgentList();
getFleetAgentListTable().should('exist');
});
});
}
);
Loading

0 comments on commit 858347a

Please sign in to comment.