diff --git a/x-pack/plugins/security/common/index.ts b/x-pack/plugins/security/common/index.ts index ac5d252c98a8b..1d05036191635 100644 --- a/x-pack/plugins/security/common/index.ts +++ b/x-pack/plugins/security/common/index.ts @@ -6,4 +6,4 @@ */ export type { SecurityLicense } from './licensing'; -export type { AuthenticatedUser } from './model'; +export type { AuthenticatedUser, PrivilegeDeprecationsService } from './model'; diff --git a/x-pack/plugins/security/server/deprecations/index.ts b/x-pack/plugins/security/server/deprecations/index.ts index 05802a5a673c5..0bbeccf1588b4 100644 --- a/x-pack/plugins/security/server/deprecations/index.ts +++ b/x-pack/plugins/security/server/deprecations/index.ts @@ -5,8 +5,13 @@ * 2.0. */ -/** - * getKibanaRolesByFeature - */ - export { getPrivilegeDeprecationsService } from './privilege_deprecations'; +export { + registerKibanaDashboardOnlyRoleDeprecation, + KIBANA_DASHBOARD_ONLY_USER_ROLE_NAME, +} from './kibana_dashboard_only_role'; +export { + registerKibanaUserRoleDeprecation, + KIBANA_ADMIN_ROLE_NAME, + KIBANA_USER_ROLE_NAME, +} from './kibana_user_role'; diff --git a/x-pack/plugins/security/server/deprecations/kibana_dashboard_only_role.test.ts b/x-pack/plugins/security/server/deprecations/kibana_dashboard_only_role.test.ts new file mode 100644 index 0000000000000..0ec55ef2f5923 --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/kibana_dashboard_only_role.test.ts @@ -0,0 +1,322 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; +import type { SecurityRoleMapping, SecurityUser } from '@elastic/elasticsearch/api/types'; + +import type { PackageInfo, RegisterDeprecationsConfig } from 'src/core/server'; +import { + deprecationsServiceMock, + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsClientMock, +} from 'src/core/server/mocks'; + +import { licenseMock } from '../../common/licensing/index.mock'; +import { securityMock } from '../mocks'; +import { registerKibanaDashboardOnlyRoleDeprecation } from './kibana_dashboard_only_role'; + +function getDepsMock() { + return { + logger: loggingSystemMock.createLogger(), + deprecationsService: deprecationsServiceMock.createSetupContract(), + license: licenseMock.create(), + packageInfo: { + branch: 'some-branch', + buildSha: 'sha', + dist: true, + version: '8.0.0', + buildNum: 1, + } as PackageInfo, + }; +} + +function getContextMock() { + return { + esClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), + }; +} + +function createMockUser(user: Partial = {}) { + return { enabled: true, username: 'userA', roles: ['roleA'], metadata: {}, ...user }; +} + +function createMockRoleMapping(mapping: Partial = {}) { + return { enabled: true, roles: ['roleA'], rules: {}, metadata: {}, ...mapping }; +} + +describe('Kibana Dashboard Only User role deprecations', () => { + let mockDeps: ReturnType; + let mockContext: ReturnType; + let deprecationHandler: RegisterDeprecationsConfig; + beforeEach(() => { + mockContext = getContextMock(); + mockDeps = getDepsMock(); + registerKibanaDashboardOnlyRoleDeprecation(mockDeps); + + expect(mockDeps.deprecationsService.registerDeprecations).toHaveBeenCalledTimes(1); + deprecationHandler = mockDeps.deprecationsService.registerDeprecations.mock.calls[0][0]; + }); + + it('does not return any deprecations if security is not enabled', async () => { + mockDeps.license.isEnabled.mockReturnValue(false); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toEqual([]); + expect(mockContext.esClient.asCurrentUser.security.getUser).not.toHaveBeenCalled(); + expect(mockContext.esClient.asCurrentUser.security.getRoleMapping).not.toHaveBeenCalled(); + }); + + it('does not return any deprecations if none of the users and role mappings has a dashboard only role', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: { enabled: true, roles: ['roleA'], rules: {}, metadata: {} }, + }, + }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toEqual([]); + }); + + it('returns deprecations even if cannot retrieve users due to permission error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 403, body: {} })) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Make sure you have a \\"manage_security\\" cluster privilege assigned.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/kibana/some-branch/xpack-security.html#_required_permissions_7", + "level": "fetch_error", + "message": "You do not have enough permissions to fix this deprecation.", + "title": "The \\"kibana_dashboard_only_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns deprecations even if cannot retrieve users due to unknown error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 500, body: {} })) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Check Kibana logs for more details.", + ], + }, + "deprecationType": "feature", + "level": "fetch_error", + "message": "Failed to perform deprecation check. Check Kibana logs for more details.", + "title": "The \\"kibana_dashboard_only_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns deprecations even if cannot retrieve role mappings due to permission error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 403, body: {} })) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Make sure you have a \\"manage_security\\" cluster privilege assigned.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/kibana/some-branch/xpack-security.html#_required_permissions_7", + "level": "fetch_error", + "message": "You do not have enough permissions to fix this deprecation.", + "title": "The \\"kibana_dashboard_only_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns deprecations even if cannot retrieve role mappings due to unknown error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 500, body: {} })) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Check Kibana logs for more details.", + ], + }, + "deprecationType": "feature", + "level": "fetch_error", + "message": "Failed to perform deprecation check. Check Kibana logs for more details.", + "title": "The \\"kibana_dashboard_only_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns only user-related deprecations', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ + body: { + userA: createMockUser({ username: 'userA', roles: ['roleA'] }), + userB: createMockUser({ + username: 'userB', + roles: ['roleB', 'kibana_dashboard_only_user'], + }), + userC: createMockUser({ username: 'userC', roles: ['roleC'] }), + userD: createMockUser({ username: 'userD', roles: ['kibana_dashboard_only_user'] }), + }, + }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Create a custom role with Kibana privileges to grant access to Dashboard only.", + "Remove the \\"kibana_dashboard_only_user\\" role from all users and add the custom role. The affected users are: userB, userD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Users with the \\"kibana_dashboard_only_user\\" role will not be able to access the Dashboard app. Use Kibana privileges instead.", + "title": "The \\"kibana_dashboard_only_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns only role-mapping-related deprecations', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: createMockRoleMapping({ roles: ['roleA'] }), + mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_dashboard_only_user'] }), + mappingC: createMockRoleMapping({ roles: ['roleC'] }), + mappingD: createMockRoleMapping({ roles: ['kibana_dashboard_only_user'] }), + }, + }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Create a custom role with Kibana privileges to grant access to Dashboard only.", + "Remove the \\"kibana_dashboard_only_user\\" role from all role mappings and add the custom role. The affected role mappings are: mappingB, mappingD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Users with the \\"kibana_dashboard_only_user\\" role will not be able to access the Dashboard app. Use Kibana privileges instead.", + "title": "The \\"kibana_dashboard_only_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns both user-related and role-mapping-related deprecations', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ + body: { + userA: createMockUser({ username: 'userA', roles: ['roleA'] }), + userB: createMockUser({ + username: 'userB', + roles: ['roleB', 'kibana_dashboard_only_user'], + }), + userC: createMockUser({ username: 'userC', roles: ['roleC'] }), + userD: createMockUser({ username: 'userD', roles: ['kibana_dashboard_only_user'] }), + }, + }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: createMockRoleMapping({ roles: ['roleA'] }), + mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_dashboard_only_user'] }), + mappingC: createMockRoleMapping({ roles: ['roleC'] }), + mappingD: createMockRoleMapping({ roles: ['kibana_dashboard_only_user'] }), + }, + }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Create a custom role with Kibana privileges to grant access to Dashboard only.", + "Remove the \\"kibana_dashboard_only_user\\" role from all users and add the custom role. The affected users are: userB, userD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Users with the \\"kibana_dashboard_only_user\\" role will not be able to access the Dashboard app. Use Kibana privileges instead.", + "title": "The \\"kibana_dashboard_only_user\\" role is deprecated", + }, + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Create a custom role with Kibana privileges to grant access to Dashboard only.", + "Remove the \\"kibana_dashboard_only_user\\" role from all role mappings and add the custom role. The affected role mappings are: mappingB, mappingD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Users with the \\"kibana_dashboard_only_user\\" role will not be able to access the Dashboard app. Use Kibana privileges instead.", + "title": "The \\"kibana_dashboard_only_user\\" role is deprecated", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/security/server/deprecations/kibana_dashboard_only_role.ts b/x-pack/plugins/security/server/deprecations/kibana_dashboard_only_role.ts new file mode 100644 index 0000000000000..4e911b16166f5 --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/kibana_dashboard_only_role.ts @@ -0,0 +1,247 @@ +/* + * 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 { + SecurityGetRoleMappingResponse, + SecurityGetUserResponse, +} from '@elastic/elasticsearch/api/types'; + +import { i18n } from '@kbn/i18n'; +import type { + DeprecationsDetails, + DeprecationsServiceSetup, + ElasticsearchClient, + Logger, + PackageInfo, +} from 'src/core/server'; + +import type { SecurityLicense } from '../../common'; +import { getDetailedErrorMessage, getErrorStatusCode } from '../errors'; + +export const KIBANA_DASHBOARD_ONLY_USER_ROLE_NAME = 'kibana_dashboard_only_user'; + +export interface Deps { + deprecationsService: DeprecationsServiceSetup; + license: SecurityLicense; + logger: Logger; + packageInfo: PackageInfo; +} + +function getDeprecationTitle() { + return i18n.translate('xpack.security.deprecations.kibanaDashboardOnlyUser.deprecationTitle', { + defaultMessage: 'The "{roleName}" role is deprecated', + values: { roleName: KIBANA_DASHBOARD_ONLY_USER_ROLE_NAME }, + }); +} + +function getDeprecationMessage() { + return i18n.translate('xpack.security.deprecations.kibanaDashboardOnlyUser.deprecationMessage', { + defaultMessage: + 'Users with the "{roleName}" role will not be able to access the Dashboard app. Use Kibana privileges instead.', + values: { roleName: KIBANA_DASHBOARD_ONLY_USER_ROLE_NAME }, + }); +} + +export const registerKibanaDashboardOnlyRoleDeprecation = ({ + deprecationsService, + logger, + license, + packageInfo, +}: Deps) => { + deprecationsService.registerDeprecations({ + getDeprecations: async (context) => { + // Nothing to do if security is disabled + if (!license.isEnabled()) { + return []; + } + + return [ + ...(await getUsersDeprecations(context.esClient.asCurrentUser, logger, packageInfo)), + ...(await getRoleMappingsDeprecations(context.esClient.asCurrentUser, logger, packageInfo)), + ]; + }, + }); +}; + +async function getUsersDeprecations( + client: ElasticsearchClient, + logger: Logger, + packageInfo: PackageInfo +): Promise { + let users: SecurityGetUserResponse; + try { + users = (await client.security.getUser()).body; + } catch (err) { + if (getErrorStatusCode(err) === 403) { + logger.warn( + `Failed to retrieve users when checking for deprecations: the "manage_security" cluster privilege is required.` + ); + } else { + logger.error( + `Failed to retrieve users when checking for deprecations, unexpected error: ${getDetailedErrorMessage( + err + )}.` + ); + } + return deprecationError(packageInfo, err); + } + + const usersWithKibanaDashboardOnlyRole = Object.values(users) + .filter((user) => user.roles.includes(KIBANA_DASHBOARD_ONLY_USER_ROLE_NAME)) + .map((user) => user.username); + if (usersWithKibanaDashboardOnlyRole.length === 0) { + return []; + } + + return [ + { + title: getDeprecationTitle(), + message: getDeprecationMessage(), + level: 'warning', + deprecationType: 'feature', + documentationUrl: `https://www.elastic.co/guide/en/elasticsearch/reference/${packageInfo.branch}/built-in-roles.html`, + correctiveActions: { + manualSteps: [ + i18n.translate( + 'xpack.security.deprecations.kibanaDashboardOnlyUser.usersDeprecationCorrectiveActionOne', + { + defaultMessage: + 'Create a custom role with Kibana privileges to grant access to Dashboard only.', + } + ), + i18n.translate( + 'xpack.security.deprecations.kibanaDashboardOnlyUser.usersDeprecationCorrectiveActionTwo', + { + defaultMessage: + 'Remove the "{roleName}" role from all users and add the custom role. The affected users are: {users}.', + values: { + roleName: KIBANA_DASHBOARD_ONLY_USER_ROLE_NAME, + users: usersWithKibanaDashboardOnlyRole.join(', '), + }, + } + ), + ], + }, + }, + ]; +} + +async function getRoleMappingsDeprecations( + client: ElasticsearchClient, + logger: Logger, + packageInfo: PackageInfo +): Promise { + let roleMappings: SecurityGetRoleMappingResponse; + try { + roleMappings = (await client.security.getRoleMapping()).body; + } catch (err) { + if (getErrorStatusCode(err) === 403) { + logger.warn( + `Failed to retrieve role mappings when checking for deprecations: the "manage_security" cluster privilege is required.` + ); + } else { + logger.error( + `Failed to retrieve role mappings when checking for deprecations, unexpected error: ${getDetailedErrorMessage( + err + )}.` + ); + } + return deprecationError(packageInfo, err); + } + + const roleMappingsWithKibanaDashboardOnlyRole = Object.entries(roleMappings) + .filter(([, roleMapping]) => roleMapping.roles.includes(KIBANA_DASHBOARD_ONLY_USER_ROLE_NAME)) + .map(([mappingName]) => mappingName); + if (roleMappingsWithKibanaDashboardOnlyRole.length === 0) { + return []; + } + + return [ + { + title: getDeprecationTitle(), + message: getDeprecationMessage(), + level: 'warning', + deprecationType: 'feature', + documentationUrl: `https://www.elastic.co/guide/en/elasticsearch/reference/${packageInfo.branch}/built-in-roles.html`, + correctiveActions: { + manualSteps: [ + i18n.translate( + 'xpack.security.deprecations.kibanaDashboardOnlyUser.roleMappingsDeprecationCorrectiveActionOne', + { + defaultMessage: + 'Create a custom role with Kibana privileges to grant access to Dashboard only.', + } + ), + i18n.translate( + 'xpack.security.deprecations.kibanaDashboardOnlyUser.roleMappingsDeprecationCorrectiveActionTwo', + { + defaultMessage: + 'Remove the "{roleName}" role from all role mappings and add the custom role. The affected role mappings are: {roleMappings}.', + values: { + roleName: KIBANA_DASHBOARD_ONLY_USER_ROLE_NAME, + roleMappings: roleMappingsWithKibanaDashboardOnlyRole.join(', '), + }, + } + ), + ], + }, + }, + ]; +} + +function deprecationError(packageInfo: PackageInfo, error: Error): DeprecationsDetails[] { + const title = getDeprecationTitle(); + + if (getErrorStatusCode(error) === 403) { + return [ + { + title, + level: 'fetch_error', + deprecationType: 'feature', + message: i18n.translate( + 'xpack.security.deprecations.kibanaDashboardOnlyUser.forbiddenErrorMessage', + { defaultMessage: 'You do not have enough permissions to fix this deprecation.' } + ), + documentationUrl: `https://www.elastic.co/guide/en/kibana/${packageInfo.branch}/xpack-security.html#_required_permissions_7`, + correctiveActions: { + manualSteps: [ + i18n.translate( + 'xpack.security.deprecations.kibanaDashboardOnlyUser.forbiddenErrorCorrectiveAction', + { + defaultMessage: + 'Make sure you have a "manage_security" cluster privilege assigned.', + } + ), + ], + }, + }, + ]; + } + + return [ + { + title, + level: 'fetch_error', + deprecationType: 'feature', + message: i18n.translate( + 'xpack.security.deprecations.kibanaDashboardOnlyUser.unknownErrorMessage', + { + defaultMessage: + 'Failed to perform deprecation check. Check Kibana logs for more details.', + } + ), + correctiveActions: { + manualSteps: [ + i18n.translate( + 'xpack.security.deprecations.kibanaDashboardOnlyUser.unknownErrorCorrectiveAction', + { defaultMessage: 'Check Kibana logs for more details.' } + ), + ], + }, + }, + ]; +} diff --git a/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts b/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts new file mode 100644 index 0000000000000..da728b12fca91 --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/kibana_user_role.test.ts @@ -0,0 +1,328 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; +import type { SecurityRoleMapping, SecurityUser } from '@elastic/elasticsearch/api/types'; + +import type { PackageInfo, RegisterDeprecationsConfig } from 'src/core/server'; +import { + deprecationsServiceMock, + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsClientMock, +} from 'src/core/server/mocks'; + +import { licenseMock } from '../../common/licensing/index.mock'; +import { securityMock } from '../mocks'; +import { registerKibanaUserRoleDeprecation } from './kibana_user_role'; + +function getDepsMock() { + return { + logger: loggingSystemMock.createLogger(), + deprecationsService: deprecationsServiceMock.createSetupContract(), + license: licenseMock.create(), + packageInfo: { + branch: 'some-branch', + buildSha: 'sha', + dist: true, + version: '8.0.0', + buildNum: 1, + } as PackageInfo, + }; +} + +function getContextMock() { + return { + esClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), + }; +} + +function createMockUser(user: Partial = {}) { + return { enabled: true, username: 'userA', roles: ['roleA'], metadata: {}, ...user }; +} + +function createMockRoleMapping(mapping: Partial = {}) { + return { enabled: true, roles: ['roleA'], rules: {}, metadata: {}, ...mapping }; +} + +describe('Kibana Dashboard Only User role deprecations', () => { + let mockDeps: ReturnType; + let mockContext: ReturnType; + let deprecationHandler: RegisterDeprecationsConfig; + beforeEach(() => { + mockContext = getContextMock(); + mockDeps = getDepsMock(); + registerKibanaUserRoleDeprecation(mockDeps); + + expect(mockDeps.deprecationsService.registerDeprecations).toHaveBeenCalledTimes(1); + deprecationHandler = mockDeps.deprecationsService.registerDeprecations.mock.calls[0][0]; + }); + + it('does not return any deprecations if security is not enabled', async () => { + mockDeps.license.isEnabled.mockReturnValue(false); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toEqual([]); + expect(mockContext.esClient.asCurrentUser.security.getUser).not.toHaveBeenCalled(); + expect(mockContext.esClient.asCurrentUser.security.getRoleMapping).not.toHaveBeenCalled(); + }); + + it('does not return any deprecations if none of the users and role mappings has a kibana user role', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: { enabled: true, roles: ['roleA'], rules: {}, metadata: {} }, + }, + }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toEqual([]); + }); + + it('returns deprecations even if cannot retrieve users due to permission error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 403, body: {} })) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Make sure you have a \\"manage_security\\" cluster privilege assigned.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/kibana/some-branch/xpack-security.html#_required_permissions_7", + "level": "fetch_error", + "message": "You do not have enough permissions to fix this deprecation.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns deprecations even if cannot retrieve users due to unknown error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 500, body: {} })) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Check Kibana logs for more details.", + ], + }, + "deprecationType": "feature", + "level": "fetch_error", + "message": "Failed to perform deprecation check. Check Kibana logs for more details.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns deprecations even if cannot retrieve role mappings due to permission error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 403, body: {} })) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Make sure you have a \\"manage_security\\" cluster privilege assigned.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/kibana/some-branch/xpack-security.html#_required_permissions_7", + "level": "fetch_error", + "message": "You do not have enough permissions to fix this deprecation.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns deprecations even if cannot retrieve role mappings due to unknown error', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 500, body: {} })) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Check Kibana logs for more details.", + ], + }, + "deprecationType": "feature", + "level": "fetch_error", + "message": "Failed to perform deprecation check. Check Kibana logs for more details.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns only user-related deprecations', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ + body: { + userA: createMockUser({ username: 'userA', roles: ['roleA'] }), + userB: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_user'] }), + userC: createMockUser({ username: 'userC', roles: ['roleC'] }), + userD: createMockUser({ username: 'userD', roles: ['kibana_user'] }), + }, + }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "api": Object { + "method": "POST", + "path": "/internal/security/deprecations/kibana_user_role/_fix_users", + }, + "manualSteps": Array [ + "Remove the \\"kibana_user\\" role from all users and add the \\"kibana_admin\\" role. The affected users are: userB, userD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns only role-mapping-related deprecations', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: createMockRoleMapping({ roles: ['roleA'] }), + mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_user'] }), + mappingC: createMockRoleMapping({ roles: ['roleC'] }), + mappingD: createMockRoleMapping({ roles: ['kibana_user'] }), + }, + }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "api": Object { + "method": "POST", + "path": "/internal/security/deprecations/kibana_user_role/_fix_role_mappings", + }, + "manualSteps": Array [ + "Remove the \\"kibana_user\\" role from all role mappings and add the \\"kibana_admin\\" role. The affected role mappings are: mappingB, mappingD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); + + it('returns both user-related and role-mapping-related deprecations', async () => { + mockContext.esClient.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ + body: { + userA: createMockUser({ username: 'userA', roles: ['roleA'] }), + userB: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_user'] }), + userC: createMockUser({ username: 'userC', roles: ['roleC'] }), + userD: createMockUser({ username: 'userD', roles: ['kibana_user'] }), + }, + }) + ); + + mockContext.esClient.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: createMockRoleMapping({ roles: ['roleA'] }), + mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_user'] }), + mappingC: createMockRoleMapping({ roles: ['roleC'] }), + mappingD: createMockRoleMapping({ roles: ['kibana_user'] }), + }, + }) + ); + + await expect(deprecationHandler.getDeprecations(mockContext)).resolves.toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "api": Object { + "method": "POST", + "path": "/internal/security/deprecations/kibana_user_role/_fix_users", + }, + "manualSteps": Array [ + "Remove the \\"kibana_user\\" role from all users and add the \\"kibana_admin\\" role. The affected users are: userB, userD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + Object { + "correctiveActions": Object { + "api": Object { + "method": "POST", + "path": "/internal/security/deprecations/kibana_user_role/_fix_role_mappings", + }, + "manualSteps": Array [ + "Remove the \\"kibana_user\\" role from all role mappings and add the \\"kibana_admin\\" role. The affected role mappings are: mappingB, mappingD.", + ], + }, + "deprecationType": "feature", + "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/some-branch/built-in-roles.html", + "level": "warning", + "message": "Use the \\"kibana_admin\\" role to grant access to all Kibana features in all spaces.", + "title": "The \\"kibana_user\\" role is deprecated", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/security/server/deprecations/kibana_user_role.ts b/x-pack/plugins/security/server/deprecations/kibana_user_role.ts new file mode 100644 index 0000000000000..d659ea273f05f --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/kibana_user_role.ts @@ -0,0 +1,238 @@ +/* + * 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 { + SecurityGetRoleMappingResponse, + SecurityGetUserResponse, +} from '@elastic/elasticsearch/api/types'; + +import { i18n } from '@kbn/i18n'; +import type { + DeprecationsDetails, + DeprecationsServiceSetup, + ElasticsearchClient, + Logger, + PackageInfo, +} from 'src/core/server'; + +import type { SecurityLicense } from '../../common'; +import { getDetailedErrorMessage, getErrorStatusCode } from '../errors'; + +export const KIBANA_USER_ROLE_NAME = 'kibana_user'; +export const KIBANA_ADMIN_ROLE_NAME = 'kibana_admin'; + +interface Deps { + deprecationsService: DeprecationsServiceSetup; + license: SecurityLicense; + logger: Logger; + packageInfo: PackageInfo; +} + +function getDeprecationTitle() { + return i18n.translate('xpack.security.deprecations.kibanaUser.deprecationTitle', { + defaultMessage: 'The "{userRoleName}" role is deprecated', + values: { userRoleName: KIBANA_USER_ROLE_NAME }, + }); +} + +function getDeprecationMessage() { + return i18n.translate('xpack.security.deprecations.kibanaUser.deprecationMessage', { + defaultMessage: + 'Use the "{adminRoleName}" role to grant access to all Kibana features in all spaces.', + values: { adminRoleName: KIBANA_ADMIN_ROLE_NAME }, + }); +} + +export const registerKibanaUserRoleDeprecation = ({ + deprecationsService, + logger, + license, + packageInfo, +}: Deps) => { + deprecationsService.registerDeprecations({ + getDeprecations: async (context) => { + // Nothing to do if security is disabled + if (!license.isEnabled()) { + return []; + } + + return [ + ...(await getUsersDeprecations(context.esClient.asCurrentUser, logger, packageInfo)), + ...(await getRoleMappingsDeprecations(context.esClient.asCurrentUser, logger, packageInfo)), + ]; + }, + }); +}; + +async function getUsersDeprecations( + client: ElasticsearchClient, + logger: Logger, + packageInfo: PackageInfo +): Promise { + let users: SecurityGetUserResponse; + try { + users = (await client.security.getUser()).body; + } catch (err) { + if (getErrorStatusCode(err) === 403) { + logger.warn( + `Failed to retrieve users when checking for deprecations: the "manage_security" cluster privilege is required.` + ); + } else { + logger.error( + `Failed to retrieve users when checking for deprecations, unexpected error: ${getDetailedErrorMessage( + err + )}.` + ); + } + return deprecationError(packageInfo, err); + } + + const usersWithKibanaUserRole = Object.values(users) + .filter((user) => user.roles.includes(KIBANA_USER_ROLE_NAME)) + .map((user) => user.username); + if (usersWithKibanaUserRole.length === 0) { + return []; + } + + return [ + { + title: getDeprecationTitle(), + message: getDeprecationMessage(), + level: 'warning', + deprecationType: 'feature', + documentationUrl: `https://www.elastic.co/guide/en/elasticsearch/reference/${packageInfo.branch}/built-in-roles.html`, + correctiveActions: { + api: { + method: 'POST', + path: '/internal/security/deprecations/kibana_user_role/_fix_users', + }, + manualSteps: [ + i18n.translate( + 'xpack.security.deprecations.kibanaUser.usersDeprecationCorrectiveAction', + { + defaultMessage: + 'Remove the "{userRoleName}" role from all users and add the "{adminRoleName}" role. The affected users are: {users}.', + values: { + userRoleName: KIBANA_USER_ROLE_NAME, + adminRoleName: KIBANA_ADMIN_ROLE_NAME, + users: usersWithKibanaUserRole.join(', '), + }, + } + ), + ], + }, + }, + ]; +} + +async function getRoleMappingsDeprecations( + client: ElasticsearchClient, + logger: Logger, + packageInfo: PackageInfo +): Promise { + let roleMappings: SecurityGetRoleMappingResponse; + try { + roleMappings = (await client.security.getRoleMapping()).body; + } catch (err) { + if (getErrorStatusCode(err) === 403) { + logger.warn( + `Failed to retrieve role mappings when checking for deprecations: the "manage_security" cluster privilege is required.` + ); + } else { + logger.error( + `Failed to retrieve role mappings when checking for deprecations, unexpected error: ${getDetailedErrorMessage( + err + )}.` + ); + } + return deprecationError(packageInfo, err); + } + + const roleMappingsWithKibanaUserRole = Object.entries(roleMappings) + .filter(([, roleMapping]) => roleMapping.roles.includes(KIBANA_USER_ROLE_NAME)) + .map(([mappingName]) => mappingName); + if (roleMappingsWithKibanaUserRole.length === 0) { + return []; + } + + return [ + { + title: getDeprecationTitle(), + message: getDeprecationMessage(), + level: 'warning', + deprecationType: 'feature', + documentationUrl: `https://www.elastic.co/guide/en/elasticsearch/reference/${packageInfo.branch}/built-in-roles.html`, + correctiveActions: { + api: { + method: 'POST', + path: '/internal/security/deprecations/kibana_user_role/_fix_role_mappings', + }, + manualSteps: [ + i18n.translate( + 'xpack.security.deprecations.kibanaUser.roleMappingsDeprecationCorrectiveAction', + { + defaultMessage: + 'Remove the "{userRoleName}" role from all role mappings and add the "{adminRoleName}" role. The affected role mappings are: {roleMappings}.', + values: { + userRoleName: KIBANA_USER_ROLE_NAME, + adminRoleName: KIBANA_ADMIN_ROLE_NAME, + roleMappings: roleMappingsWithKibanaUserRole.join(', '), + }, + } + ), + ], + }, + }, + ]; +} + +function deprecationError(packageInfo: PackageInfo, error: Error): DeprecationsDetails[] { + const title = getDeprecationTitle(); + + if (getErrorStatusCode(error) === 403) { + return [ + { + title, + level: 'fetch_error', + deprecationType: 'feature', + message: i18n.translate('xpack.security.deprecations.kibanaUser.forbiddenErrorMessage', { + defaultMessage: 'You do not have enough permissions to fix this deprecation.', + }), + documentationUrl: `https://www.elastic.co/guide/en/kibana/${packageInfo.branch}/xpack-security.html#_required_permissions_7`, + correctiveActions: { + manualSteps: [ + i18n.translate( + 'xpack.security.deprecations.kibanaUser.forbiddenErrorCorrectiveAction', + { + defaultMessage: + 'Make sure you have a "manage_security" cluster privilege assigned.', + } + ), + ], + }, + }, + ]; + } + + return [ + { + title, + level: 'fetch_error', + deprecationType: 'feature', + message: i18n.translate('xpack.security.deprecations.kibanaUser.unknownErrorMessage', { + defaultMessage: 'Failed to perform deprecation check. Check Kibana logs for more details.', + }), + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.security.deprecations.kibanaUser.unknownErrorCorrectiveAction', { + defaultMessage: 'Check Kibana logs for more details.', + }), + ], + }, + }, + ]; +} diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 98e77038f168a..bf540d5d4ddc8 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -27,9 +27,8 @@ import type { import type { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import type { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import type { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import type { SecurityLicense } from '../common/licensing'; +import type { AuthenticatedUser, PrivilegeDeprecationsService, SecurityLicense } from '../common'; import { SecurityLicenseService } from '../common/licensing'; -import type { AuthenticatedUser, PrivilegeDeprecationsService } from '../common/model'; import type { AnonymousAccessServiceStart } from './anonymous_access'; import { AnonymousAccessService } from './anonymous_access'; import type { AuditServiceSetup } from './audit'; @@ -43,7 +42,11 @@ import type { AuthorizationServiceSetup, AuthorizationServiceSetupInternal } fro import { AuthorizationService } from './authorization'; import type { ConfigSchema, ConfigType } from './config'; import { createConfig } from './config'; -import { getPrivilegeDeprecationsService } from './deprecations'; +import { + getPrivilegeDeprecationsService, + registerKibanaDashboardOnlyRoleDeprecation, + registerKibanaUserRoleDeprecation, +} from './deprecations'; import { ElasticsearchService } from './elasticsearch'; import type { SecurityFeatureUsageServiceStart } from './feature_usage'; import { SecurityFeatureUsageService } from './feature_usage'; @@ -290,6 +293,8 @@ export class SecurityPlugin getSpacesService: () => spaces?.spacesService, }); + this.registerDeprecations(core, license); + defineRoutes({ router: core.http.createRouter(), basePath: core.http.basePath, @@ -414,4 +419,20 @@ export class SecurityPlugin this.authorizationService.stop(); this.sessionManagementService.stop(); } + + private registerDeprecations(core: CoreSetup, license: SecurityLicense) { + const logger = this.logger.get('deprecations'); + registerKibanaDashboardOnlyRoleDeprecation({ + deprecationsService: core.deprecations, + license, + logger, + packageInfo: this.initializerContext.env.packageInfo, + }); + registerKibanaUserRoleDeprecation({ + deprecationsService: core.deprecations, + license, + logger, + packageInfo: this.initializerContext.env.packageInfo, + }); + } } diff --git a/x-pack/plugins/security/server/routes/deprecations/index.ts b/x-pack/plugins/security/server/routes/deprecations/index.ts new file mode 100644 index 0000000000000..cbc186ed2e925 --- /dev/null +++ b/x-pack/plugins/security/server/routes/deprecations/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { RouteDefinitionParams } from '../'; +import { defineKibanaUserRoleDeprecationRoutes } from './kibana_user_role'; + +export function defineDeprecationsRoutes(params: RouteDefinitionParams) { + defineKibanaUserRoleDeprecationRoutes(params); +} diff --git a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts new file mode 100644 index 0000000000000..b2ae2543bd652 --- /dev/null +++ b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.test.ts @@ -0,0 +1,283 @@ +/* + * 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 { errors } from '@elastic/elasticsearch'; +import type { SecurityRoleMapping, SecurityUser } from '@elastic/elasticsearch/api/types'; + +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; +import type { RequestHandler, RouteConfig } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; + +import { securityMock } from '../../mocks'; +import type { SecurityRequestHandlerContext, SecurityRouter } from '../../types'; +import { routeDefinitionParamsMock } from '../index.mock'; +import { defineKibanaUserRoleDeprecationRoutes } from './kibana_user_role'; + +function createMockUser(user: Partial = {}) { + return { enabled: true, username: 'userA', roles: ['roleA'], metadata: {}, ...user }; +} + +function createMockRoleMapping(mapping: Partial = {}) { + return { enabled: true, roles: ['roleA'], rules: {}, metadata: {}, ...mapping }; +} + +describe('Kibana user deprecation routes', () => { + let router: jest.Mocked; + let mockContext: DeeplyMockedKeys; + beforeEach(() => { + const routeParamsMock = routeDefinitionParamsMock.create(); + router = routeParamsMock.router; + + mockContext = { + core: coreMock.createRequestHandlerContext(), + licensing: { license: { check: jest.fn().mockReturnValue({ state: 'valid' }) } }, + } as any; + + defineKibanaUserRoleDeprecationRoutes(routeParamsMock); + }); + + describe('Users with Kibana user role', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + beforeEach(() => { + const [fixUsersRouteConfig, fixUsersRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/internal/security/deprecations/kibana_user_role/_fix_users' + )!; + + routeConfig = fixUsersRouteConfig; + routeHandler = fixUsersRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it('fails if cannot retrieve users', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockRejectedValue( + new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') }) + ) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 500 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).not.toHaveBeenCalled(); + }); + + it('fails if fails to update user', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ + body: { + userA: createMockUser({ username: 'userA', roles: ['roleA', 'kibana_user'] }), + userB: createMockUser({ username: 'userB', roles: ['kibana_user'] }), + }, + }) + ); + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser.mockRejectedValue( + new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') }) + ) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 500 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledTimes(1); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledWith({ + username: 'userA', + body: createMockUser({ username: 'userA', roles: ['roleA', 'kibana_admin'] }), + }); + }); + + it('does nothing if there are no users with Kibana user role', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ body: { userA: createMockUser() } }) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 200 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).not.toHaveBeenCalled(); + }); + + it('updates users with Kibana user role', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getUser.mockResolvedValue( + securityMock.createApiResponse({ + body: { + userA: createMockUser({ username: 'userA', roles: ['roleA'] }), + userB: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_user'] }), + userC: createMockUser({ username: 'userC', roles: ['roleC'] }), + userD: createMockUser({ username: 'userD', roles: ['kibana_user'] }), + userE: createMockUser({ + username: 'userE', + roles: ['kibana_user', 'kibana_admin', 'roleE'], + }), + }, + }) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 200 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledTimes(3); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledWith({ + username: 'userB', + body: createMockUser({ username: 'userB', roles: ['roleB', 'kibana_admin'] }), + }); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledWith({ + username: 'userD', + body: createMockUser({ username: 'userD', roles: ['kibana_admin'] }), + }); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putUser + ).toHaveBeenCalledWith({ + username: 'userE', + body: createMockUser({ username: 'userE', roles: ['kibana_admin', 'roleE'] }), + }); + }); + }); + + describe('Role mappings with Kibana user role', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + beforeEach(() => { + const [fixRoleMappingsRouteConfig, fixRoleMappingsRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => + path === '/internal/security/deprecations/kibana_user_role/_fix_role_mappings' + )!; + + routeConfig = fixRoleMappingsRouteConfig; + routeHandler = fixRoleMappingsRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it('fails if cannot retrieve role mappings', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockRejectedValue( + new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') }) + ) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 500 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).not.toHaveBeenCalled(); + }); + + it('fails if fails to update role mapping', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: createMockRoleMapping({ roles: ['roleA', 'kibana_user'] }), + mappingB: createMockRoleMapping({ roles: ['kibana_user'] }), + }, + }) + ); + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping.mockRejectedValue( + new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: new Error('Oh no') }) + ) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 500 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledTimes(1); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name: 'mappingA', + body: createMockRoleMapping({ roles: ['roleA', 'kibana_admin'] }), + }); + }); + + it('does nothing if there are no role mappings with Kibana user role', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ body: { mappingA: createMockRoleMapping() } }) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 200 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).not.toHaveBeenCalled(); + }); + + it('updates role mappings with Kibana user role', async () => { + mockContext.core.elasticsearch.client.asCurrentUser.security.getRoleMapping.mockResolvedValue( + securityMock.createApiResponse({ + body: { + mappingA: createMockRoleMapping({ roles: ['roleA'] }), + mappingB: createMockRoleMapping({ roles: ['roleB', 'kibana_user'] }), + mappingC: createMockRoleMapping({ roles: ['roleC'] }), + mappingD: createMockRoleMapping({ roles: ['kibana_user'] }), + mappingE: createMockRoleMapping({ roles: ['kibana_user', 'kibana_admin', 'roleE'] }), + }, + }) + ); + + await expect( + routeHandler(mockContext, httpServerMock.createKibanaRequest(), kibanaResponseFactory) + ).resolves.toEqual(expect.objectContaining({ status: 200 })); + + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledTimes(3); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name: 'mappingB', + body: createMockRoleMapping({ roles: ['roleB', 'kibana_admin'] }), + }); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name: 'mappingD', + body: createMockRoleMapping({ roles: ['kibana_admin'] }), + }); + expect( + mockContext.core.elasticsearch.client.asCurrentUser.security.putRoleMapping + ).toHaveBeenCalledWith({ + name: 'mappingE', + body: createMockRoleMapping({ roles: ['kibana_admin', 'roleE'] }), + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts new file mode 100644 index 0000000000000..21bb9db7329b6 --- /dev/null +++ b/x-pack/plugins/security/server/routes/deprecations/kibana_user_role.ts @@ -0,0 +1,145 @@ +/* + * 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 { + SecurityGetRoleMappingResponse, + SecurityGetUserResponse, +} from '@elastic/elasticsearch/api/types'; + +import type { RouteDefinitionParams } from '..'; +import { KIBANA_ADMIN_ROLE_NAME, KIBANA_USER_ROLE_NAME } from '../../deprecations'; +import { + getDetailedErrorMessage, + getErrorStatusCode, + wrapIntoCustomErrorResponse, +} from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +/** + * Defines routes required to handle `kibana_user` deprecation. + */ +export function defineKibanaUserRoleDeprecationRoutes({ router, logger }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/deprecations/kibana_user_role/_fix_users', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + let users: SecurityGetUserResponse; + try { + users = (await context.core.elasticsearch.client.asCurrentUser.security.getUser()).body; + } catch (err) { + if (getErrorStatusCode(err) === 403) { + logger.warn( + `Failed to retrieve users when checking for deprecations: the manage_security cluster privilege is required` + ); + } else { + logger.error( + `Failed to retrieve users when checking for deprecations, unexpected error: ${getDetailedErrorMessage( + err + )}` + ); + } + return response.customError(wrapIntoCustomErrorResponse(err)); + } + + const usersWithKibanaUserRole = Object.values(users).filter((user) => + user.roles.includes(KIBANA_USER_ROLE_NAME) + ); + + if (usersWithKibanaUserRole.length === 0) { + logger.debug(`No users with "${KIBANA_USER_ROLE_NAME}" role found.`); + } else { + logger.debug( + `The following users with "${KIBANA_USER_ROLE_NAME}" role found and will be migrated to "${KIBANA_ADMIN_ROLE_NAME}" role: ${usersWithKibanaUserRole + .map((user) => user.username) + .join(', ')}.` + ); + } + + for (const userToUpdate of usersWithKibanaUserRole) { + const roles = userToUpdate.roles.filter((role) => role !== KIBANA_USER_ROLE_NAME); + if (!roles.includes(KIBANA_ADMIN_ROLE_NAME)) { + roles.push(KIBANA_ADMIN_ROLE_NAME); + } + + try { + await context.core.elasticsearch.client.asCurrentUser.security.putUser({ + username: userToUpdate.username, + body: { ...userToUpdate, roles }, + }); + } catch (err) { + logger.error( + `Failed to update user "${userToUpdate.username}": ${getDetailedErrorMessage(err)}.` + ); + return response.customError(wrapIntoCustomErrorResponse(err)); + } + + logger.debug(`Successfully updated user "${userToUpdate.username}".`); + } + + return response.ok({ body: {} }); + }) + ); + + router.post( + { + path: '/internal/security/deprecations/kibana_user_role/_fix_role_mappings', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + let roleMappings: SecurityGetRoleMappingResponse; + try { + roleMappings = ( + await context.core.elasticsearch.client.asCurrentUser.security.getRoleMapping() + ).body; + } catch (err) { + logger.error(`Failed to retrieve role mappings: ${getDetailedErrorMessage(err)}.`); + return response.customError(wrapIntoCustomErrorResponse(err)); + } + + const roleMappingsWithKibanaUserRole = Object.entries(roleMappings).filter(([, mapping]) => + mapping.roles.includes(KIBANA_USER_ROLE_NAME) + ); + + if (roleMappingsWithKibanaUserRole.length === 0) { + logger.debug(`No role mappings with "${KIBANA_USER_ROLE_NAME}" role found.`); + } else { + logger.debug( + `The following role mappings with "${KIBANA_USER_ROLE_NAME}" role found and will be migrated to "${KIBANA_ADMIN_ROLE_NAME}" role: ${roleMappingsWithKibanaUserRole + .map(([mappingName]) => mappingName) + .join(', ')}.` + ); + } + + for (const [mappingNameToUpdate, mappingToUpdate] of roleMappingsWithKibanaUserRole) { + const roles = mappingToUpdate.roles.filter((role) => role !== KIBANA_USER_ROLE_NAME); + if (!roles.includes(KIBANA_ADMIN_ROLE_NAME)) { + roles.push(KIBANA_ADMIN_ROLE_NAME); + } + + try { + await context.core.elasticsearch.client.asCurrentUser.security.putRoleMapping({ + name: mappingNameToUpdate, + body: { ...mappingToUpdate, roles }, + }); + } catch (err) { + logger.error( + `Failed to update role mapping "${mappingNameToUpdate}": ${getDetailedErrorMessage( + err + )}.` + ); + return response.customError(wrapIntoCustomErrorResponse(err)); + } + + logger.debug(`Successfully updated role mapping "${mappingNameToUpdate}".`); + } + + return response.ok({ body: {} }); + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 851e70a357cf9..6785fe57c6b32 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -11,7 +11,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import type { HttpResources, IBasePath, Logger } from 'src/core/server'; import type { KibanaFeature } from '../../../features/server'; -import type { SecurityLicense } from '../../common/licensing'; +import type { SecurityLicense } from '../../common'; import type { AnonymousAccessServiceStart } from '../anonymous_access'; import type { InternalAuthenticationServiceStart } from '../authentication'; import type { AuthorizationServiceSetupInternal } from '../authorization'; @@ -23,6 +23,7 @@ import { defineAnonymousAccessRoutes } from './anonymous_access'; import { defineApiKeysRoutes } from './api_keys'; import { defineAuthenticationRoutes } from './authentication'; import { defineAuthorizationRoutes } from './authorization'; +import { defineDeprecationsRoutes } from './deprecations'; import { defineIndicesRoutes } from './indices'; import { defineRoleMappingRoutes } from './role_mapping'; import { defineSecurityCheckupGetStateRoutes } from './security_checkup'; @@ -58,6 +59,7 @@ export function defineRoutes(params: RouteDefinitionParams) { defineUsersRoutes(params); defineRoleMappingRoutes(params); defineViewRoutes(params); + defineDeprecationsRoutes(params); defineAnonymousAccessRoutes(params); defineSecurityCheckupGetStateRoutes(params); }