Skip to content

Commit ddc8471

Browse files
committed
add error handling and tests for privilege related errors
1 parent e067fa2 commit ddc8471

File tree

5 files changed

+277
-2
lines changed

5 files changed

+277
-2
lines changed

x-pack/plugins/data_usage/server/common/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ export class BaseError<MetaType = unknown> extends Error {
1616
}
1717
}
1818
}
19+
20+
export const NoPrivilegeMeteringError =
21+
'You do not have the necessary privileges to access data stream statistics. Please contact your administrator.';
22+
23+
export const NoIndicesMeteringError =
24+
'No data streams or indices are available for the current user. Ensure that the data streams or indices you are authorized to access have been created and contain data. If you believe this is an error, please contact your administrator.';

x-pack/plugins/data_usage/server/routes/error_handler.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import type { IKibanaResponse, KibanaResponseFactory, Logger } from '@kbn/core/server';
99
import { CustomHttpRequestError } from '../utils/custom_http_request_error';
10-
import { BaseError } from '../common/errors';
10+
import { BaseError, NoIndicesMeteringError, NoPrivilegeMeteringError } from '../common/errors';
1111
import { AutoOpsError } from '../services/errors';
1212

1313
export class NotFoundError extends BaseError {}
@@ -43,6 +43,22 @@ export const errorHandler = <E extends Error>(
4343
return res.notFound({ body: error });
4444
}
4545

46+
if (error.message.includes('security_exception')) {
47+
return res.forbidden({
48+
body: {
49+
message: NoPrivilegeMeteringError,
50+
},
51+
});
52+
}
53+
54+
if (error.message.includes('index_not_found_exception')) {
55+
return res.notFound({
56+
body: {
57+
message: NoIndicesMeteringError,
58+
},
59+
});
60+
}
61+
4662
// Kibana CORE will take care of `500` errors when the handler `throw`'s, including logging the error
4763
throw error;
4864
};

