Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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({
foo: {
username: 'foo',
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": "Check for users assigned the deprecated \\"apm_user\\" role",
},
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": "Check for role mappings using the deprecated \\"apm_user\\" role",
},
]
`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* 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 [];
}

const [userDeprecations, roleMappingDeprecations] = await Promise.all([
getUsersDeprecations(client, apmDeps, docLinks),
getRoleMappingsDeprecations(client, apmDeps, docLinks),
]);

return [...userDeprecations, ...roleMappingDeprecations];
}

async function getUsersDeprecations(
client: ElasticsearchClient,
apmDeps: DeprecationApmDeps,
docLinks: DocLinksServiceSetup
): Promise<DeprecationsDetails[]> {
const title = i18n.translate('xpack.apm.deprecations.apmUser.title', {
defaultMessage: `Check for users assigned the deprecated "{apmUserRoleName}" role`,
values: { apmUserRoleName: APM_USER_ROLE_NAME },
});

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(title, err, docLinks);
}

const reportingUsers = Object.entries(users)
.filter(([_, user]) => user.roles.find(hasApmUserRole))
.map(([userName]) => userName);

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

return [
{
title,
message: i18n.translate('xpack.apm.deprecations.apmUser.description', {
defaultMessage: `The "{apmUserRoleName}" role has been deprecated. Remove the "{apmUserRoleName}" role from affected users in this cluster including: {users}`,
values: { apmUserRoleName: APM_USER_ROLE_NAME, users: reportingUsers.join() },
}),
correctiveActions: {
manualSteps: [
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.',
values: { apmUserRoleName: APM_USER_ROLE_NAME },
}),
],
},
level: 'critical',
deprecationType: 'feature',
documentationUrl: getKibanaPrivilegesDocumentationUrl(docLinks.version),
},
];
}

async function getRoleMappingsDeprecations(
client: ElasticsearchClient,
apmDeps: DeprecationApmDeps,
docLinks: DocLinksServiceSetup
): Promise<DeprecationsDetails[]> {
const title = i18n.translate('xpack.apm.deprecations.apmUserRoleMappings.title', {
defaultMessage: `Check for role mappings using the deprecated "{apmUserRoleName}" role`,
values: { apmUserRoleName: APM_USER_ROLE_NAME },
});

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(title, err, docLinks);
}

const roleMappingsWithReportingRole: string[] = Object.entries(roleMappings)
.filter(([_, role]) => role.roles?.find(hasApmUserRole))
?.map(([roleName]) => roleName);

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

return [
{
title,
message: i18n.translate('xpack.apm.deprecations.apmUserRoleMappings.description', {
defaultMessage: `The "{apmUserRoleName}" role has been deprecated. Remove the "{apmUserRoleName}" role from affected role mappings in this cluster including: {roles}`,
values: {
apmUserRoleName: APM_USER_ROLE_NAME,
roles: roleMappingsWithReportingRole.join(),
},
}),
correctiveActions: {
manualSteps: [
i18n.translate('xpack.apm.deprecations.apmUserRoleMappings.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.manualStepTwo', {
defaultMessage:
'Remove the "{apmUserRoleName}" role from all role mappings and add the built-in "viewer" role',
values: { apmUserRoleName: APM_USER_ROLE_NAME },
}),
],
},
level: 'critical',
deprecationType: 'feature',
documentationUrl: getKibanaPrivilegesDocumentationUrl(docLinks.version),
},
];
}

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