[Cases][ResponseOps] Splitting out cases privileges (#134860)

* Splitting out cases privs

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* Getting tests working

* Fixing import error

* Fixing tests

* Fixing role to only have delete permissions

* Extracting sub feature tests to trial license

* Removing deletion user from common tests

* Addressing feedback

* Fixing tests

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jonathan Buttner 2022-06-29 10:47:06 -04:00 committed by GitHub
parent 4b7b363e9c
commit c4c9c73668
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 728 additions and 107 deletions

View file

@ -156,6 +156,26 @@ export interface FeatureKibanaPrivileges {
* ```
*/
all?: readonly string[];
/**
* List of case owners which users should have push access to when granted this privilege.
* @example
* ```ts
* {
* push: ['securitySolution']
* }
* ```
*/
push?: readonly string[];
/**
* List of case owners which users should have create access to when granted this privilege.
* @example
* ```ts
* {
* create: ['securitySolution']
* }
* ```
*/
create?: readonly string[];
/**
* List of case owners which users should have read-only access to when granted this privilege.
* @example
@ -166,6 +186,26 @@ export interface FeatureKibanaPrivileges {
* ```
*/
read?: readonly string[];
/**
* List of case owners which users should have update access to when granted this privilege.
* @example
* ```ts
* {
* update: ['securitySolution']
* }
* ```
*/
update?: readonly string[];
/**
* List of case owners which users should have delete access to when granted this privilege.
* @example
* ```ts
* {
* delete: ['securitySolution']
* }
* ```
*/
delete?: readonly string[];
};
/**

View file

@ -530,7 +530,11 @@ Array [
],
"cases": Object {
"all": Array [],
"create": Array [],
"delete": Array [],
"push": Array [],
"read": Array [],
"update": Array [],
},
"catalogue": Array [
"dashboard",
@ -682,7 +686,11 @@ Array [
],
"cases": Object {
"all": Array [],
"create": Array [],
"delete": Array [],
"push": Array [],
"read": Array [],
"update": Array [],
},
"catalogue": Array [
"discover",
@ -893,7 +901,11 @@ Array [
],
"cases": Object {
"all": Array [],
"create": Array [],
"delete": Array [],
"push": Array [],
"read": Array [],
"update": Array [],
},
"catalogue": Array [
"visualize",
@ -1026,7 +1038,11 @@ Array [
],
"cases": Object {
"all": Array [],
"create": Array [],
"delete": Array [],
"push": Array [],
"read": Array [],
"update": Array [],
},
"catalogue": Array [
"dashboard",
@ -1178,7 +1194,11 @@ Array [
],
"cases": Object {
"all": Array [],
"create": Array [],
"delete": Array [],
"push": Array [],
"read": Array [],
"update": Array [],
},
"catalogue": Array [
"discover",
@ -1389,7 +1409,11 @@ Array [
],
"cases": Object {
"all": Array [],
"create": Array [],
"delete": Array [],
"push": Array [],
"read": Array [],
"update": Array [],
},
"catalogue": Array [
"visualize",

View file

@ -72,7 +72,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -137,7 +141,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -201,7 +209,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -267,7 +279,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -303,7 +319,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -360,7 +380,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-sub-type'],
create: ['cases-create-sub-type'],
read: ['cases-read-sub-type'],
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -402,7 +426,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -465,7 +493,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -522,7 +554,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-sub-type'],
create: ['cases-create-sub-type'],
read: ['cases-read-sub-type'],
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -564,7 +600,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -627,7 +667,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -685,7 +729,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-sub-type'],
create: ['cases-create-sub-type'],
read: ['cases-read-sub-type'],
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -730,7 +778,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type', 'cases-all-sub-type'],
create: ['cases-create-type', 'cases-create-sub-type'],
read: ['cases-read-type', 'cases-read-sub-type'],
update: ['cases-update-type', 'cases-update-sub-type'],
delete: ['cases-delete-type', 'cases-delete-sub-type'],
push: ['cases-push-type', 'cases-push-sub-type'],
},
ui: ['ui-action', 'ui-sub-type'],
},
@ -762,6 +814,10 @@ describe('featurePrivilegeIterator', () => {
cases: {
all: ['cases-all-sub-type'],
read: ['cases-read-type', 'cases-read-sub-type'],
create: ['cases-create-sub-type'],
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
},
ui: ['ui-action', 'ui-sub-type'],
},
@ -799,7 +855,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -899,7 +959,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -929,7 +993,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: [],
create: [],
read: ['cases-read-type'],
update: [],
delete: [],
push: [],
},
ui: ['ui-action'],
},
@ -965,7 +1033,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -1024,6 +1096,10 @@ describe('featurePrivilegeIterator', () => {
cases: {
all: ['cases-all-sub-type'],
read: ['cases-read-sub-type'],
create: ['cases-create-sub-type'],
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -1068,7 +1144,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type', 'cases-all-sub-type'],
create: ['cases-create-type', 'cases-create-sub-type'],
read: ['cases-read-type', 'cases-read-sub-type'],
update: ['cases-update-type', 'cases-update-sub-type'],
delete: ['cases-delete-type', 'cases-delete-sub-type'],
push: ['cases-push-type', 'cases-push-sub-type'],
},
ui: ['ui-action', 'ui-sub-type'],
},
@ -1256,7 +1336,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-sub-type'],
create: ['cases-create-sub-type'],
read: ['cases-read-sub-type'],
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -1301,7 +1385,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-sub-type'],
create: ['cases-create-sub-type'],
read: ['cases-read-sub-type'],
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -1332,7 +1420,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-sub-type'],
create: ['cases-create-sub-type'],
read: ['cases-read-sub-type'],
update: ['cases-update-sub-type'],
delete: ['cases-delete-sub-type'],
push: ['cases-push-sub-type'],
},
ui: ['ui-sub-type'],
},
@ -1368,7 +1460,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -1454,7 +1550,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: ['cases-all-type'],
create: ['cases-create-type'],
read: ['cases-read-type'],
update: ['cases-update-type'],
delete: ['cases-delete-type'],
push: ['cases-push-type'],
},
ui: ['ui-action'],
},
@ -1484,7 +1584,11 @@ describe('featurePrivilegeIterator', () => {
},
cases: {
all: [],
create: [],
read: ['cases-read-type'],
update: [],
delete: [],
push: [],
},
ui: ['ui-action'],
},

View file

@ -135,7 +135,20 @@ function mergeWithSubFeatures(
mergedConfig.cases = {
all: mergeArrays(mergedConfig.cases?.all ?? [], subFeaturePrivilege.cases?.all ?? []),
create: mergeArrays(
mergedConfig.cases?.create ?? [],
subFeaturePrivilege.cases?.create ?? []
),
read: mergeArrays(mergedConfig.cases?.read ?? [], subFeaturePrivilege.cases?.read ?? []),
update: mergeArrays(
mergedConfig.cases?.update ?? [],
subFeaturePrivilege.cases?.update ?? []
),
delete: mergeArrays(
mergedConfig.cases?.delete ?? [],
subFeaturePrivilege.cases?.delete ?? []
),
push: mergeArrays(mergedConfig.cases?.push ?? [], subFeaturePrivilege.cases?.push ?? []),
};
}
return mergedConfig;

View file

@ -74,6 +74,17 @@ const appCategorySchema = schema.object({
order: schema.maybe(schema.number()),
});
const casesSchemaObject = schema.maybe(
schema.object({
all: schema.maybe(casesSchema),
create: schema.maybe(casesSchema),
read: schema.maybe(casesSchema),
update: schema.maybe(casesSchema),
delete: schema.maybe(casesSchema),
push: schema.maybe(casesSchema),
})
);
const kibanaPrivilegeSchema = schema.object({
excludeFromBasePrivileges: schema.maybe(schema.boolean()),
requireAllSpaces: schema.maybe(schema.boolean()),
@ -98,12 +109,7 @@ const kibanaPrivilegeSchema = schema.object({
),
})
),
cases: schema.maybe(
schema.object({
all: schema.maybe(casesSchema),
read: schema.maybe(casesSchema),
})
),
cases: casesSchemaObject,
savedObject: schema.object({
all: schema.arrayOf(schema.string()),
read: schema.arrayOf(schema.string()),
@ -140,12 +146,7 @@ const kibanaIndependentSubFeaturePrivilegeSchema = schema.object({
),
})
),
cases: schema.maybe(
schema.object({
all: schema.maybe(casesSchema),
read: schema.maybe(casesSchema),
})
),
cases: casesSchemaObject,
api: schema.maybe(schema.arrayOf(schema.string())),
app: schema.maybe(schema.arrayOf(schema.string())),
savedObject: schema.object({

View file

@ -0,0 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`cases feature_privilege_builder within feature grants all privileges under feature with id observability 1`] = `
Array [
"cases:1.0.0-zeta1:observability/pushCase",
"cases:1.0.0-zeta1:observability/createCase",
"cases:1.0.0-zeta1:observability/createComment",
"cases:1.0.0-zeta1:observability/createConfiguration",
"cases:1.0.0-zeta1:observability/getCase",
"cases:1.0.0-zeta1:observability/getComment",
"cases:1.0.0-zeta1:observability/getTags",
"cases:1.0.0-zeta1:observability/getReporters",
"cases:1.0.0-zeta1:observability/getUserActions",
"cases:1.0.0-zeta1:observability/findConfigurations",
"cases:1.0.0-zeta1:observability/updateCase",
"cases:1.0.0-zeta1:observability/updateComment",
"cases:1.0.0-zeta1:observability/updateConfiguration",
"cases:1.0.0-zeta1:observability/deleteCase",
"cases:1.0.0-zeta1:observability/deleteComment",
]
`;
exports[`cases feature_privilege_builder within feature grants create privileges under feature with id securitySolution 1`] = `
Array [
"cases:1.0.0-zeta1:securitySolution/createCase",
"cases:1.0.0-zeta1:securitySolution/createComment",
"cases:1.0.0-zeta1:securitySolution/createConfiguration",
]
`;
exports[`cases feature_privilege_builder within feature grants delete privileges under feature with id securitySolution 1`] = `
Array [
"cases:1.0.0-zeta1:securitySolution/deleteCase",
"cases:1.0.0-zeta1:securitySolution/deleteComment",
]
`;
exports[`cases feature_privilege_builder within feature grants push privileges under feature with id obs 1`] = `
Array [
"cases:1.0.0-zeta1:obs/pushCase",
]
`;
exports[`cases feature_privilege_builder within feature grants read privileges under feature with id observability 1`] = `
Array [
"cases:1.0.0-zeta1:observability/getCase",
"cases:1.0.0-zeta1:observability/getComment",
"cases:1.0.0-zeta1:observability/getTags",
"cases:1.0.0-zeta1:observability/getReporters",
"cases:1.0.0-zeta1:observability/getUserActions",
"cases:1.0.0-zeta1:observability/findConfigurations",
]
`;
exports[`cases feature_privilege_builder within feature grants update privileges under feature with id observability 1`] = `
Array [
"cases:1.0.0-zeta1:observability/updateCase",
"cases:1.0.0-zeta1:observability/updateComment",
"cases:1.0.0-zeta1:observability/updateConfiguration",
]
`;

View file

@ -42,13 +42,20 @@ describe(`cases`, () => {
});
describe(`within feature`, () => {
it('grants `read` privileges under feature', () => {
it.each([
['all', 'observability'],
['push', 'obs'],
['create', 'securitySolution'],
['read', 'observability'],
['update', 'observability'],
['delete', 'securitySolution'],
])('grants %s privileges under feature with id %s', (operation, featureID) => {
const actions = new Actions(version);
const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions);
const privilege: FeatureKibanaPrivileges = {
cases: {
read: ['observability'],
[operation]: [featureID],
},
savedObject: {
@ -69,73 +76,19 @@ describe(`cases`, () => {
},
});
expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(`
Array [
"cases:1.0.0-zeta1:observability/getCase",
"cases:1.0.0-zeta1:observability/getComment",
"cases:1.0.0-zeta1:observability/getTags",
"cases:1.0.0-zeta1:observability/getReporters",
"cases:1.0.0-zeta1:observability/getUserActions",
"cases:1.0.0-zeta1:observability/findConfigurations",
]
`);
expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchSnapshot();
});
it('grants `all` privileges under feature', () => {
const actions = new Actions(version);
const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions);
const privilege: FeatureKibanaPrivileges = {
cases: {
all: ['security'],
},
savedObject: {
all: [],
read: [],
},
ui: [],
};
const feature = new KibanaFeature({
id: 'my-feature',
name: 'my-feature',
app: [],
category: { id: 'foo', label: 'foo' },
privileges: {
all: privilege,
read: privilege,
},
});
expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(`
Array [
"cases:1.0.0-zeta1:security/getCase",
"cases:1.0.0-zeta1:security/getComment",
"cases:1.0.0-zeta1:security/getTags",
"cases:1.0.0-zeta1:security/getReporters",
"cases:1.0.0-zeta1:security/getUserActions",
"cases:1.0.0-zeta1:security/findConfigurations",
"cases:1.0.0-zeta1:security/createCase",
"cases:1.0.0-zeta1:security/deleteCase",
"cases:1.0.0-zeta1:security/updateCase",
"cases:1.0.0-zeta1:security/pushCase",
"cases:1.0.0-zeta1:security/createComment",
"cases:1.0.0-zeta1:security/deleteComment",
"cases:1.0.0-zeta1:security/updateComment",
"cases:1.0.0-zeta1:security/createConfiguration",
"cases:1.0.0-zeta1:security/updateConfiguration",
]
`);
});
it('grants both `all` and `read` privileges under feature', () => {
it('grants all privileges under feature', () => {
const actions = new Actions(version);
const casesFeaturePrivilege = new FeaturePrivilegeCasesBuilder(actions);
const privilege: FeatureKibanaPrivileges = {
cases: {
all: ['security'],
create: ['security'],
update: ['obs'],
delete: ['security'],
read: ['obs'],
},
@ -159,27 +112,30 @@ describe(`cases`, () => {
expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(`
Array [
"cases:1.0.0-zeta1:security/pushCase",
"cases:1.0.0-zeta1:security/createCase",
"cases:1.0.0-zeta1:security/createComment",
"cases:1.0.0-zeta1:security/createConfiguration",
"cases:1.0.0-zeta1:security/getCase",
"cases:1.0.0-zeta1:security/getComment",
"cases:1.0.0-zeta1:security/getTags",
"cases:1.0.0-zeta1:security/getReporters",
"cases:1.0.0-zeta1:security/getUserActions",
"cases:1.0.0-zeta1:security/findConfigurations",
"cases:1.0.0-zeta1:security/createCase",
"cases:1.0.0-zeta1:security/deleteCase",
"cases:1.0.0-zeta1:security/updateCase",
"cases:1.0.0-zeta1:security/pushCase",
"cases:1.0.0-zeta1:security/createComment",
"cases:1.0.0-zeta1:security/deleteComment",
"cases:1.0.0-zeta1:security/updateComment",
"cases:1.0.0-zeta1:security/createConfiguration",
"cases:1.0.0-zeta1:security/updateConfiguration",
"cases:1.0.0-zeta1:security/deleteCase",
"cases:1.0.0-zeta1:security/deleteComment",
"cases:1.0.0-zeta1:obs/getCase",
"cases:1.0.0-zeta1:obs/getComment",
"cases:1.0.0-zeta1:obs/getTags",
"cases:1.0.0-zeta1:obs/getReporters",
"cases:1.0.0-zeta1:obs/getUserActions",
"cases:1.0.0-zeta1:obs/findConfigurations",
"cases:1.0.0-zeta1:obs/updateCase",
"cases:1.0.0-zeta1:obs/updateComment",
"cases:1.0.0-zeta1:obs/updateConfiguration",
]
`);
});
@ -214,36 +170,36 @@ describe(`cases`, () => {
expect(casesFeaturePrivilege.getActions(privilege, feature)).toMatchInlineSnapshot(`
Array [
"cases:1.0.0-zeta1:security/pushCase",
"cases:1.0.0-zeta1:security/createCase",
"cases:1.0.0-zeta1:security/createComment",
"cases:1.0.0-zeta1:security/createConfiguration",
"cases:1.0.0-zeta1:security/getCase",
"cases:1.0.0-zeta1:security/getComment",
"cases:1.0.0-zeta1:security/getTags",
"cases:1.0.0-zeta1:security/getReporters",
"cases:1.0.0-zeta1:security/getUserActions",
"cases:1.0.0-zeta1:security/findConfigurations",
"cases:1.0.0-zeta1:security/createCase",
"cases:1.0.0-zeta1:security/deleteCase",
"cases:1.0.0-zeta1:security/updateCase",
"cases:1.0.0-zeta1:security/pushCase",
"cases:1.0.0-zeta1:security/createComment",
"cases:1.0.0-zeta1:security/deleteComment",
"cases:1.0.0-zeta1:security/updateComment",
"cases:1.0.0-zeta1:security/createConfiguration",
"cases:1.0.0-zeta1:security/updateConfiguration",
"cases:1.0.0-zeta1:security/deleteCase",
"cases:1.0.0-zeta1:security/deleteComment",
"cases:1.0.0-zeta1:other-security/pushCase",
"cases:1.0.0-zeta1:other-security/createCase",
"cases:1.0.0-zeta1:other-security/createComment",
"cases:1.0.0-zeta1:other-security/createConfiguration",
"cases:1.0.0-zeta1:other-security/getCase",
"cases:1.0.0-zeta1:other-security/getComment",
"cases:1.0.0-zeta1:other-security/getTags",
"cases:1.0.0-zeta1:other-security/getReporters",
"cases:1.0.0-zeta1:other-security/getUserActions",
"cases:1.0.0-zeta1:other-security/findConfigurations",
"cases:1.0.0-zeta1:other-security/createCase",
"cases:1.0.0-zeta1:other-security/deleteCase",
"cases:1.0.0-zeta1:other-security/updateCase",
"cases:1.0.0-zeta1:other-security/pushCase",
"cases:1.0.0-zeta1:other-security/createComment",
"cases:1.0.0-zeta1:other-security/deleteComment",
"cases:1.0.0-zeta1:other-security/updateComment",
"cases:1.0.0-zeta1:other-security/createConfiguration",
"cases:1.0.0-zeta1:other-security/updateConfiguration",
"cases:1.0.0-zeta1:other-security/deleteCase",
"cases:1.0.0-zeta1:other-security/deleteComment",
"cases:1.0.0-zeta1:obs/getCase",
"cases:1.0.0-zeta1:obs/getComment",
"cases:1.0.0-zeta1:obs/getTags",

View file

@ -15,6 +15,9 @@ export type CasesSupportedOperations = typeof allOperations[number];
// if you add a value here you'll likely also need to make changes here:
// x-pack/plugins/cases/server/authorization/index.ts
const pushOperations = ['pushCase'] as const;
const createOperations = ['createCase', 'createComment', 'createConfiguration'] as const;
const readOperations = [
'getCase',
'getComment',
@ -23,19 +26,15 @@ const readOperations = [
'getUserActions',
'findConfigurations',
] as const;
const writeOperations = [
'createCase',
'deleteCase',
'updateCase',
'pushCase',
'createComment',
'deleteComment',
'updateComment',
'createConfiguration',
'updateConfiguration',
const updateOperations = ['updateCase', 'updateComment', 'updateConfiguration'] as const;
const deleteOperations = ['deleteCase', 'deleteComment'] as const;
const allOperations = [
...pushOperations,
...createOperations,
...readOperations,
...updateOperations,
...deleteOperations,
] as const;
const allOperations = [...readOperations, ...writeOperations] as const;
export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder {
public getActions(
@ -44,7 +43,7 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder {
): string[] {
const getCasesPrivilege = (
operations: readonly CasesSupportedOperations[],
owners: readonly string[]
owners: readonly string[] = []
) => {
return owners.flatMap((owner) =>
operations.map((operation) => this.actions.cases.get(owner, operation))
@ -52,8 +51,12 @@ export class FeaturePrivilegeCasesBuilder extends BaseFeaturePrivilegeBuilder {
};
return uniq([
...getCasesPrivilege(allOperations, privilegeDefinition.cases?.all ?? []),
...getCasesPrivilege(readOperations, privilegeDefinition.cases?.read ?? []),
...getCasesPrivilege(allOperations, privilegeDefinition.cases?.all),
...getCasesPrivilege(pushOperations, privilegeDefinition.cases?.push),
...getCasesPrivilege(createOperations, privilegeDefinition.cases?.create),
...getCasesPrivilege(readOperations, privilegeDefinition.cases?.read),
...getCasesPrivilege(updateOperations, privilegeDefinition.cases?.update),
...getCasesPrivilege(deleteOperations, privilegeDefinition.cases?.delete),
]);
}
}

View file

@ -33,7 +33,10 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
all: {
app: ['kibana'],
cases: {
all: ['securitySolutionFixture'],
create: ['securitySolutionFixture'],
read: ['securitySolutionFixture'],
update: ['securitySolutionFixture'],
push: ['securitySolutionFixture'],
},
savedObject: {
all: [],
@ -53,6 +56,31 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
ui: [],
},
},
subFeatures: [
{
name: 'Custom privileges',
privilegeGroups: [
{
groupType: 'independent',
privileges: [
{
name: 'Delete',
id: 'cases_delete',
includeIn: 'all',
cases: {
delete: ['securitySolutionFixture'],
},
savedObject: {
all: [],
read: [],
},
ui: [],
},
],
},
],
},
],
});
features.registerKibanaFeature({

View file

@ -95,6 +95,54 @@ export const securitySolutionOnlyAll: Role = {
},
};
export const securitySolutionOnlyDelete: Role = {
name: 'sec_only_delete',
privileges: {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
},
],
},
kibana: [
{
feature: {
securitySolutionFixture: ['cases_delete'],
actions: ['all'],
actionsSimulators: ['all'],
},
spaces: ['space1'],
},
],
},
};
export const securitySolutionOnlyNoDelete: Role = {
name: 'sec_only_no_delete',
privileges: {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
},
],
},
kibana: [
{
feature: {
securitySolutionFixture: ['minimal_all'],
actions: ['all'],
actionsSimulators: ['all'],
},
spaces: ['space1'],
},
],
},
};
export const securitySolutionOnlyRead: Role = {
name: 'sec_only_read',
privileges: {
@ -172,6 +220,8 @@ export const roles = [
globalRead,
securitySolutionOnlyAll,
securitySolutionOnlyRead,
securitySolutionOnlyDelete,
securitySolutionOnlyNoDelete,
observabilityOnlyAll,
observabilityOnlyRead,
testDisabledPluginAll,

View file

@ -17,6 +17,8 @@ import {
observabilityOnlyAllSpacesAll,
observabilityOnlyReadSpacesAll,
testDisabledPluginAll,
securitySolutionOnlyDelete,
securitySolutionOnlyNoDelete,
} from './roles';
import { User } from './types';
@ -38,6 +40,18 @@ export const secOnly: User = {
roles: [securitySolutionOnlyAll.name],
};
export const secOnlyDelete: User = {
username: 'sec_only_delete',
password: 'sec_only_delete',
roles: [securitySolutionOnlyDelete.name],
};
export const secOnlyNoDelete: User = {
username: 'sec_only_no_delete',
password: 'sec_only_no_delete',
roles: [securitySolutionOnlyNoDelete.name],
};
export const secOnlyRead: User = {
username: 'sec_only_read',
password: 'sec_only_read',
@ -84,6 +98,8 @@ export const users = [
superUser,
secOnly,
secOnlyRead,
secOnlyDelete,
secOnlyNoDelete,
obsOnly,
obsOnlyRead,
obsSec,

View file

@ -0,0 +1,323 @@
/*
* 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 { CASES_URL } from '@kbn/cases-plugin/common';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { getPostCaseRequest, postCommentUserReq } from '../../../common/lib/mock';
import {
deleteAllCaseItems,
createCase,
deleteCases,
createComment,
getCase,
superUserSpace1Auth,
findCases,
deleteComment,
getSpaceUrlPrefix,
updateComment,
deleteAllComments,
} from '../../../common/lib/utils';
import {
superUser,
secOnlyDelete,
secOnlyNoDelete,
} from '../../../common/lib/authentication/users';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
const supertestWithoutAuth = getService('supertestWithoutAuth');
const es = getService('es');
describe('user with deletion sub privilege', () => {
afterEach(async () => {
await deleteAllCaseItems(es);
});
describe('successful operations', () => {
it(`should delete a case`, async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{
user: superUser,
space: 'space1',
}
);
await deleteCases({
supertest: supertestWithoutAuth,
caseIDs: [postedCase.id],
expectedHttpCode: 204,
auth: { user: secOnlyDelete, space: 'space1' },
});
});
it(`should delete a case with a comment`, async () => {
const secCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{ user: superUser, space: 'space1' }
);
await createComment({
supertest: supertestWithoutAuth,
caseId: secCase.id,
params: postCommentUserReq,
auth: { user: superUser, space: 'space1' },
});
await deleteCases({
supertest: supertestWithoutAuth,
caseIDs: [secCase.id],
expectedHttpCode: 204,
auth: { user: secOnlyDelete, space: 'space1' },
});
});
it(`should delete a comment from the appropriate owner`, async () => {
const secCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{ user: superUser, space: 'space1' }
);
const commentResp = await createComment({
supertest: supertestWithoutAuth,
caseId: secCase.id,
params: postCommentUserReq,
auth: { user: superUser, space: 'space1' },
});
await deleteComment({
supertest: supertestWithoutAuth,
caseId: secCase.id,
commentId: commentResp.comments![0].id,
auth: { user: secOnlyDelete, space: 'space1' },
});
});
it(`should delete all comments from a case`, async () => {
const secCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{ user: superUser, space: 'space1' }
);
await createComment({
supertest: supertestWithoutAuth,
caseId: secCase.id,
params: postCommentUserReq,
auth: { user: superUser, space: 'space1' },
});
await createComment({
supertest: supertestWithoutAuth,
caseId: secCase.id,
params: postCommentUserReq,
auth: { user: superUser, space: 'space1' },
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: secCase.id,
auth: { user: secOnlyDelete, space: 'space1' },
});
});
});
describe('failed operations', () => {
describe('cases', () => {
for (const scenario of [{ user: secOnlyDelete, space: 'space1' }]) {
it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${
scenario.space
} - should NOT read a case`, async () => {
// super user creates a case at the appropriate space
await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{
user: superUser,
space: scenario.space,
}
);
// user should not be able to read cases at the appropriate space
await findCases({
supertest: supertestWithoutAuth,
auth: {
user: scenario.user,
space: scenario.space,
},
expectedHttpCode: 403,
});
});
}
it('should not get a case when the user does not have read permissions', async () => {
const newCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{
user: superUser,
space: 'space1',
}
);
for (const user of [secOnlyDelete]) {
await getCase({
supertest: supertestWithoutAuth,
caseId: newCase.id,
expectedHttpCode: 403,
auth: { user, space: 'space1' },
});
}
});
it(`should not create a case`, async () => {
await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
403,
{
user: secOnlyDelete,
space: 'space1',
}
);
});
it(`should create a case but not delete a case`, async () => {
const caseInfo = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{
user: secOnlyNoDelete,
space: 'space1',
}
);
await deleteCases({
supertest: supertestWithoutAuth,
caseIDs: [caseInfo.id],
expectedHttpCode: 403,
auth: { user: secOnlyNoDelete, space: 'space1' },
});
});
});
describe('comments', () => {
for (const scenario of [{ user: secOnlyDelete, space: 'space1' }]) {
it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${
scenario.space
} - should NOT read a comment`, async () => {
// super user creates a case and comment in the appropriate space
const caseInfo = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{ user: superUser, space: scenario.space }
);
await createComment({
supertest: supertestWithoutAuth,
auth: { user: superUser, space: scenario.space },
params: { ...postCommentUserReq, owner: 'securitySolutionFixture' },
caseId: caseInfo.id,
});
// user should not be able to read comments
await supertestWithoutAuth
.get(`${getSpaceUrlPrefix(scenario.space)}${CASES_URL}/${caseInfo.id}/comments/_find`)
.auth(scenario.user.username, scenario.user.password)
.expect(403);
});
}
it('should NOT update a comment', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
superUserSpace1Auth
);
const patchedCase = await createComment({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
params: postCommentUserReq,
auth: superUserSpace1Auth,
});
const newComment = 'Well I decided to update my comment. So what? Deal with it.';
await updateComment({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
req: {
...postCommentUserReq,
id: patchedCase.comments![0].id,
version: patchedCase.comments![0].version,
comment: newComment,
},
auth: { user: secOnlyDelete, space: 'space1' },
expectedHttpCode: 403,
});
});
it('should not create a comment', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
superUserSpace1Auth
);
await createComment({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
params: postCommentUserReq,
auth: { user: secOnlyDelete, space: 'space1' },
expectedHttpCode: 403,
});
});
it(`should create a comment but not delete a comment`, async () => {
const caseInfo = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{
user: secOnlyNoDelete,
space: 'space1',
}
);
const commentResp = await createComment({
supertest: supertestWithoutAuth,
caseId: caseInfo.id,
params: postCommentUserReq,
auth: { user: secOnlyNoDelete, space: 'space1' },
});
await deleteComment({
supertest: supertestWithoutAuth,
caseId: caseInfo.id,
commentId: commentResp.comments![0].id,
auth: { user: secOnlyNoDelete, space: 'space1' },
expectedHttpCode: 403,
});
});
});
});
});
};

View file

@ -23,6 +23,8 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => {
loadTestFile(require.resolve('./cases/push_case'));
loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions'));
loadTestFile(require.resolve('./configure'));
// sub privileges are only available with a license above basic
loadTestFile(require.resolve('./delete_sub_privilege'));
// Common
loadTestFile(require.resolve('../common'));