[SECURITY] Add Privilege deprecations services in security plugin (#113151) (#113955)

* non-working POC for privilege deprecations

* wip to be bale to deprecated sub feature case in security solutions

* finalyze deprecations of cases sub feature in security solutions

* only adding the deprecation servces in security

* add test + translation

* Update x-pack/plugins/security/server/deprecations/privilege_deprecations.ts

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>

* joe reviews

* renaming + double check

Co-authored-by: Larry Gregory <larry.gregory@elastic.co>
Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Co-authored-by: Larry Gregory <larry.gregory@elastic.co>
Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2021-10-05 14:18:40 -04:00 committed by GitHub
parent ffeefb5f94
commit 748677ead0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 464 additions and 10 deletions

View file

@ -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 PrivilegeDeprecationsService {
getKibanaRolesByFeatureId: (
args: PrivilegeDeprecationsRolesByFeatureIdRequest
) => Promise<PrivilegeDeprecationsRolesByFeatureIdResponse>;
}

View file

@ -33,3 +33,8 @@ export {
RoleTemplate,
RoleMapping,
} from './role_mapping';
export {
PrivilegeDeprecationsRolesByFeatureIdRequest,
PrivilegeDeprecationsRolesByFeatureIdResponse,
PrivilegeDeprecationsService,
} from './deprecations';

View file

@ -13,3 +13,4 @@ export {
} from './authorization_service';
export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges';
export { CheckPrivilegesPayload } from './types';
export { transformElasticsearchRoleToRole, ElasticsearchRole } from './roles';

View file

@ -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<Role, 'name' | 'metadata' | 'transient_metadata'> & {
applications: Array<{

View file

@ -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';

View file

@ -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 { getPrivilegeDeprecationsService } from './privilege_deprecations';

View file

@ -0,0 +1,284 @@
/*
* 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 } from 'src/core/server';
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
import { getPrivilegeDeprecationsService } from '.';
import { licenseMock } from '../../common/licensing/index.mock';
const kibanaIndexName = '.a-kibana-index';
const application = `kibana-${kibanaIndexName}`;
describe('#getPrivilegeDeprecationsService', () => {
describe('#getKibanaRolesByFeatureId', () => {
const mockAsCurrentUser = elasticsearchServiceMock.createScopedClusterClient();
const mockLicense = licenseMock.create();
const mockLogger = loggingSystemMock.createLogger();
const authz = { applicationName: application };
const { getKibanaRolesByFeatureId } = getPrivilegeDeprecationsService(
authz,
mockLicense,
mockLogger
);
it('happy path to find siem roles with feature_siem privileges', async () => {
mockAsCurrentUser.asCurrentUser.security.getRole.mockResolvedValue(
elasticsearchServiceMock.createSuccessTransportRequestPromise({
first_role: {
cluster: [],
indices: [],
applications: [
{
application,
privileges: ['feature_siem.all', 'feature_siem.cases_read'],
resources: ['space:securitySolutions'],
},
],
run_as: [],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
})
);
const mockContext = {
esClient: mockAsCurrentUser,
savedObjectsClient: jest.fn(),
} as unknown as GetDeprecationsContext;
const resp = await getKibanaRolesByFeatureId({ context: mockContext, featureId: 'siem' });
expect(resp).toMatchInlineSnapshot(`
Object {
"roles": Array [
Object {
"_transform_error": Array [],
"_unrecognized_applications": Array [],
"elasticsearch": Object {
"cluster": Array [],
"indices": Array [],
"run_as": Array [],
},
"kibana": Array [
Object {
"base": Array [],
"feature": Object {
"siem": Array [
"all",
"cases_read",
],
},
"spaces": Array [
"securitySolutions",
],
},
],
"metadata": Object {
"_reserved": true,
},
"name": "first_role",
"transient_metadata": Object {
"enabled": true,
},
},
],
}
`);
});
it('happy path to find siem roles with feature_siem and feature_foo and feature_bar privileges', async () => {
mockAsCurrentUser.asCurrentUser.security.getRole.mockResolvedValue(
elasticsearchServiceMock.createSuccessTransportRequestPromise({
first_role: {
cluster: [],
indices: [],
applications: [
{
application,
privileges: [
'feature_foo.foo-privilege-1',
'feature_foo.foo-privilege-2',
'feature_bar.bar-privilege-1',
'feature_siem.all',
'feature_siem.cases_read',
],
resources: ['space:securitySolutions'],
},
],
run_as: [],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
})
);
const mockContext = {
esClient: mockAsCurrentUser,
savedObjectsClient: jest.fn(),
} as unknown as GetDeprecationsContext;
const resp = await getKibanaRolesByFeatureId({ context: mockContext, featureId: 'siem' });
expect(resp).toMatchInlineSnapshot(`
Object {
"roles": Array [
Object {
"_transform_error": Array [],
"_unrecognized_applications": Array [],
"elasticsearch": Object {
"cluster": Array [],
"indices": Array [],
"run_as": Array [],
},
"kibana": Array [
Object {
"base": Array [],
"feature": Object {
"bar": Array [
"bar-privilege-1",
],
"foo": Array [
"foo-privilege-1",
"foo-privilege-2",
],
"siem": Array [
"all",
"cases_read",
],
},
"spaces": Array [
"securitySolutions",
],
},
],
"metadata": Object {
"_reserved": true,
},
"name": "first_role",
"transient_metadata": Object {
"enabled": true,
},
},
],
}
`);
});
it('happy path to NOT find siem roles with and feature_foo and feature_bar privileges', async () => {
mockAsCurrentUser.asCurrentUser.security.getRole.mockResolvedValue(
elasticsearchServiceMock.createSuccessTransportRequestPromise({
first_role: {
cluster: [],
indices: [],
applications: [
{
application,
privileges: [
'feature_foo.foo-privilege-1',
'feature_foo.foo-privilege-2',
'feature_bar.bar-privilege-1',
],
resources: ['space:securitySolutions'],
},
],
run_as: [],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
})
);
const mockContext = {
esClient: mockAsCurrentUser,
savedObjectsClient: jest.fn(),
} as unknown as GetDeprecationsContext;
const resp = await getKibanaRolesByFeatureId({ context: mockContext, featureId: 'siem' });
expect(resp).toMatchInlineSnapshot(`
Object {
"roles": Array [],
}
`);
});
it('unhappy path with status code 400, we should have the attribute errors', async () => {
mockAsCurrentUser.asCurrentUser.security.getRole.mockResolvedValue(
elasticsearchServiceMock.createErrorTransportRequestPromise({
message: 'Test error',
statusCode: 400,
})
);
const mockContext = {
esClient: mockAsCurrentUser,
savedObjectsClient: jest.fn(),
} as unknown as GetDeprecationsContext;
const resp = await getKibanaRolesByFeatureId({ context: mockContext, featureId: 'siem' });
expect(resp).toMatchInlineSnapshot(`
Object {
"errors": Array [
Object {
"correctiveActions": Object {
"manualSteps": Array [
"A user with the \\"manage_security\\" cluster privilege is required to perform this check.",
],
},
"level": "fetch_error",
"message": "Error retrieving roles for privilege deprecations: Test error",
"title": "Error in privilege deprecations services",
},
],
}
`);
});
it('unhappy path with status code 403, we should have unauthorized message in the attribute errors', async () => {
mockAsCurrentUser.asCurrentUser.security.getRole.mockResolvedValue(
elasticsearchServiceMock.createErrorTransportRequestPromise({
message: 'Test error',
statusCode: 403,
})
);
const mockContext = {
esClient: mockAsCurrentUser,
savedObjectsClient: jest.fn(),
} as unknown as GetDeprecationsContext;
const resp = await getKibanaRolesByFeatureId({ context: mockContext, featureId: 'siem' });
expect(resp).toMatchInlineSnapshot(`
Object {
"errors": Array [
Object {
"correctiveActions": Object {
"manualSteps": Array [
"A user with the \\"manage_security\\" cluster privilege is required to perform this check.",
],
},
"level": "fetch_error",
"message": "You must have the 'manage_security' cluster privilege to fix role deprecations.",
"title": "Error in privilege deprecations services",
},
],
}
`);
});
});
});

View file

@ -0,0 +1,106 @@
/*
* 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 type { Logger } from 'src/core/server';
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 getPrivilegeDeprecationsService = (
authz: Pick<AuthorizationServiceSetupInternal, 'applicationName'>,
license: SecurityLicense,
logger: Logger
) => {
const getKibanaRolesByFeatureId = async ({
context,
featureId,
}: PrivilegeDeprecationsRolesByFeatureIdRequest): Promise<PrivilegeDeprecationsRolesByFeatureIdResponse> => {
// 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<string, ElasticsearchRole>
>();
kibanaRoles = Object.entries(elasticsearchRoles).map(([roleName, elasticsearchRole]) =>
transformElasticsearchRoleToRole(
// @ts-expect-error `SecurityIndicesPrivileges.names` expected to be `string[]`
elasticsearchRole,
roleName,
authz.applicationName
)
);
} catch (e) {
const statusCode = getErrorStatusCode(e);
const isUnauthorized = statusCode === 403;
const message = isUnauthorized
? i18n.translate('xpack.security.privilegeDeprecationsService.error.unauthorized.message', {
defaultMessage: `You must have the 'manage_security' cluster privilege to fix role deprecations.`,
})
: i18n.translate(
'xpack.security.privilegeDeprecationsService.error.retrievingRoles.message',
{
defaultMessage: `Error retrieving roles for privilege deprecations: {message}`,
values: {
message: getDetailedErrorMessage(e),
},
}
);
if (isUnauthorized) {
logger.warn(
`Failed to retrieve roles when checking for deprecations: the manage_security cluster privilege is required`
);
} else {
logger.error(
`Failed to retrieve roles when checking for deprecations, unexpected error: ${getDetailedErrorMessage(
e
)}`
);
}
return {
errors: [
{
title: i18n.translate('xpack.security.privilegeDeprecationsService.error.title', {
defaultMessage: `Error in privilege deprecations services`,
}),
level: 'fetch_error',
message,
correctiveActions: {
manualSteps: [
i18n.translate('xpack.security.privilegeDeprecationsService.manualSteps.message', {
defaultMessage:
'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,
});
};

View file

@ -28,6 +28,9 @@ function createSetupMock() {
},
registerSpacesService: jest.fn(),
license: licenseMock.create(),
privilegeDeprecationsService: {
getKibanaRolesByFeatureId: jest.fn(),
},
};
}

View file

@ -123,6 +123,9 @@ describe('Security Plugin', () => {
"isEnabled": [Function],
"isLicenseAvailable": [Function],
},
"privilegeDeprecationsService": Object {
"getKibanaRolesByFeatureId": [Function],
},
}
`);
});

View file

@ -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, PrivilegeDeprecationsService } 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 { getPrivilegeDeprecationsService } 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
*/
privilegeDeprecationsService: PrivilegeDeprecationsService;
}
/**
@ -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,12 @@ export class SecurityPlugin
this.authorizationSetup.checkSavedObjectsPrivilegesWithRequest,
mode: this.authorizationSetup.mode,
},
license,
privilegeDeprecationsService: getPrivilegeDeprecationsService(
this.authorizationSetup,
license,
this.logger.get('deprecations')
),
});
}

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export { ElasticsearchRole, transformElasticsearchRoleToRole } from './elasticsearch_role';
export { ElasticsearchRole, transformElasticsearchRoleToRole } from '../../../../authorization';
export { getPutPayloadSchema, transformPutPayloadToElasticsearchRole } from './put_payload';

View file

@ -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.