Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,8 @@ describe('Endpoint Authz service', () => {
['upload', 'canWriteFileOperations'],
['scan', 'canWriteScanOperations'],
['runscript', 'canWriteExecuteOperations'],
['cancel', 'canReadActionsLogManagement'],
// Note: cancel commands are not tested here since getRequiredCancelPermissions()
// should be called with the target command being cancelled, not 'cancel' itself
])('should return correct permissions for %s command', (command, expectedCommandSpecific) => {
const result = getRequiredCancelPermissions(command);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { ProductFeaturesService } from '../../../../server/lib/product_feat
import {
RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ,
RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP,
DYNAMIC_COMMAND_BASED,
} from '../response_actions/constants';
import type { LicenseService } from '../../../license';
import type { EndpointAuthz, EndpointAuthzKeyList } from '../../types/authz';
Expand Down Expand Up @@ -184,13 +185,21 @@ export const calculateEndpointAuthz = (
// of the response actions except `release`. Sole access to `release` is something
// that is supported for a user in a license downgrade scenario, and in that case, we don't want
// to allow access to Response Console.
// Note: We filter out dynamic permissions since they can't be evaluated without context
const staticAuthzValues = Object.values(
omit(RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ, ['release'])
).filter((authzKey) => authzKey !== DYNAMIC_COMMAND_BASED);

// Check if user has any static response action permissions
const hasStaticResponseActionAccess = staticAuthzValues.some((responseActionAuthzKey) => {
return authz[responseActionAuthzKey as keyof EndpointAuthz];
});

// For dynamic permissions (like cancel), we still want to grant console access
// if users have any response action permissions, since they can use cancel functionality
// This maintains the existing behavior while supporting the new dynamic authorization model
authz.canAccessResponseConsole =
isEnterpriseLicense &&
Object.values(omit(RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ, 'release')).some(
(responseActionAuthzKey) => {
return authz[responseActionAuthzKey];
}
);
isEnterpriseLicense && (hasStaticResponseActionAccess || hasAnyResponseActionPrivilege(authz));

return authz;
};
Expand Down Expand Up @@ -265,10 +274,25 @@ export const canFetchPackageAndAgentPolicies = (capabilities: Capabilities): boo
return canReadPolicyManagement || (canReadFleetAgentPolicies && canReadIntegrations);
};

/**
* Checks if the user has any response action privileges (excluding release-only access).
* This utility consolidates the logic for determining whether a user can perform any response actions.
*/
export const hasAnyResponseActionPrivilege = (authz: EndpointAuthz): boolean => {
return (
authz.canIsolateHost ||
authz.canKillProcess ||
authz.canSuspendProcess ||
authz.canGetRunningProcesses ||
authz.canWriteFileOperations ||
authz.canWriteExecuteOperations ||
authz.canWriteScanOperations
);
};

/**
* Determines the required permissions to cancel a specific action based on its command type permissions to cancel actions.
**/

export const getRequiredCancelPermissions = (
command: ResponseActionsApiCommandNames
): EndpointAuthzKeyList[number] => {
Expand All @@ -278,5 +302,11 @@ export const getRequiredCancelPermissions = (
throw new Error(`Unknown or unsupported command for cancellation: ${command}`);
}

return RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ[consoleCommand];
const authzKey = RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ[consoleCommand];

if (authzKey === DYNAMIC_COMMAND_BASED) {
throw new Error(`Cannot resolve dynamic permission for command: ${command} without context`);
}

return authzKey;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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 {
resolveCommandPermission,
type ConsoleResponseActionCommands,
type ResponseActionsApiCommandNames,
} from './constants';

describe('resolveCommandPermission', () => {
describe('static permission resolution', () => {
it.each([
['isolate', 'canIsolateHost'],
['release', 'canUnIsolateHost'],
['execute', 'canWriteExecuteOperations'],
['get-file', 'canWriteFileOperations'],
['upload', 'canWriteFileOperations'],
['processes', 'canGetRunningProcesses'],
['kill-process', 'canKillProcess'],
['suspend-process', 'canSuspendProcess'],
['scan', 'canWriteScanOperations'],
['runscript', 'canWriteExecuteOperations'],
])('should resolve static permission for %s command', (command, expectedPermission) => {
const result = resolveCommandPermission(command as ConsoleResponseActionCommands);
expect(result).toBe(expectedPermission);
});
});

describe('dynamic permission resolution', () => {
// No setup needed - cancel command already uses DYNAMIC_COMMAND_BASED

it('should resolve cancel permission based on target action command', () => {
const result = resolveCommandPermission('cancel', { targetActionCommand: 'isolate' });
expect(result).toBe('canIsolateHost');
});

it.each([
['isolate', 'canIsolateHost'],
['unisolate', 'canUnIsolateHost'],
['kill-process', 'canKillProcess'],
['suspend-process', 'canSuspendProcess'],
['running-processes', 'canGetRunningProcesses'],
['get-file', 'canWriteFileOperations'],
['execute', 'canWriteExecuteOperations'],
['upload', 'canWriteFileOperations'],
['scan', 'canWriteScanOperations'],
['runscript', 'canWriteExecuteOperations'],
])(
'should resolve cancel permission for %s target command',
(targetCommand, expectedPermission) => {
const result = resolveCommandPermission('cancel', {
targetActionCommand: targetCommand as ResponseActionsApiCommandNames,
});
expect(result).toBe(expectedPermission);
}
);

it('should throw error when cancel command lacks target action context', () => {
expect(() => resolveCommandPermission('cancel')).toThrow(
'Cancel command requires target action command context for permission resolution'
);
});

it('should throw error when cancel command has undefined target action', () => {
expect(() => resolveCommandPermission('cancel', {})).toThrow(
'Cancel command requires target action command context for permission resolution'
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ export type ResponseConsoleRbacControls =
// TODO: Check for Cancel action, or we can change RESPONSE_CONSOLE_ACTION_COMMANDS_TO_RBAC_FEATURE_CONTROL to use Partial and omit cancel
| 'readActionsLogManagement';

/**
* Sentinel value used to indicate that a command requires dynamic, context-dependent authorization
* rather than a static permission mapping.
*/
export const DYNAMIC_COMMAND_BASED = 'DYNAMIC_COMMAND_BASED' as const;

/**
* maps the console command to the RBAC control (kibana feature control) that is required to access it via console
*/
Expand Down Expand Up @@ -167,10 +173,19 @@ export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_ENDPOINT_CAPABILITY = Object.fr
});

/**
* The list of console commands mapped to the required EndpointAuthz to access that command
* Authorization types for response action commands
*/
type StaticAuthzKey = EndpointAuthzKeyList[number];
type DynamicAuthzKey = typeof DYNAMIC_COMMAND_BASED;
export type AuthzKey = StaticAuthzKey | DynamicAuthzKey;

/**
* The list of console commands mapped to the required EndpointAuthz to access that command.
* Most commands have static authorization requirements, but some (like 'cancel') require
* dynamic, context-dependent authorization.
*/
export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ = Object.freeze<
Record<ConsoleResponseActionCommands, EndpointAuthzKeyList[number]>
Record<ConsoleResponseActionCommands, AuthzKey>
>({
isolate: 'canIsolateHost',
release: 'canUnIsolateHost',
Expand All @@ -182,9 +197,51 @@ export const RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ = Object.freeze<
'suspend-process': 'canSuspendProcess',
scan: 'canWriteScanOperations',
runscript: 'canWriteExecuteOperations',
cancel: 'canReadActionsLogManagement',
cancel: DYNAMIC_COMMAND_BASED,
});

/**
* Resolves the required permission for a console command, supporting both static and dynamic authorization.
*
* @param command - The console command to resolve permissions for
* @param context - Optional context for dynamic resolution (e.g., target action command for cancel operations)
* @returns The required authorization key for the command
* @throws Error if dynamic resolution is required but context is missing
*/
export const resolveCommandPermission = (
command: ConsoleResponseActionCommands,
context?: { targetActionCommand?: ResponseActionsApiCommandNames }
): EndpointAuthzKeyList[number] => {
const authzKey = RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ[command];

// Handle static permissions
if (authzKey !== DYNAMIC_COMMAND_BASED) {
return authzKey;
}

// Handle dynamic permissions
if (command === 'cancel') {
if (!context?.targetActionCommand) {
throw new Error(
'Cancel command requires target action command context for permission resolution'
);
}

// Dynamic resolution for cancel - delegate to existing logic
const consoleCommand =
RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP[context.targetActionCommand];
const targetPermission = RESPONSE_CONSOLE_ACTION_COMMANDS_TO_REQUIRED_AUTHZ[consoleCommand];

if (targetPermission === DYNAMIC_COMMAND_BASED) {
throw new Error(`Cannot resolve nested dynamic permission for command: ${consoleCommand}`);
}

return targetPermission;
}

throw new Error(`Unsupported dynamic command: ${command}`);
};

// 4 hrs in seconds
// 4 * 60 * 60
export const DEFAULT_EXECUTE_ACTION_TIMEOUT = 14400;
Expand Down
Loading