Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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 { GetDeprecationsContext, IScopedClusterClient, CoreSetup } from '@kbn/core/server';
import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks';
import { getDeprecationsInfo } from './apm_user_role';
import { SecurityPluginSetup } from '@kbn/security-plugin/server';

let context: GetDeprecationsContext;
let esClient: jest.Mocked<IScopedClusterClient>;
const core = { docLinks: { version: 'main' } } as unknown as CoreSetup;
const logger = loggingSystemMock.createLogger();
const security = { license: { isEnabled: () => true } } as unknown as SecurityPluginSetup;

describe('apm_user deprecation', () => {
beforeEach(async () => {
esClient = elasticsearchServiceMock.createScopedClusterClient();
esClient.asCurrentUser.security.getUser = jest.fn().mockResolvedValue({
xyz: { username: 'normal_user', roles: ['data_analyst'] },
});
esClient.asCurrentUser.security.getRoleMapping = jest.fn().mockResolvedValue({});

context = { esClient } as unknown as GetDeprecationsContext;
});

test('logs no deprecations when setup has no issues', async () => {
expect(await getDeprecationsInfo(context, core, { logger, security })).toMatchInlineSnapshot(
`Array []`
);
});

describe('users assigned to a removed role', () => {
test('logs a deprecation when a user was found with a removed apm_user role', async () => {
esClient.asCurrentUser.security.getUser = jest.fn().mockResolvedValue({
reportron: {
username: 'reportron',
roles: ['kibana_admin', 'apm_user'],
},
});

expect(await getDeprecationsInfo(context, core, { logger, security })).toMatchSnapshot();
});
});

describe('roles mapped to a removed role', () => {
test('logs a deprecation when a role was found that maps to the removed apm_user role', async () => {
esClient.asCurrentUser.security.getRoleMapping = jest
.fn()
.mockResolvedValue({ dungeon_master: { roles: ['apm_user'] } });

expect(await getDeprecationsInfo(context, core, { logger, security })).toMatchSnapshot();
});
});

describe('check deprecations when security is disabled', () => {
test('logs no deprecations', async () => {
expect(
await getDeprecationsInfo(context, core, { logger, security: undefined })
).toMatchInlineSnapshot(`Array []`);
});
});

it('insufficient permissions', async () => {
const permissionsError = new Error('you shall not pass');
(permissionsError as unknown as { statusCode: number }).statusCode = 403;
esClient.asCurrentUser.security.getUser = jest.fn().mockRejectedValue(permissionsError);
esClient.asCurrentUser.security.getRoleMapping = jest.fn().mockRejectedValue(permissionsError);

expect(await getDeprecationsInfo(context, core, { logger, security })).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/main/xpack-security.html#_required_permissions_7",
"level": "fetch_error",
"message": "You do not have enough permissions to fix this deprecation.",
"title": "The \\"apm_user\\" role has been removed: check user roles",
},
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/main/xpack-security.html#_required_permissions_7",
"level": "fetch_error",
"message": "You do not have enough permissions to fix this deprecation.",
"title": "The \\"apm_user\\" role has been removed: check role mappings",
},
]
`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* 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 {
SecurityGetRoleMappingResponse,
SecurityGetUserResponse,
} from '@elastic/elasticsearch/lib/api/types';
import type {
CoreSetup,
DeprecationsDetails,
DocLinksServiceSetup,
ElasticsearchClient,
GetDeprecationsContext,
} from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import type { DeprecationApmDeps } from '.';
import { deprecations } from '../lib/deprecations';

const APM_USER_ROLE_NAME = 'apm_user';
const getKibanaPrivilegesDocumentationUrl = (branch: string) => {
return `https://www.elastic.co/guide/en/kibana/${branch}/kibana-privileges.html`;
};

export async function getDeprecationsInfo(
{ esClient }: GetDeprecationsContext,
core: CoreSetup,
apmDeps: DeprecationApmDeps
) {
const client = esClient.asCurrentUser;
const { docLinks } = core;
const { security } = apmDeps;

// Nothing to do if security is disabled
if (!security?.license.isEnabled()) {
return [];
}

return [
...(await getUsersDeprecations(client, apmDeps, docLinks)),
...(await getRoleMappingsDeprecations(client, apmDeps, docLinks)),
];
}

