diff --git a/x-pack/plugins/security/common/model/deprecations.ts b/x-pack/plugins/security/common/model/deprecations.ts new file mode 100644 index 0000000000000..313cb5e8b5dad --- /dev/null +++ b/x-pack/plugins/security/common/model/deprecations.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationsDetails, GetDeprecationsContext } from '../../../../../src/core/server'; +import type { Role } from './role'; + +export interface PrivilegeDeprecationsRolesByFeatureIdResponse { + roles?: Role[]; + errors?: DeprecationsDetails[]; +} + +export interface PrivilegeDeprecationsRolesByFeatureIdRequest { + context: GetDeprecationsContext; + featureId: string; +} +export interface PrivilegeDeprecationsServices { + getKibanaRolesByFeatureId: ( + args: PrivilegeDeprecationsRolesByFeatureIdRequest + ) => Promise; +} diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index 8eb341ef9bd37..cb2e3082dc497 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -33,3 +33,8 @@ export { RoleTemplate, RoleMapping, } from './role_mapping'; +export { + PrivilegeDeprecationsRolesByFeatureIdRequest, + PrivilegeDeprecationsRolesByFeatureIdResponse, + PrivilegeDeprecationsServices, +} from './deprecations'; diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index 4d67f3435e7da..221baa85a65f6 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -13,3 +13,4 @@ export { } from './authorization_service'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; export { CheckPrivilegesPayload } from './types'; +export { transformElasticsearchRoleToRole, ElasticsearchRole } from './roles'; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts similarity index 96% rename from x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts rename to x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts index fa119ca704753..c0dab16f97af8 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts +++ b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts @@ -8,10 +8,10 @@ import { GLOBAL_RESOURCE, RESERVED_PRIVILEGES_APPLICATION_WILDCARD, -} from '../../../../../common/constants'; -import type { Role, RoleKibanaPrivilege } from '../../../../../common/model'; -import { PrivilegeSerializer } from '../../../../authorization/privilege_serializer'; -import { ResourceSerializer } from '../../../../authorization/resource_serializer'; +} from '../../../common/constants'; +import type { Role, RoleKibanaPrivilege } from '../../../common/model'; +import { PrivilegeSerializer } from '../privilege_serializer'; +import { ResourceSerializer } from '../resource_serializer'; export type ElasticsearchRole = Pick & { applications: Array<{ diff --git a/x-pack/plugins/security/server/authorization/roles/index.ts b/x-pack/plugins/security/server/authorization/roles/index.ts new file mode 100644 index 0000000000000..a5047a1872c09 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/roles/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { transformElasticsearchRoleToRole, ElasticsearchRole } from './elasticsearch_role'; diff --git a/x-pack/plugins/security/server/deprecations/index.ts b/x-pack/plugins/security/server/deprecations/index.ts new file mode 100644 index 0000000000000..a2e93314cb8c2 --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +/** + * getKibanaRolesByFeature + */ + +export { getPrivilegeDeprecationsServices } from './privilege_deprecations'; diff --git a/x-pack/plugins/security/server/deprecations/privilege_deprecations.ts b/x-pack/plugins/security/server/deprecations/privilege_deprecations.ts new file mode 100644 index 0000000000000..8d5794078bcbb --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/privilege_deprecations.ts @@ -0,0 +1,76 @@ +/* + * 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 type { SecurityLicense } from '../../common/licensing'; +import type { + PrivilegeDeprecationsRolesByFeatureIdRequest, + PrivilegeDeprecationsRolesByFeatureIdResponse, +} from '../../common/model'; +import { transformElasticsearchRoleToRole } from '../authorization'; +import type { AuthorizationServiceSetupInternal, ElasticsearchRole } from '../authorization'; +import { getDetailedErrorMessage, getErrorStatusCode } from '../errors'; + +export const getPrivilegeDeprecationsServices = ( + authz: AuthorizationServiceSetupInternal, + license: SecurityLicense +) => { + const getKibanaRolesByFeatureId = async ({ + context, + featureId, + }: PrivilegeDeprecationsRolesByFeatureIdRequest): Promise => { + // Nothing to do if security is disabled + if (!license.isEnabled()) { + return { + roles: [], + }; + } + let kibanaRoles; + try { + const { body: elasticsearchRoles } = await context.esClient.asCurrentUser.security.getRole< + Record + >(); + + kibanaRoles = Object.entries(elasticsearchRoles).map(([roleName, elasticsearchRole]) => + transformElasticsearchRoleToRole( + // @ts-expect-error @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`. + elasticsearchRole, + roleName, + authz.applicationName + ) + ); + } catch (e) { + const statusCode = getErrorStatusCode(e); + const isUnauthorized = statusCode === 403; + const message = isUnauthorized + ? `You must have the 'manage_security' cluster privilege to fix role deprecations.` + : `Error retrieving roles for privilege deprecations: ${getDetailedErrorMessage(e)}`; + + return { + errors: [ + { + title: 'title', + level: 'fetch_error', + message, + correctiveActions: { + manualSteps: [ + 'A user with the "manage_security" cluster privilege is required to perform this check.', + ], + }, + }, + ], + }; + } + return { + roles: kibanaRoles.filter((role) => + role.kibana.find((privilege) => Object.hasOwnProperty.call(privilege.feature, featureId)) + ), + }; + }; + return Object.freeze({ + getKibanaRolesByFeatureId, + }); +}; diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index f1f858a40a465..778d0f0a267ad 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -28,6 +28,9 @@ function createSetupMock() { }, registerSpacesService: jest.fn(), license: licenseMock.create(), + privilegeDeprecationServices: { + getKibanaRolesByFeatureId: jest.fn(), + }, }; } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index eb88aba1c0e1b..9a5f8ea276609 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -123,6 +123,9 @@ describe('Security Plugin', () => { "isEnabled": [Function], "isLicenseAvailable": [Function], }, + "privilegeDeprecationServices": Object { + "getKibanaRolesByFeatureId": [Function], + }, } `); }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index e3da0716f29ee..245e9488f8412 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -30,7 +30,7 @@ import type { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import type { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import type { SecurityLicense } from '../common/licensing'; import { SecurityLicenseService } from '../common/licensing'; -import type { AuthenticatedUser } from '../common/model'; +import type { AuthenticatedUser, PrivilegeDeprecationsServices } from '../common/model'; import type { AnonymousAccessServiceStart } from './anonymous_access'; import { AnonymousAccessService } from './anonymous_access'; import type { AuditServiceSetup } from './audit'; @@ -44,6 +44,7 @@ import type { AuthorizationServiceSetup, AuthorizationServiceSetupInternal } fro import { AuthorizationService } from './authorization'; import type { ConfigSchema, ConfigType } from './config'; import { createConfig } from './config'; +import { getPrivilegeDeprecationsServices } from './deprecations'; import { ElasticsearchService } from './elasticsearch'; import type { SecurityFeatureUsageServiceStart } from './feature_usage'; import { SecurityFeatureUsageService } from './feature_usage'; @@ -85,6 +86,10 @@ export interface SecurityPluginSetup { * Exposes services for audit logging. */ audit: AuditServiceSetup; + /** + * Exposes services to access kibana roles per feature id with the GetDeprecationsContext + */ + privilegeDeprecationServices: PrivilegeDeprecationsServices; } /** @@ -321,9 +326,7 @@ export class SecurityPlugin asScoped: this.auditSetup.asScoped, getLogger: this.auditSetup.getLogger, }, - authc: { getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request) }, - authz: { actions: this.authorizationSetup.actions, checkPrivilegesWithRequest: this.authorizationSetup.checkPrivilegesWithRequest, @@ -333,8 +336,11 @@ export class SecurityPlugin this.authorizationSetup.checkSavedObjectsPrivilegesWithRequest, mode: this.authorizationSetup.mode, }, - license, + privilegeDeprecationServices: getPrivilegeDeprecationsServices( + this.authorizationSetup, + license + ), }); } diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts index 8334dd3c05476..e090cd26dc39f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { ElasticsearchRole, transformElasticsearchRoleToRole } from './elasticsearch_role'; +export { ElasticsearchRole, transformElasticsearchRoleToRole } from '../../../../authorization'; export { getPutPayloadSchema, transformPutPayloadToElasticsearchRole } from './put_payload'; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts index 8a560d7b6dd87..7fb2baf0fd410 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts @@ -10,10 +10,10 @@ import _ from 'lodash'; import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; +import type { ElasticsearchRole } from '.'; import { GLOBAL_RESOURCE } from '../../../../../common/constants'; import { PrivilegeSerializer } from '../../../../authorization/privilege_serializer'; import { ResourceSerializer } from '../../../../authorization/resource_serializer'; -import type { ElasticsearchRole } from './elasticsearch_role'; /** * Elasticsearch specific portion of the role definition. diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 2e2dffa05c9fb..83e8e649e6c8d 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -10,6 +10,7 @@ import { ENABLE_CASE_CONNECTOR } from '../../cases/common'; import { metadataTransformPattern } from './endpoint/constants'; export const APP_ID = 'securitySolution'; +export const CASES_FEATURE_ID = 'securitySolutionCases'; export const SERVER_APP_ID = 'siem'; export const APP_NAME = 'Security'; export const APP_ICON = 'securityAnalyticsApp'; diff --git a/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts b/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts new file mode 100644 index 0000000000000..452ed661a4cc6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts @@ -0,0 +1,123 @@ +/* + * 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 { updateSecuritySolutionPrivileges } from '.'; + +describe('deprecations', () => { + describe('create cases privileges from siem privileges without cases sub-feature', () => { + test('should be empty if siem privileges is an empty array', () => { + expect(updateSecuritySolutionPrivileges([])).toMatchInlineSnapshot(`Object {}`); + }); + + test('siem privileges === ["all"]', () => { + expect(updateSecuritySolutionPrivileges(['all'])).toMatchInlineSnapshot(` + Object { + "securitySolutionCases": Array [ + "all", + ], + "siem": Array [ + "all", + ], + } + `); + }); + + test('siem privileges === ["read"]', () => { + expect(updateSecuritySolutionPrivileges(['read'])).toMatchInlineSnapshot(` + Object { + "securitySolutionCases": Array [ + "read", + ], + "siem": Array [ + "read", + ], + } + `); + }); + }); + + describe('create cases privileges from siem privileges with cases sub-feature', () => { + test('should be empty if siem privileges is an empty array', () => { + expect(updateSecuritySolutionPrivileges([])).toMatchInlineSnapshot(`Object {}`); + }); + + test('siem privileges === ["minimal_all"]', () => { + expect(updateSecuritySolutionPrivileges(['minimal_all'])).toMatchInlineSnapshot(` + Object { + "siem": Array [ + "all", + ], + } + `); + }); + + test('siem privileges === ["minimal_all", "cases_read"]', () => { + expect(updateSecuritySolutionPrivileges(['minimal_all', 'cases_read'])) + .toMatchInlineSnapshot(` + Object { + "securitySolutionCases": Array [ + "read", + ], + "siem": Array [ + "all", + ], + } + `); + }); + + test('siem privileges === ["minimal_all", "cases_all"]', () => { + expect(updateSecuritySolutionPrivileges(['minimal_all', 'cases_all'])).toMatchInlineSnapshot(` + Object { + "securitySolutionCases": Array [ + "all", + ], + "siem": Array [ + "all", + ], + } + `); + }); + + test('siem privileges === ["minimal_read"]', () => { + expect(updateSecuritySolutionPrivileges(['minimal_read'])).toMatchInlineSnapshot(` + Object { + "siem": Array [ + "read", + ], + } + `); + }); + + test('siem privileges === ["minimal_read", "cases_read"]', () => { + expect(updateSecuritySolutionPrivileges(['minimal_read', 'cases_read'])) + .toMatchInlineSnapshot(` + Object { + "securitySolutionCases": Array [ + "read", + ], + "siem": Array [ + "read", + ], + } + `); + }); + + test('siem privileges === ["minimal_read", "cases_all"]', () => { + expect(updateSecuritySolutionPrivileges(['minimal_read', 'cases_all'])) + .toMatchInlineSnapshot(` + Object { + "securitySolutionCases": Array [ + "all", + ], + "siem": Array [ + "read", + ], + } + `); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts b/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts new file mode 100644 index 0000000000000..38e7f24b894c1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts @@ -0,0 +1,128 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { DeprecationsDetails, DeprecationsServiceSetup } from '../../../../../src/core/server'; +import type { PrivilegeDeprecationsServices } from '../../../security/common/model'; +import { CASES_FEATURE_ID, SERVER_APP_ID } from '../../common/constants'; + +interface Deps { + deprecationsService: DeprecationsServiceSetup; + getKibanaRolesByFeatureId?: PrivilegeDeprecationsServices['getKibanaRolesByFeatureId']; +} + +export const updateSecuritySolutionPrivileges = ( + siemPrivileges: string[] +): Partial> => { + const newSiemPrivileges = siemPrivileges.reduce((acc, priv) => { + if (!acc.includes('all') && (priv === 'minimal_all' || priv === 'all')) { + return [...acc, 'all']; + } else if (!acc.includes('read') && (priv === 'minimal_read' || priv === 'read')) { + return [...acc, 'read']; + } + return acc; + }, []); + + const casePrivileges = + siemPrivileges.includes('minimal_read') || siemPrivileges.includes('minimal_all') + ? siemPrivileges.reduce((acc, priv) => { + if (priv === 'cases_all') { + return [...acc, 'all']; + } else if (priv === 'cases_read') { + return [...acc, 'read']; + } + return acc; + }, []) + : newSiemPrivileges; + + return { + ...(newSiemPrivileges.length > 0 + ? { + [SERVER_APP_ID]: newSiemPrivileges, + } + : {}), + ...(casePrivileges.length > 0 + ? { + [CASES_FEATURE_ID]: casePrivileges, + } + : {}), + }; +}; + +export const registerPrivilegeDeprecations = ({ + deprecationsService, + getKibanaRolesByFeatureId, +}: Deps) => { + deprecationsService.registerDeprecations({ + getDeprecations: async (context) => { + if (getKibanaRolesByFeatureId) { + const responseRoles = await getKibanaRolesByFeatureId({ + context, + featureId: 'siem', + }); + + if (responseRoles.errors && responseRoles.errors.length > 0) { + return responseRoles.errors; + } + + const roles = responseRoles.roles ?? []; + return roles.map((role) => { + const { metadata, elasticsearch, kibana } = role; + + const updatedKibana = kibana.map((privilege) => { + const { siem, ...otherFeatures } = privilege.feature; + const privilegeContainsSiem = Array.isArray(siem) && siem.length > 0; + + if (privilegeContainsSiem) { + return { + ...privilege, + feature: { + ...otherFeatures, + ...updateSecuritySolutionPrivileges(siem), + }, + }; + } + return privilege; + }); + + const updatedRole = { + metadata, + elasticsearch, + kibana: updatedKibana, + }; + + return { + title: i18n.translate( + 'xpack.securitySolution.deprecation.casesSubfeaturePrivileges.title', + { + defaultMessage: 'Deprecate cases sub-feature privileges in Security', + } + ), + message: i18n.translate( + 'xpack.securitySolution.deprecation.ccasesSubfeaturePrivileges.message', + { + defaultMessage: + 'The "securitySolutions" feature privilege has been populated with siem feature or cases sub feature if existing.', + } + ), + level: 'warning', + correctiveActions: { + api: { + method: 'PUT', + path: `/api/security/role/${encodeURIComponent(role.name)}`, + body: updatedRole, + }, + manualSteps: [], + }, + }; + }); + } + return []; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 14da8ca650960..ebd8316dd7ffd 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -104,6 +104,7 @@ import { getKibanaPrivilegesFeaturePrivileges } from './features'; import { EndpointMetadataService } from './endpoint/services/metadata'; import { CreateRuleOptions } from './lib/detection_engine/rule_types/types'; import { ctiFieldMap } from './lib/detection_engine/rule_types/field_maps/cti'; +import { registerPrivilegeDeprecations } from './deprecation_privileges'; export interface SetupPlugins { alerting: AlertingSetup; @@ -351,6 +352,12 @@ export class Plugin implements IPlugin