[8.12] [ResponseOps] Fix Actions authz for SentinelOne to ensure that the user explicitly has ALL privilege (#172528) (#173089)

# Backport

This will backport the following commits from `main` to `8.12`:
- [[ResponseOps] Fix Actions authz for SentinelOne to ensure that the
user explicitly has `ALL` privilege
(#172528)](https://github.com/elastic/kibana/pull/172528)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Paul
Tavares","email":"56442535+paul-tavares@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-12-11T17:31:25Z","message":"[ResponseOps]
Fix Actions authz for SentinelOne to ensure that the user explicitly has
`ALL` privilege (#172528)\n\n## Summary\r\n\r\n- Fixes the Actions
plugin sub-actions execution for SentinelOne\r\nconnector to ensure that
a user must have `ALL` privilege to \"Action
and\r\nConnectors\"","sha":"88ea8498b417d0b0f22364b434d57764ad958030","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Defend
Workflows","v8.12.0","v8.13.0"],"number":172528,"url":"https://github.com/elastic/kibana/pull/172528","mergeCommit":{"message":"[ResponseOps]
Fix Actions authz for SentinelOne to ensure that the user explicitly has
`ALL` privilege (#172528)\n\n## Summary\r\n\r\n- Fixes the Actions
plugin sub-actions execution for SentinelOne\r\nconnector to ensure that
a user must have `ALL` privilege to \"Action
and\r\nConnectors\"","sha":"88ea8498b417d0b0f22364b434d57764ad958030"}},"sourceBranch":"main","suggestedTargetBranches":["8.12"],"targetPullRequestStates":[{"branch":"8.12","label":"v8.12.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/172528","number":172528,"mergeCommit":{"message":"[ResponseOps]
Fix Actions authz for SentinelOne to ensure that the user explicitly has
`ALL` privilege (#172528)\n\n## Summary\r\n\r\n- Fixes the Actions
plugin sub-actions execution for SentinelOne\r\nconnector to ensure that
a user must have `ALL` privilege to \"Action
and\r\nConnectors\"","sha":"88ea8498b417d0b0f22364b434d57764ad958030"}}]}]
BACKPORT-->

Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2023-12-11 13:52:48 -05:00 committed by GitHub
parent f578d469a2
commit fc3de170dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 483 additions and 29 deletions

View file

@ -43,7 +43,7 @@ import { actionsAuthorizationMock } from '../authorization/actions_authorization
import { trackLegacyRBACExemption } from '../lib/track_legacy_rbac_exemption';
import { ConnectorTokenClient } from '../lib/connector_token_client';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { Logger } from '@kbn/core/server';
import { Logger, SavedObject } from '@kbn/core/server';
import { connectorTokenClientMock } from '../lib/connector_token_client.mock';
import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock';
import { getOAuthJwtAccessToken } from '../lib/get_oauth_jwt_access_token';
@ -120,6 +120,14 @@ const executor: ExecutorType<{}, {}, {}, void> = async (options) => {
const connectorTokenClient = connectorTokenClientMock.create();
const inMemoryMetrics = inMemoryMetricsMock.create();
const actionTypeIdFromSavedObjectMock = (actionTypeId: string = 'my-action-type') => {
return {
attributes: {
actionTypeId,
},
} as SavedObject;
};
beforeEach(() => {
jest.resetAllMocks();
mockedLicenseState = licenseStateMock.create();
@ -2664,6 +2672,7 @@ describe('execute()', () => {
(getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => {
return AuthorizationMode.RBAC;
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock());
await actionsClient.execute({
actionId: 'action-id',
params: {
@ -2672,6 +2681,7 @@ describe('execute()', () => {
source: asHttpRequestExecutionSource(request),
});
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
actionTypeId: 'my-action-type',
operation: 'execute',
additionalPrivileges: [],
});
@ -2685,6 +2695,8 @@ describe('execute()', () => {
new Error(`Unauthorized to execute all actions`)
);
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock());
await expect(
actionsClient.execute({
actionId: 'action-id',
@ -2696,6 +2708,7 @@ describe('execute()', () => {
).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`);
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
actionTypeId: 'my-action-type',
operation: 'execute',
additionalPrivileges: [],
});
@ -2768,12 +2781,15 @@ describe('execute()', () => {
executor,
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock());
await actionsClient.execute({
actionId: 'system-connector-.cases',
params: {},
});
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
actionTypeId: 'my-action-type',
operation: 'execute',
additionalPrivileges: ['test/create'],
});
@ -2832,12 +2848,15 @@ describe('execute()', () => {
executor,
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock());
await actionsClient.execute({
actionId: 'testPreconfigured',
params: {},
});
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
actionTypeId: 'my-action-type',
operation: 'execute',
additionalPrivileges: [],
});
@ -2895,12 +2914,15 @@ describe('execute()', () => {
executor,
});
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(actionTypeIdFromSavedObjectMock());
await actionsClient.execute({
actionId: 'system-connector-.cases',
params: { foo: 'bar' },
});
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
actionTypeId: 'my-action-type',
operation: 'execute',
additionalPrivileges: ['test/create'],
});
@ -3032,6 +3054,7 @@ describe('bulkEnqueueExecution()', () => {
},
]);
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
actionTypeId: 'my-action-type',
operation: 'execute',
});
});

View file

@ -11,7 +11,7 @@ import url from 'url';
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
import { i18n } from '@kbn/i18n';
import { omitBy, isUndefined, compact } from 'lodash';
import { omitBy, isUndefined, compact, uniq } from 'lodash';
import {
IScopedClusterClient,
SavedObjectsClientContract,
@ -681,14 +681,39 @@ export class ActionsClient {
}: Omit<ExecuteOptions, 'request' | 'actionExecutionId'>): Promise<
ActionTypeExecutorResult<unknown>
> {
const log = this.context.logger;
if (
(await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) ===
AuthorizationMode.RBAC
) {
const additionalPrivileges = this.getSystemActionKibanaPrivileges(actionId, params);
let actionTypeId: string | undefined;
try {
if (this.isPreconfigured(actionId)) {
const connector = this.context.inMemoryConnectors.find(
(inMemoryConnector) => inMemoryConnector.id === actionId
);
actionTypeId = connector?.actionTypeId;
} else {
// TODO: Optimize so we don't do another get on top of getAuthorizationModeBySource and within the actionExecutor.execute
const { attributes } = await this.context.unsecuredSavedObjectsClient.get<RawAction>(
'action',
actionId
);
actionTypeId = attributes.actionTypeId;
}
} catch (err) {
log.debug(`Failed to retrieve actionTypeId for action [${actionId}]`, err);
}
await this.context.authorization.ensureAuthorized({
operation: 'execute',
additionalPrivileges,
actionTypeId,
});
} else {
trackLegacyRBACExemption('execute', this.context.usageCounter);
@ -723,6 +748,11 @@ export class ActionsClient {
* inside the ActionExecutor at execution time
*/
await this.context.authorization.ensureAuthorized({ operation: 'execute' });
await Promise.all(
uniq(options.map((o) => o.actionTypeId)).map((actionTypeId) =>
this.context.authorization.ensureAuthorized({ operation: 'execute', actionTypeId })
)
);
}
if (authModes[AuthorizationMode.Legacy] > 0) {
trackLegacyRBACExemption(
@ -740,7 +770,10 @@ export class ActionsClient {
(await getAuthorizationModeBySource(this.context.unsecuredSavedObjectsClient, source)) ===
AuthorizationMode.RBAC
) {
await this.context.authorization.ensureAuthorized({ operation: 'execute' });
await this.context.authorization.ensureAuthorized({
operation: 'execute',
actionTypeId: options.actionTypeId,
});
} else {
trackLegacyRBACExemption('ephemeralEnqueuedExecution', this.context.usageCounter);
}

View file

@ -14,10 +14,16 @@ import {
} from '../constants/saved_objects';
import { AuthenticatedUser } from '@kbn/security-plugin/server';
import { AuthorizationMode } from './get_authorization_mode_by_source';
import {
CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG,
CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG,
} from '../feature';
const request = {} as KibanaRequest;
const mockAuthorizationAction = (type: string, operation: string) => `${type}/${operation}`;
const BASIC_EXECUTE_AUTHZ = `api:${CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG}`;
const ADVANCED_EXECUTE_AUTHZ = `api:${CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG}`;
function mockSecurity() {
const security = securityMock.createSetup();
@ -83,7 +89,7 @@ describe('ensureAuthorized', () => {
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create');
expect(checkPrivileges).toHaveBeenCalledWith({
kibana: [mockAuthorizationAction('action', 'create')],
kibana: [mockAuthorizationAction('action', 'create'), BASIC_EXECUTE_AUTHZ],
});
});
@ -123,6 +129,7 @@ describe('ensureAuthorized', () => {
kibana: [
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
BASIC_EXECUTE_AUTHZ,
],
});
});
@ -225,6 +232,54 @@ describe('ensureAuthorized', () => {
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
'test/create',
BASIC_EXECUTE_AUTHZ,
],
});
});
test('checks SentinelOne connector privileges correctly', async () => {
const { authorization } = mockSecurity();
const checkPrivileges: jest.MockedFunction<
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
> = jest.fn();
authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges);
const actionsAuthorization = new ActionsAuthorization({
request,
authorization,
});
checkPrivileges.mockResolvedValueOnce({
username: 'some-user',
hasAllRequested: true,
privileges: [
{
privilege: mockAuthorizationAction('myType', 'execute'),
authorized: true,
},
],
});
await actionsAuthorization.ensureAuthorized({
operation: 'execute',
actionTypeId: '.sentinelone',
});
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
ACTION_SAVED_OBJECT_TYPE,
'get'
);
expect(authorization.actions.savedObject.get).toHaveBeenCalledWith(
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
'create'
);
expect(checkPrivileges).toHaveBeenCalledWith({
kibana: [
mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'),
mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'),
ADVANCED_EXECUTE_AUTHZ,
],
});
});

View file

@ -74,7 +74,15 @@ export class ActionsAuthorization {
: [authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation)];
const { hasAllRequested } = await checkPrivileges({
kibana: [...privileges, ...additionalPrivileges],
kibana: [
...privileges,
...additionalPrivileges,
// SentinelOne sub-actions require that a user have `all` privilege to Actions and Connectors.
// This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions
actionTypeId === '.sentinelone'
? 'api:actions:execute-advanced-connectors'
: 'api:actions:execute-basic-connectors',
],
});
if (!hasAllRequested) {
throw Boom.forbidden(

View file

@ -13,6 +13,9 @@ import {
CONNECTOR_TOKEN_SAVED_OBJECT_TYPE,
} from './constants/saved_objects';
export const CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG = 'actions:execute-advanced-connectors';
export const CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG = 'actions:execute-basic-connectors';
/**
* The order of appearance in the feature privilege page
* under the management section.
@ -33,7 +36,10 @@ export const ACTIONS_FEATURE = {
privileges: {
all: {
app: [],
api: [],
api: [
CONNECTORS_ADVANCED_EXECUTE_PRIVILEGE_API_TAG,
CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG,
],
catalogue: [],
management: {
insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'],
@ -50,7 +56,7 @@ export const ACTIONS_FEATURE = {
},
read: {
app: [],
api: [],
api: [CONNECTORS_BASIC_EXECUTE_PRIVILEGE_API_TAG],
catalogue: [],
management: {
insightsAndAlerting: ['triggersActions', 'triggersActionsConnectors'],

View file

@ -835,6 +835,7 @@ test('successfully authorize system actions', async () => {
await actionExecutor.execute({ ...executeParams, actionId: 'system-connector-.cases' });
expect(authorizationMock.ensureAuthorized).toBeCalledWith({
actionTypeId: '.cases',
operation: 'execute',
additionalPrivileges: ['test/create'],
});
@ -875,7 +876,10 @@ test('Execute of SentinelOne sub-actions require create privilege', async () =>
await actionExecutor.execute({ ...executeParams, actionId: 'sentinel-one-connector-authz' });
expect(authorizationMock.ensureAuthorized).toHaveBeenCalledWith({ operation: 'create' });
expect(authorizationMock.ensureAuthorized).toHaveBeenCalledWith({
operation: 'execute',
actionTypeId: '.sentinelone',
});
});
test('pass the params to the actionTypeRegistry when authorizing system actions', async () => {
@ -909,6 +913,7 @@ test('pass the params to the actionTypeRegistry when authorizing system actions'
});
expect(authorizationMock.ensureAuthorized).toBeCalledWith({
actionTypeId: '.cases',
operation: 'execute',
additionalPrivileges: ['test/create'],
});

View file

@ -565,14 +565,17 @@ const ensureAuthorizedToExecute = async ({
params
);
await authorization.ensureAuthorized({ operation: 'execute', additionalPrivileges });
}
// SentinelOne sub-actions require that a user have `all` privilege to Actions and Connectors.
// This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions
if (actionTypeId === '.sentinelone') {
await authorization.ensureAuthorized({
operation: 'create',
operation: 'execute',
additionalPrivileges,
actionTypeId,
});
} else if (actionTypeId === '.sentinelone') {
// SentinelOne sub-actions require that a user have `all` privilege to Actions and Connectors.
// This is a temporary solution until a more robust RBAC approach can be implemented for sub-actions
await authorization.ensureAuthorized({
operation: 'execute',
actionTypeId,
});
}
} catch (error) {

View file

@ -16,7 +16,7 @@ export const allowedExperimentalValues = Object.freeze({
sentinelOneConnectorOn: false,
});
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
export type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
type Mutable<T> = { -readonly [P in keyof T]: T[P] };
const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<ExperimentalConfigKeys>;

View file

@ -10,6 +10,8 @@ import getPort from 'get-port';
import { CA_CERT_PATH } from '@kbn/dev-utils';
import { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test';
import { getAllExternalServiceSimulatorPaths } from '@kbn/actions-simulators-plugin/server/plugin';
import { ExperimentalConfigKeys } from '@kbn/stack-connectors-plugin/common/experimental_features';
import { SENTINELONE_CONNECTOR_ID } from '@kbn/stack-connectors-plugin/common/sentinelone/constants';
import { services } from './services';
import { getTlsWebhookServerUrls } from './lib/get_tls_webhook_servers';
@ -29,6 +31,7 @@ interface CreateTestConfigOptions {
useDedicatedTaskRunner: boolean;
enableFooterInEmail?: boolean;
maxScheduledPerMinute?: number;
experimentalFeatures?: ExperimentalConfigKeys;
}
// test.not-enabled is specifically not enabled
@ -48,6 +51,7 @@ const enabledActionTypes = [
'.resilient',
'.gen-ai',
'.d3security',
SENTINELONE_CONNECTOR_ID,
'.slack',
'.slack_api',
'.tines',
@ -85,6 +89,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
useDedicatedTaskRunner,
enableFooterInEmail = true,
maxScheduledPerMinute,
experimentalFeatures = [],
} = options;
return async ({ readConfigFile }: FtrConfigProviderContext) => {
@ -344,6 +349,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
'--xpack.task_manager.allow_reading_invalid_state=false',
'--xpack.task_manager.requeue_invalid_tasks.enabled=true',
'--xpack.actions.queued.max=500',
`--xpack.stack_connectors.enableExperimental=${JSON.stringify(experimentalFeatures)}`,
],
},
};

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import SuperTest from 'supertest';
import { ToolingLog } from '@kbn/tooling-log';
export interface LogErrorDetailsInterface {
(this: SuperTest.Test, err: Error & { response?: any }): SuperTest.Test;
ignoreCodes: (
codes: number[]
) => (this: SuperTest.Test, err: Error & { response?: SuperTest.Response }) => SuperTest.Test;
}
/**
* Creates a logger that can be used with `supertest` to log details around errors
*
* @param log
*
* @example
* const errorLogger = createSupertestErrorLogger(log);
* supertestWithoutAuth
* .post(`some/url`)
* .on('error', errorLogger) //<< Add logger to `error` event
* .send({})
*/
export const createSupertestErrorLogger = (log: ToolingLog): LogErrorDetailsInterface => {
/**
* Utility for use with `supertest` that logs errors with details returned by the API
* @param err
*/
const logErrorDetails: LogErrorDetailsInterface = function (err) {
if (err.response && (err.response.body || err.response.text)) {
let outputData =
'RESPONSE:\n' + err.response.body
? JSON.stringify(err.response.body, null, 2)
: err.response.text;
if (err.response.request) {
const { url = '', method = '', _data = '' } = err.response.request;
outputData += `\nREQUEST:
${method} ${url}
${JSON.stringify(_data, null, 2)}
`;
}
log.error(outputData);
}
return this ?? err;
};
logErrorDetails.ignoreCodes = (codes) => {
return function (err) {
if (err.response && err.response.status && !codes.includes(err.response.status)) {
return logErrorDetails.call(this, err);
}
return this;
};
};
return logErrorDetails;
};

View file

@ -16,4 +16,5 @@ export default createTestConfig('security_and_spaces', {
publicBaseUrl: true,
testFiles: [require.resolve('./tests')],
useDedicatedTaskRunner: true,
experimentalFeatures: ['sentinelOneConnectorOn'],
});

View file

@ -16,4 +16,5 @@ export default createTestConfig('security_and_spaces', {
publicBaseUrl: true,
testFiles: [require.resolve('./tests')],
useDedicatedTaskRunner: false,
experimentalFeatures: ['sentinelOneConnectorOn'],
});

View file

@ -133,7 +133,8 @@ export default function ({ getService }: FtrProviderContext) {
connectorId,
outcome: 'failure',
message: `action execution failure: test.system-action-kibana-privileges:${connectorId}: ${name}`,
errorMessage: 'Unauthorized to execute actions',
errorMessage:
'Unauthorized to execute a "test.system-action-kibana-privileges" action',
startDate,
});
break;

View file

@ -16,4 +16,5 @@ export default createTestConfig('security_and_spaces', {
publicBaseUrl: true,
testFiles: [require.resolve('.')],
useDedicatedTaskRunner: true,
experimentalFeatures: ['sentinelOneConnectorOn'],
});

View file

@ -0,0 +1,240 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
SENTINELONE_CONNECTOR_ID,
SUB_ACTION,
} from '@kbn/stack-connectors-plugin/common/sentinelone/constants';
import { FeaturesPrivileges, Role } from '@kbn/security-plugin/common';
import SuperTest from 'supertest';
import expect from '@kbn/expect';
import { getUrlPrefix } from '../../../../../common/lib';
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { createSupertestErrorLogger } from '../../../../../common/lib/log_supertest_errors';
// eslint-disable-next-line import/no-default-export
export default function createSentinelOneTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const securityService = getService('security');
const log = getService('log');
const logErrorDetails = createSupertestErrorLogger(log);
describe('SentinelOne', () => {
describe('sub-actions authz', () => {
interface CreatedUser {
username: string;
password: string;
deleteUser: () => Promise<void>;
}
// SentinelOne supported sub-actions
const s1SubActions = [
SUB_ACTION.KILL_PROCESS,
SUB_ACTION.GET_AGENTS,
SUB_ACTION.ISOLATE_HOST,
SUB_ACTION.RELEASE_HOST,
SUB_ACTION.GET_REMOTE_SCRIPT_STATUS,
SUB_ACTION.GET_REMOTE_SCRIPT_RESULTS,
];
let connectorId: string;
const createUser = async ({
username,
password = 'changeme',
kibanaFeatures = { actions: ['all'] },
}: {
username: string;
password?: string;
kibanaFeatures?: FeaturesPrivileges;
}): Promise<CreatedUser> => {
const role: Role = {
name: username,
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
base: [],
feature: {
// Important: Saved Objects Managemnt should be set to `all` to ensure that authz
// is not defaulted to the check done against SO's for SentinelOne
savedObjectsManagement: ['all'],
...kibanaFeatures,
},
spaces: ['*'],
},
],
};
await securityService.role.create(role.name, {
kibana: role.kibana,
elasticsearch: role.elasticsearch,
});
await securityService.user.create(username, {
password: 'changeme',
full_name: role.name,
roles: [role.name],
});
return {
username,
password,
deleteUser: async () => {
await securityService.user.delete(role.name);
await securityService.role.delete(role.name);
},
};
};
before(async () => {
const response = await supertest
.post(`${getUrlPrefix('default')}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.on('error', logErrorDetails)
.send({
name: 'My sub connector',
connector_type_id: SENTINELONE_CONNECTOR_ID,
config: { url: 'https://some.non.existent.com' },
secrets: { token: 'abc-123' },
})
.expect(200);
connectorId = response.body.id;
});
after(async () => {
if (connectorId) {
await supertest
.delete(`${getUrlPrefix('default')}/api/actions/connector/${connectorId}`)
.set('kbn-xsrf', 'true')
.send()
.expect(({ ok, status }) => {
// Should cover all success codes (ex. 204 (no content), 200, etc...)
if (!ok) {
throw new Error(
`Expected delete to return a status code in the 200, but got ${status}`
);
}
});
connectorId = '';
}
});
const executeSubAction = async ({
subAction,
subActionParams,
expectedHttpCode = 200,
username = 'elastic',
password = 'changeme',
errorLogger = logErrorDetails,
}: {
supertest: SuperTest.SuperTest<SuperTest.Test>;
subAction: string;
subActionParams: Record<string, unknown>;
expectedHttpCode?: number;
username?: string;
password?: string;
errorLogger?: (err: any) => void;
}) => {
return supertestWithoutAuth
.post(`${getUrlPrefix('default')}/api/actions/connector/${connectorId}/_execute`)
.set('kbn-xsrf', 'foo')
.on('error', errorLogger)
.auth(username, password)
.send({
params: {
subAction,
subActionParams,
},
})
.expect(expectedHttpCode);
};
describe('and user has NO privileges', () => {
let user: CreatedUser;
before(async () => {
user = await createUser({
username: 'read_access_user',
kibanaFeatures: { actions: ['read'] },
});
});
after(async () => {
if (user) {
await user.deleteUser();
}
});
for (const s1SubAction of s1SubActions) {
it(`should deny execute of ${s1SubAction}`, async () => {
const execRes = await executeSubAction({
supertest: supertestWithoutAuth,
subAction: s1SubAction,
subActionParams: {},
username: user.username,
password: user.password,
expectedHttpCode: 403,
errorLogger: logErrorDetails.ignoreCodes([403]),
});
expect(execRes.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: 'Unauthorized to execute a ".sentinelone" action',
});
});
}
});
describe('and user has proper privileges', () => {
let user: CreatedUser;
before(async () => {
user = await createUser({
username: 'all_access_user',
});
});
after(async () => {
if (user) {
await user.deleteUser();
// @ts-expect-error
user = undefined;
}
});
for (const s1SubAction of s1SubActions) {
it(`should allow execute of ${s1SubAction}`, async () => {
const {
// eslint-disable-next-line @typescript-eslint/naming-convention
body: { status, message, connector_id },
} = await executeSubAction({
supertest: supertestWithoutAuth,
subAction: s1SubAction,
subActionParams: {},
username: user.username,
password: user.password,
});
expect({ status, message, connector_id }).to.eql({
status: 'error',
message: 'an error occurred while running the action',
connector_id: connectorId,
});
});
}
});
});
});
}

View file

@ -41,12 +41,13 @@ export default function ({ getService }: FtrProviderContext) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should handle execute request appropriately', async () => {
const connectorTypeId = 'test.index-record';
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'My action',
connector_type_id: 'test.index-record',
connector_type_id: connectorTypeId,
config: {
unencrypted: `This value shouldn't get encrypted`,
},
@ -78,7 +79,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(response.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: 'Unauthorized to execute actions',
message: `Unauthorized to execute a "${connectorTypeId}" action`,
});
break;
case 'global_read at space1':
@ -161,7 +162,7 @@ export default function ({ getService }: FtrProviderContext) {
case 'space_1_all at space2':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(403);
expect(response.statusCode).to.eql(403, response.text);
expect(response.body).to.eql({
statusCode: 403,
error: 'Forbidden',
@ -184,12 +185,13 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should handle execute request appropriately after action is updated', async () => {
const connectorTypeId = 'test.index-record';
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'My action',
connector_type_id: 'test.index-record',
connector_type_id: connectorTypeId,
config: {
unencrypted: `This value shouldn't get encrypted`,
},
@ -235,7 +237,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(response.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: 'Unauthorized to execute actions',
message: `Unauthorized to execute a "${connectorTypeId}" action`,
});
break;
case 'global_read at space1':
@ -286,7 +288,7 @@ export default function ({ getService }: FtrProviderContext) {
case 'no_kibana_privileges at space1':
case 'space_1_all_alerts_none_actions at space1':
case 'space_1_all at space2':
expect(response.statusCode).to.eql(403);
expect(response.statusCode).to.eql(403, response.text);
expect(response.body).to.eql({
statusCode: 403,
error: 'Forbidden',
@ -340,12 +342,13 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should handle execute request appropriately after changing config properties', async () => {
const connectorTypeId = '.email';
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'test email action',
connector_type_id: '.email',
connector_type_id: connectorTypeId,
config: {
from: 'email-from-1@example.com',
// this host is specifically added to allowedHosts in:
@ -397,7 +400,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(response.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: 'Unauthorized to execute actions',
message: `Unauthorized to execute a "${connectorTypeId}" action`,
});
break;
case 'global_read at space1':
@ -416,12 +419,13 @@ export default function ({ getService }: FtrProviderContext) {
let indexedRecord: any;
let searchResult: any;
const reference = `actions-execute-3:${user.username}`;
const connectorTypeId = 'test.authorization';
const { body: createdAction } = await supertest
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: 'My action',
connector_type_id: 'test.authorization',
connector_type_id: connectorTypeId,
})
.expect(200);
objectRemover.add(space.id, createdAction.id, 'action', 'actions');
@ -448,7 +452,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(response.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: 'Unauthorized to execute actions',
message: `Unauthorized to execute a "${connectorTypeId}" action`,
});
break;
case 'global_read at space1':
@ -528,7 +532,7 @@ export default function ({ getService }: FtrProviderContext) {
case 'global_read at space1':
case 'space_1_all at space1':
case 'space_1_all_with_restricted_fixture at space1':
expect(response.statusCode).to.eql(403);
expect(response.statusCode).to.eql(403, response.text);
expect(response.body).to.eql({
statusCode: 403,
error: 'Forbidden',
@ -542,7 +546,7 @@ export default function ({ getService }: FtrProviderContext) {
*/
case 'superuser at space1':
case 'system_actions at space1':
expect(response.statusCode).to.eql(200);
expect(response.statusCode).to.eql(200, response.text);
await validateSystemEventLog({
spaceId: space.id,

View file

@ -32,6 +32,7 @@ export default function connectorsTests({ loadTestFile, getService }: FtrProvide
loadTestFile(require.resolve('./connector_types/es_index_preconfigured'));
loadTestFile(require.resolve('./connector_types/opsgenie'));
loadTestFile(require.resolve('./connector_types/pagerduty'));
loadTestFile(require.resolve('./connector_types/sentinelone'));
loadTestFile(require.resolve('./connector_types/server_log'));
loadTestFile(require.resolve('./connector_types/slack_webhook'));
loadTestFile(require.resolve('./connector_types/slack_api'));