async function getUsersDeprecations(
client: ElasticsearchClient,
apmDeps: DeprecationApmDeps,
docLinks: DocLinksServiceSetup
): Promise<DeprecationsDetails[]> {
const strings = {
title: i18n.translate('xpack.apm.deprecations.apmUser.title', {
defaultMessage: `The "{apmUserRoleName}" role has been removed: check user roles`,
values: { apmUserRoleName: APM_USER_ROLE_NAME },
}),
message: i18n.translate('xpack.apm.deprecations.apmUser.description', {
defaultMessage: `The "{apmUserRoleName}" has been removed and this cluster has users with the removed role.`,
values: { apmUserRoleName: APM_USER_ROLE_NAME },
}),
manualSteps: (usersRoles: string) => [
i18n.translate('xpack.apm.deprecations.apmUser.manualStepOne', {
defaultMessage: `Go to Management > Security > Users to find users with the "{apmUserRoleName}" role.`,
values: { apmUserRoleName: APM_USER_ROLE_NAME },
}),
i18n.translate('xpack.apm.deprecations.apmUser.manualStepTwo', {
defaultMessage:
`Remove the "{apmUserRoleName}" role from all users and add the built-in "viewer" role.` +
` The affected users are: {usersRoles}.`,
values: { apmUserRoleName: APM_USER_ROLE_NAME, usersRoles },
}),
],
};

let users: SecurityGetUserResponse;
try {
users = await client.security.getUser();
} catch (err) {
const { logger } = apmDeps;
if (deprecations.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: ${deprecations.getDetailedErrorMessage(err)}.`
);
}
return deprecations.deprecationError(strings.title, err, docLinks);
}

const reportingUsers = Object.entries(users).reduce((userSet, current) => {
const [userName, user] = current;
const foundRole = user.roles.find(hasApmUserRole);
if (foundRole) {
userSet.push(`${userName}[${foundRole}]`);
}
return userSet;
}, [] as string[]);

if (reportingUsers.length === 0) {
return [];
}

return [
{
title: strings.title,
message: strings.message,
correctiveActions: { manualSteps: strings.manualSteps(reportingUsers.join(', ')) },
level: 'critical',
deprecationType: 'feature',
documentationUrl: getKibanaPrivilegesDocumentationUrl(docLinks.version),
},
];
}

async function getRoleMappingsDeprecations(
client: ElasticsearchClient,
apmDeps: DeprecationApmDeps,
docLinks: DocLinksServiceSetup
): Promise<DeprecationsDetails[]> {
const strings = {
title: i18n.translate('xpack.apm.deprecations.apmUserRoleMappings.title', {
defaultMessage: `The "{apmUserRoleName}" role has been removed: check role mappings`,
values: { apmUserRoleName: APM_USER_ROLE_NAME },
}),
message: i18n.translate('xpack.apm.deprecations.apmUser.description', {
defaultMessage: `The "{apmUserRoleName}" role has been removed.`,
values: { apmUserRoleName: APM_USER_ROLE_NAME },
}),
manualSteps: (roleMappings: string) => [
i18n.translate('xpack.apm.deprecations.apmUser.manualStepOne', {
defaultMessage: `Go to Management > Security > Roles to find roles with the "{apmUserRoleName}" role.`,
values: { apmUserRoleName: APM_USER_ROLE_NAME },
}),
i18n.translate('xpack.apm.deprecations.apmUserRoleMappings.manualStepFive', {
defaultMessage:
`Remove the "{apmUserRoleName}" role from all role mappings and add the built-in "viewer" role.` +
` The affected role mappings are: {roleMappings}.`,
values: { apmUserRoleName: APM_USER_ROLE_NAME, roleMappings },
}),
],
};

let roleMappings: SecurityGetRoleMappingResponse;
try {
roleMappings = await client.security.getRoleMapping();
} catch (err) {
const { logger } = apmDeps;
if (deprecations.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: ${deprecations.getDetailedErrorMessage(err)}.`
);
}
return deprecations.deprecationError(strings.title, err, docLinks);
}

const roleMappingsWithReportingRole: string[] = Object.entries(roleMappings).reduce(
(roleSet, current) => {
const [roleName, role] = current;
const foundMapping = role.roles?.find(hasApmUserRole);
if (foundMapping) {
roleSet.push(`${roleName}[${foundMapping}]`);
}
return roleSet;
},
[] as string[]
);

if (roleMappingsWithReportingRole.length === 0) {
return [];
}

return [
{
title: strings.title,
message: strings.message,
correctiveActions: {
manualSteps: strings.manualSteps(roleMappingsWithReportingRole.join(', ')),
},
level: 'critical',
deprecationType: 'feature',
documentationUrl: getKibanaPrivilegesDocumentationUrl(docLinks.version),
},
];
}

const hasApmUserRole = (role: string) => role === APM_USER_ROLE_NAME;
Loading