x-pack/test_serverless/api_integration/test_suites/common/data_usage/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
1111
describe('Serverless Data Usage APIs', function () {
1212
this.tags(['esGate']);
1313

14+
loadTestFile(require.resolve('./tests/data_streams_privileges'));
1415
loadTestFile(require.resolve('./tests/data_streams'));
1516
loadTestFile(require.resolve('./tests/metrics'));
1617
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import expect from '@kbn/expect';
9+
import { DataStreamsResponseBodySchemaBody } from '@kbn/data-usage-plugin/common/rest_types';
10+
import { DATA_USAGE_DATA_STREAMS_API_ROUTE } from '@kbn/data-usage-plugin/common';
11+
import type { RoleCredentials } from '@kbn/ftr-common-functional-services';
12+
import {
13+
NoIndicesMeteringError,
14+
NoPrivilegeMeteringError,
15+
} from '@kbn/data-usage-plugin/server/common/errors';
16+
import { FtrProviderContext } from '../../../../ftr_provider_context';
17+
18+
export default function ({ getService }: FtrProviderContext) {
19+
const svlDatastreamsHelpers = getService('svlDatastreamsHelpers');
20+
const svlCommonApi = getService('svlCommonApi');
21+
const samlAuth = getService('samlAuth');
22+
const supertestWithoutAuth = getService('supertestWithoutAuth');
23+
const testDataStreamName = 'test-data-stream';
24+
const otherTestDataStreamName = 'other-test-data-stream';
25+
let roleAuthc: RoleCredentials;
26+
27+
describe('privileges with custom roles', function () {
28+
// custom role testing is not supported in MKI
29+
// the metering api which this route calls requires one of: monitor,view_index_metadata,manage,all
30+
this.tags(['skipMKI']);
31+
before(async () => {
32+
await svlDatastreamsHelpers.createDataStream(testDataStreamName);
33+
await svlDatastreamsHelpers.createDataStream(otherTestDataStreamName);
34+
});
35+
after(async () => {
36+
await svlDatastreamsHelpers.deleteDataStream(testDataStreamName);
37+
await svlDatastreamsHelpers.deleteDataStream(otherTestDataStreamName);
38+
});
39+
afterEach(async () => {
40+
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
41+
await samlAuth.deleteCustomRole();
42+
});
43+
it('returns all data streams for indices with necessary privileges', async () => {
44+
await samlAuth.setCustomRole({
45+
elasticsearch: {
46+
indices: [{ names: ['*'], privileges: ['all'] }],
47+
},
48+
kibana: [
49+
{
50+
base: ['all'],
51+
feature: {},
52+
spaces: ['*'],
53+
},
54+
],
55+
});
56+
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
57+
const res = await supertestWithoutAuth
58+
.get(DATA_USAGE_DATA_STREAMS_API_ROUTE)
59+
.query({ includeZeroStorage: true })
60+
.set(svlCommonApi.getInternalRequestHeader())
61+
.set(roleAuthc.apiKeyHeader)
62+
.set('elastic-api-version', '1');
63+
64+
const dataStreams: DataStreamsResponseBodySchemaBody = res.body;
65+
const foundTestDataStream = dataStreams.find((stream) => stream.name === testDataStreamName);
66+
const foundTestDataStream2 = dataStreams.find(
67+
(stream) => stream.name === otherTestDataStreamName
68+
);
69+
expect(res.statusCode).to.be(200);
70+
expect(foundTestDataStream?.name).to.be(testDataStreamName);
71+
expect(foundTestDataStream2?.name).to.be(otherTestDataStreamName);
72+
});
73+
it('returns data streams for only a subset of indices with necessary privileges', async () => {
74+
await samlAuth.setCustomRole({
75+
elasticsearch: {
76+
indices: [{ names: ['test-data-stream*'], privileges: ['all'] }],
77+
},
78+
kibana: [
79+
{
80+
base: ['all'],
81+
feature: {},
82+
spaces: ['*'],
83+
},
84+
],
85+
});
86+
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
87+
const res = await supertestWithoutAuth
88+
.get(DATA_USAGE_DATA_STREAMS_API_ROUTE)
89+
.query({ includeZeroStorage: true })
90+
.set(svlCommonApi.getInternalRequestHeader())
91+
.set(roleAuthc.apiKeyHeader)
92+
.set('elastic-api-version', '1');
93+
94+
const dataStreams: DataStreamsResponseBodySchemaBody = res.body;
95+
const foundTestDataStream = dataStreams.find((stream) => stream.name === testDataStreamName);
96+
const dataStreamNoPermission = dataStreams.find(
97+
(stream) => stream.name === otherTestDataStreamName
98+
);
99+
100+
expect(res.statusCode).to.be(200);
101+
expect(foundTestDataStream?.name).to.be(testDataStreamName);
102+
expect(dataStreamNoPermission?.name).to.be(undefined);
103+
});
104+
it('returns no data streams without necessary privileges', async () => {
105+
await samlAuth.setCustomRole({
106+
elasticsearch: {
107+
indices: [{ names: ['*'], privileges: ['write'] }],
108+
},
109+
kibana: [
110+
{
111+
base: ['all'],
112+
feature: {},
113+
spaces: ['*'],
114+
},
115+
],
116+
});
117+
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
118+
const res = await supertestWithoutAuth
119+
.get(DATA_USAGE_DATA_STREAMS_API_ROUTE)
120+
.query({ includeZeroStorage: true })
121+
.set(svlCommonApi.getInternalRequestHeader())
122+
.set(roleAuthc.apiKeyHeader)
123+
.set('elastic-api-version', '1');
124+
125+
expect(res.statusCode).to.be(403);
126+
expect(res.body.message).to.contain(NoPrivilegeMeteringError);
127+
});
128+
it('returns no data streams when there are none it has access to', async () => {
129+
await samlAuth.setCustomRole({
130+
elasticsearch: {
131+
indices: [{ names: ['none*'], privileges: ['all'] }],
132+
},
133+
kibana: [
134+
{
135+
base: ['all'],
136+
feature: {},
137+
spaces: ['*'],
138+
},
139+
],
140+
});
141+
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('customRole');
142+
const res = await supertestWithoutAuth
143+
.get(DATA_USAGE_DATA_STREAMS_API_ROUTE)
144+
.query({ includeZeroStorage: true })
145+
.set(svlCommonApi.getInternalRequestHeader())
146+
.set(roleAuthc.apiKeyHeader)
147+
.set('elastic-api-version', '1');
148+
149+
expect(res.statusCode).to.be(404);
150+
expect(res.body.message).to.contain(NoIndicesMeteringError);
151+
});
152+
});
153+
}

x-pack/test_serverless/functional/test_suites/common/data_usage/privileges.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@
55
* 2.0.
66
*/
77

8+
import expect from '@kbn/expect';
9+
import {
10+
NoIndicesMeteringError,
11+
NoPrivilegeMeteringError,
12+
} from '@kbn/data-usage-plugin/server/common/errors';
813
import { FtrProviderContext } from '../../../ftr_provider_context';
914

1015
export default ({ getPageObjects, getService }: FtrProviderContext) => {
1116
const pageObjects = getPageObjects(['svlCommonPage', 'svlManagementPage', 'common']);
1217
const testSubjects = getService('testSubjects');
1318
const samlAuth = getService('samlAuth');
1419
const retry = getService('retry');
20+
const es = getService('es');
1521
const dataUsageAppUrl = 'management/data/data_usage';
22+
const toasts = getService('toasts');
1623

1724
const navigateAndVerify = async (expectedVisible: boolean) => {
1825
await pageObjects.common.navigateToApp('management');
@@ -32,6 +39,32 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
3239
};
3340

3441
describe('privileges', function () {
42+
before(async () => {
43+
await es.indices.putIndexTemplate({
44+
name: 'test-datastream',
45+
body: {
46+
index_patterns: ['test-datastream'],
47+
data_stream: {},
48+
priority: 200,
49+
},
50+
});
51+
52+
await es.indices.createDataStream({ name: 'test-datastream' });
53+
await es.indices.putIndexTemplate({
54+
name: 'no-permission-test-datastream',
55+
body: {
56+
index_patterns: ['no-permission-test-datastream'],
57+
data_stream: {},
58+
priority: 200,
59+
},
60+
});
61+
62+
await es.indices.createDataStream({ name: 'no-permission-test-datastream' });
63+
});
64+
after(async () => {
65+
await es.indices.deleteDataStream({ name: 'test-datastream' });
66+
await es.indices.deleteDataStream({ name: 'no-permission-test-datastream' });
67+
});
3568
it('renders for the admin role', async () => {
3669
await pageObjects.svlCommonPage.loginAsAdmin();
3770
await navigateAndVerify(true);
@@ -63,7 +96,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
6396
afterEach(async () => {
6497
await samlAuth.deleteCustomRole();
6598
});
66-
it('renders with a custom role that has the monitor cluster privilege', async () => {
99+
it('renders with a custom role that has the privileges cluster: monitor and indices all', async () => {
67100
await samlAuth.setCustomRole({
68101
elasticsearch: {
69102
cluster: ['monitor'],
@@ -97,6 +130,72 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
97130
await pageObjects.svlCommonPage.loginWithCustomRole();
98131
await navigateAndVerify(false);
99132
});
133+
134+
describe.skip('with custom role and data streams', function () {
135+
// skip in all environments. requires a code change to the data_streams route
136+
// to allow zero storage data streams to not be filtered out, but useful for testing.
137+
// the api integration tests can pass a flag to get around this case but we can't in the UI.
138+
// metering api requires one of: monitor,view_index_metadata,manage,all
139+
it('does not load data streams without necessary index privilege for any index', async () => {
140+
await samlAuth.setCustomRole({
141+
elasticsearch: {
142+
cluster: ['monitor'],
143+
indices: [{ names: ['*'], privileges: ['read'] }],
144+
},
145+
kibana: [
146+
{
147+
base: ['all'],
148+
feature: {},
149+
spaces: ['*'],
150+
},
151+
],
152+
});
153+
await pageObjects.svlCommonPage.loginWithCustomRole();
154+
await navigateAndVerify(true);
155+
const toastContent = await toasts.getContentByIndex(1);
156+
expect(toastContent).to.contain(NoPrivilegeMeteringError);
157+
});
158+
159+
it('does load data streams with necessary index privilege for some indices', async () => {
160+
await samlAuth.setCustomRole({
161+
elasticsearch: {
162+
cluster: ['monitor'],
163+
indices: [
164+
{ names: ['test-datastream*'], privileges: ['all'] },
165+
{ names: ['.*'], privileges: ['read'] },
166+
],
167+
},
168+
kibana: [
169+
{
170+
base: ['all'],
171+
feature: {},
172+
spaces: ['*'],
173+
},
174+
],
175+
});
176+
await pageObjects.svlCommonPage.loginWithCustomRole();
177+
await navigateAndVerify(true);
178+
});
179+
it('handles error when no data streams that it has permission to exist (index_not_found_exception)', async () => {
180+
await samlAuth.setCustomRole({
181+
elasticsearch: {
182+
cluster: ['monitor'],
183+
indices: [{ names: ['none*'], privileges: ['all'] }],
184+
},
185+
kibana: [
186+
{
187+
base: ['all'],
188+
feature: {},
189+
spaces: ['*'],
190+
},
191+
],
192+
});
193+
await pageObjects.svlCommonPage.loginWithCustomRole();
194+
await navigateAndVerify(true);
195+
const toastContent = await toasts.getContentByIndex(1);
196+
expect(toastContent).to.contain(NoIndicesMeteringError);
197+
});
198+
});
100199
});
101200
});
102201
};

0 commit comments

Comments
 (0)