Skip to content
25 changes: 25 additions & 0 deletions x-pack/plugins/security/common/model/deprecations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import type { DeprecationsDetails, GetDeprecationsContext } from '../../../../../src/core/server';
import type { Role } from './role';

export interface PrivilegeDeprecationsRolesByFeatureIdResponse {
roles?: Role[];
errors?: DeprecationsDetails[];
}

export interface PrivilegeDeprecationsRolesByFeatureIdRequest {
context: GetDeprecationsContext;
featureId: string;
}
export interface PrivilegeDeprecationsServices {
getKibanaRolesByFeatureId: (
args: PrivilegeDeprecationsRolesByFeatureIdRequest
) => Promise<PrivilegeDeprecationsRolesByFeatureIdResponse>;
}
5 changes: 5 additions & 0 deletions x-pack/plugins/security/common/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ export {
RoleTemplate,
RoleMapping,
} from './role_mapping';
export {
PrivilegeDeprecationsRolesByFeatureIdRequest,
PrivilegeDeprecationsRolesByFeatureIdResponse,
PrivilegeDeprecationsServices,
} from './deprecations';
1 change: 1 addition & 0 deletions x-pack/plugins/security/server/authorization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export {
} from './authorization_service';
export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges';
export { CheckPrivilegesPayload } from './types';
export { transformElasticsearchRoleToRole, ElasticsearchRole } from './roles';
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/security/server/authorization/roles/index.ts
Original file line number Diff line number Diff line change
@@ -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';
12 changes: 12 additions & 0 deletions x-pack/plugins/security/server/deprecations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

/**
* getKibanaRolesByFeature
*/

export { getPrivilegeDeprecationsServices } from './privilege_deprecations';
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { SecurityLicense } from '../../common/licensing';
import type {
PrivilegeDeprecationsRolesByFeatureIdRequest,
PrivilegeDeprecationsRolesByFeatureIdResponse,
} from '../../common/model';
import { transformElasticsearchRoleToRole } from '../authorization';
import type { AuthorizationServiceSetupInternal, ElasticsearchRole } from '../authorization';
import { getDetailedErrorMessage, getErrorStatusCode } from '../errors';

export const getPrivilegeDeprecationsServices = (
authz: AuthorizationServiceSetupInternal,
license: SecurityLicense
) => {
const getKibanaRolesByFeatureId = async ({
context,
featureId,
}: PrivilegeDeprecationsRolesByFeatureIdRequest): Promise<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 @elastic/elasticsearch `XPackRole` type doesn't define `applications` and `transient_metadata`.
elasticsearchRole,
roleName,
authz.applicationName
)
);
} catch (e) {
const statusCode = getErrorStatusCode(e);
const isUnauthorized = statusCode === 403;
const message = isUnauthorized
? `You must have the 'manage_security' cluster privilege to fix role deprecations.`
Copy link
Copy Markdown
Contributor Author

@XavierM XavierM Sep 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to convert that to i18n and the other ones

: `Error retrieving roles for privilege deprecations: ${getDetailedErrorMessage(e)}`;

return {
errors: [
{
title: 'title',
level: 'fetch_error',
message,
correctiveActions: {
manualSteps: [
'A user with the "manage_security" cluster privilege is required to perform this check.',
],
},
},
],
};
}
return {
roles: kibanaRoles.filter((role) =>
role.kibana.find((privilege) => Object.hasOwnProperty.call(privilege.feature, featureId))
),
};
};
return Object.freeze({
getKibanaRolesByFeatureId,
});
};
3 changes: 3 additions & 0 deletions x-pack/plugins/security/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ function createSetupMock() {
},
registerSpacesService: jest.fn(),
license: licenseMock.create(),
privilegeDeprecationServices: {
getKibanaRolesByFeatureId: jest.fn(),
},
};
}

Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/security/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ describe('Security Plugin', () => {
"isEnabled": [Function],
"isLicenseAvailable": [Function],
},
"privilegeDeprecationServices": Object {
"getKibanaRolesByFeatureId": [Function],
},
}
`);
});
Expand Down
14 changes: 10 additions & 4 deletions x-pack/plugins/security/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import type { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server';
import type { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
import type { SecurityLicense } from '../common/licensing';
import { SecurityLicenseService } from '../common/licensing';
import type { AuthenticatedUser } from '../common/model';
import type { AuthenticatedUser, PrivilegeDeprecationsServices } from '../common/model';
import type { AnonymousAccessServiceStart } from './anonymous_access';
import { AnonymousAccessService } from './anonymous_access';
import type { AuditServiceSetup } from './audit';
Expand All @@ -44,6 +44,7 @@ import type { AuthorizationServiceSetup, AuthorizationServiceSetupInternal } fro
import { AuthorizationService } from './authorization';
import type { ConfigSchema, ConfigType } from './config';
import { createConfig } from './config';
import { getPrivilegeDeprecationsServices } from './deprecations';
import { ElasticsearchService } from './elasticsearch';
import type { SecurityFeatureUsageServiceStart } from './feature_usage';
import { SecurityFeatureUsageService } from './feature_usage';
Expand Down Expand Up @@ -85,6 +86,10 @@ export interface SecurityPluginSetup {
* Exposes services for audit logging.
*/
audit: AuditServiceSetup;
/**
* Exposes services to access kibana roles per feature id with the GetDeprecationsContext
*/
privilegeDeprecationServices: PrivilegeDeprecationsServices;
}

/**
Expand Down Expand Up @@ -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,
Expand All @@ -333,8 +336,11 @@ export class SecurityPlugin
this.authorizationSetup.checkSavedObjectsPrivilegesWithRequest,
mode: this.authorizationSetup.mode,
},

license,
privilegeDeprecationServices: getPrivilegeDeprecationsServices(
this.authorizationSetup,
license
),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
* 2.0.
*/

export { ElasticsearchRole, transformElasticsearchRoleToRole } from './elasticsearch_role';
export { ElasticsearchRole, transformElasticsearchRoleToRole } from '../../../../authorization';
export { getPutPayloadSchema, transformPutPayloadToElasticsearchRole } from './put_payload';
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ENABLE_CASE_CONNECTOR } from '../../cases/common';
import { metadataTransformPattern } from './endpoint/constants';

export const APP_ID = 'securitySolution';
export const CASES_FEATURE_ID = 'securitySolutionCases';
export const SERVER_APP_ID = 'siem';
export const APP_NAME = 'Security';
export const APP_ICON = 'securityAnalyticsApp';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { updateSecuritySolutionPrivileges } from '.';

describe('deprecations', () => {
describe('create cases privileges from siem privileges without cases sub-feature', () => {
test('should be empty if siem privileges is an empty array', () => {
expect(updateSecuritySolutionPrivileges([])).toMatchInlineSnapshot(`Object {}`);
});

test('siem privileges === ["all"]', () => {
expect(updateSecuritySolutionPrivileges(['all'])).toMatchInlineSnapshot(`
Object {
"securitySolutionCases": Array [
"all",
],
"siem": Array [
"all",
],
}
`);
});

test('siem privileges === ["read"]', () => {
expect(updateSecuritySolutionPrivileges(['read'])).toMatchInlineSnapshot(`
Object {
"securitySolutionCases": Array [
"read",
],
"siem": Array [
"read",
],
}
`);
});
});

describe('create cases privileges from siem privileges with cases sub-feature', () => {
test('should be empty if siem privileges is an empty array', () => {
expect(updateSecuritySolutionPrivileges([])).toMatchInlineSnapshot(`Object {}`);
});

test('siem privileges === ["minimal_all"]', () => {
expect(updateSecuritySolutionPrivileges(['minimal_all'])).toMatchInlineSnapshot(`
Object {
"siem": Array [
"all",
],
}
`);
});

test('siem privileges === ["minimal_all", "cases_read"]', () => {
expect(updateSecuritySolutionPrivileges(['minimal_all', 'cases_read']))
.toMatchInlineSnapshot(`
Object {
"securitySolutionCases": Array [
"read",
],
"siem": Array [
"all",
],
}
`);
});

test('siem privileges === ["minimal_all", "cases_all"]', () => {
expect(updateSecuritySolutionPrivileges(['minimal_all', 'cases_all'])).toMatchInlineSnapshot(`
Object {
"securitySolutionCases": Array [
"all",
],
"siem": Array [
"all",
],
}
`);
});

test('siem privileges === ["minimal_read"]', () => {
expect(updateSecuritySolutionPrivileges(['minimal_read'])).toMatchInlineSnapshot(`
Object {
"siem": Array [
"read",
],
}
`);
});

test('siem privileges === ["minimal_read", "cases_read"]', () => {
expect(updateSecuritySolutionPrivileges(['minimal_read', 'cases_read']))
.toMatchInlineSnapshot(`
Object {
"securitySolutionCases": Array [
"read",
],
"siem": Array [
"read",
],
}
`);
});

test('siem privileges === ["minimal_read", "cases_all"]', () => {
expect(updateSecuritySolutionPrivileges(['minimal_read', 'cases_all']))
.toMatchInlineSnapshot(`
Object {
"securitySolutionCases": Array [
"all",
],
"siem": Array [
"read",
],
}
`);
});
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's possible to get in the following state:

image

image

Could we add a test for

[minimal_all, cases_read, cases_all]

Copy link
Copy Markdown
Contributor Author

@XavierM XavierM Sep 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also imagine we can have [minimal_read, cases_read, cases_all]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add them just in case

Loading