[data views] check kibana capabilities for all saving / editing / deleting (#118480)

* implement permissions check

* implement permissions check

* fix server side usage

* pass request in more places

* add tests, cleanup

* infra doesn't edit data views

* reporting only reads data views

* update api consumers to reflect read only access

* update api consumers to reflect read only access

* Update index.ts

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Matthew Kime 2021-11-30 22:10:44 -06:00 committed by GitHub
parent a23874cecd
commit fdc7459fcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 152 additions and 37 deletions

View file

@ -22,6 +22,10 @@ FldList [
]
`;
exports[`IndexPatterns createAndSave will throw if insufficient access 1`] = `[DataViewInsufficientAccessError: Operation failed due to insufficient access, id: undefined]`;
exports[`IndexPatterns delete will throw if insufficient access 1`] = `[DataViewInsufficientAccessError: Operation failed due to insufficient access, id: 1]`;
exports[`IndexPatterns savedObjectToSpec 1`] = `
Object {
"allowNoIndex": undefined,
@ -60,3 +64,5 @@ Object {
"version": "version",
}
`;
exports[`IndexPatterns updateSavedObject will throw if insufficient access 1`] = `[DataViewInsufficientAccessError: Operation failed due to insufficient access, id: id]`;

View file

@ -48,6 +48,7 @@ const savedObject = {
describe('IndexPatterns', () => {
let indexPatterns: DataViewsService;
let indexPatternsNoAccess: DataViewsService;
let savedObjectsClient: SavedObjectsClientCommon;
let SOClientGetDelay = 0;
const uiSettings = {
@ -99,6 +100,18 @@ describe('IndexPatterns', () => {
onNotification: () => {},
onError: () => {},
onRedirectNoIndexPattern: () => {},
getCanSave: () => Promise.resolve(true),
});
indexPatternsNoAccess = new DataViewsService({
uiSettings,
savedObjectsClient: savedObjectsClient as unknown as SavedObjectsClientCommon,
apiClient: createFieldsFetcher(),
fieldFormats,
onNotification: () => {},
onError: () => {},
onRedirectNoIndexPattern: () => {},
getCanSave: () => Promise.resolve(false),
});
});
@ -171,6 +184,10 @@ describe('IndexPatterns', () => {
expect(indexPattern).not.toBe(await indexPatterns.get(id));
});
test('delete will throw if insufficient access', async () => {
await expect(indexPatternsNoAccess.delete('1')).rejects.toMatchSnapshot();
});
test('should handle version conflicts', async () => {
setDocsourcePayload(null, {
id: 'foo',
@ -246,6 +263,18 @@ describe('IndexPatterns', () => {
expect(indexPatterns.setDefault).toBeCalled();
});
test('createAndSave will throw if insufficient access', async () => {
const title = 'kibana-*';
await expect(indexPatternsNoAccess.createAndSave({ title })).rejects.toMatchSnapshot();
});
test('updateSavedObject will throw if insufficient access', async () => {
await expect(
indexPatternsNoAccess.updateSavedObject({ id: 'id' } as unknown as DataView)
).rejects.toMatchSnapshot();
});
test('savedObjectToSpec', () => {
const spec = indexPatterns.savedObjectToSpec(savedObject);
expect(spec).toMatchSnapshot();

View file

@ -35,7 +35,7 @@ import { META_FIELDS, SavedObject } from '../../common';
import { SavedObjectNotFound } from '../../../kibana_utils/common';
import { DataViewMissingIndices } from '../lib';
import { findByTitle } from '../utils';
import { DuplicateDataViewError } from '../errors';
import { DuplicateDataViewError, DataViewInsufficientAccessError } from '../errors';
const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3;
@ -67,6 +67,7 @@ interface IndexPatternsServiceDeps {
onNotification: OnNotification;
onError: OnError;
onRedirectNoIndexPattern?: () => void;
getCanSave: () => Promise<boolean>;
}
export class DataViewsService {
@ -78,6 +79,7 @@ export class DataViewsService {
private onNotification: OnNotification;
private onError: OnError;
private dataViewCache: ReturnType<typeof createDataViewCache>;
private getCanSave: () => Promise<boolean>;
/**
* @deprecated Use `getDefaultDataView` instead (when loading data view) and handle
@ -93,6 +95,7 @@ export class DataViewsService {
onNotification,
onError,
onRedirectNoIndexPattern = () => {},
getCanSave = () => Promise.resolve(false),
}: IndexPatternsServiceDeps) {
this.apiClient = apiClient;
this.config = uiSettings;
@ -101,6 +104,7 @@ export class DataViewsService {
this.onNotification = onNotification;
this.onError = onError;
this.ensureDefaultDataView = createEnsureDefaultDataView(uiSettings, onRedirectNoIndexPattern);
this.getCanSave = getCanSave;
this.dataViewCache = createDataViewCache();
}
@ -557,6 +561,9 @@ export class DataViewsService {
*/
async createSavedObject(indexPattern: DataView, override = false) {
if (!(await this.getCanSave())) {
throw new DataViewInsufficientAccessError();
}
const dupe = await findByTitle(this.savedObjectsClient, indexPattern.title);
if (dupe) {
if (override) {
@ -595,6 +602,9 @@ export class DataViewsService {
ignoreErrors: boolean = false
): Promise<void | Error> {
if (!indexPattern.id) return;
if (!(await this.getCanSave())) {
throw new DataViewInsufficientAccessError(indexPattern.id);
}
// get the list of attributes
const body = indexPattern.getAsSavedObjectBody();
@ -678,6 +688,9 @@ export class DataViewsService {
* @param indexPatternId: Id of kibana Index Pattern to delete
*/
async delete(indexPatternId: string) {
if (!(await this.getCanSave())) {
throw new DataViewInsufficientAccessError(indexPatternId);
}
this.dataViewCache.clear(indexPatternId);
return this.savedObjectsClient.delete(DATA_VIEW_SAVED_OBJECT_TYPE, indexPatternId);
}

View file

@ -8,3 +8,4 @@
export * from './duplicate_index_pattern';
export * from './data_view_saved_object_conflict';
export * from './insufficient_access';

View file

@ -0,0 +1,14 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export class DataViewInsufficientAccessError extends Error {
constructor(savedObjectId?: string) {
super(`Operation failed due to insufficient access, id: ${savedObjectId}`);
this.name = 'DataViewInsufficientAccessError';
}
}

View file

@ -61,6 +61,7 @@ export class DataViewsPublicPlugin
application.navigateToApp,
overlays
),
getCanSave: () => Promise.resolve(application.capabilities.indexPatterns.save === true),
});
}

View file

@ -11,6 +11,8 @@ import {
SavedObjectsClientContract,
ElasticsearchClient,
UiSettingsServiceStart,
KibanaRequest,
CoreStart,
} from 'kibana/server';
import { DataViewsService } from '../common';
import { FieldFormatsStart } from '../../field_formats/server';
@ -23,14 +25,17 @@ export const dataViewsServiceFactory =
logger,
uiSettings,
fieldFormats,
capabilities,
}: {
logger: Logger;
uiSettings: UiSettingsServiceStart;
fieldFormats: FieldFormatsStart;
capabilities: CoreStart['capabilities'];
}) =>
async (
savedObjectsClient: SavedObjectsClientContract,
elasticsearchClient: ElasticsearchClient
elasticsearchClient: ElasticsearchClient,
request?: KibanaRequest
) => {
const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient);
@ -46,5 +51,9 @@ export const dataViewsServiceFactory =
onNotification: ({ title, text }) => {
logger.warn(`${title}${text ? ` : ${text}` : ''}`);
},
getCanSave: async () =>
request
? (await capabilities.resolveCapabilities(request)).indexPatterns.save === true
: false,
});
};

View file

@ -85,7 +85,8 @@ export function getIndexPatternLoad({
return {
indexPatterns: await indexPatternsServiceFactory(
savedObjects.getScopedClient(request),
elasticsearch.client.asScoped(request).asCurrentUser
elasticsearch.client.asScoped(request).asCurrentUser,
request
),
};
},

View file

@ -53,13 +53,14 @@ export class DataViewsServerPlugin
}
public start(
{ uiSettings }: CoreStart,
{ uiSettings, capabilities }: CoreStart,
{ fieldFormats }: DataViewsServerPluginStartDependencies
) {
const serviceFactory = dataViewsServiceFactory({
logger: this.logger.get('indexPatterns'),
uiSettings,
fieldFormats,
capabilities,
});
return {

View file

@ -71,7 +71,8 @@ export const registerCreateIndexPatternRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const body = req.body;

View file

@ -29,7 +29,8 @@ export const registerManageDefaultIndexPatternRoutes = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const defaultIndexPatternId = await indexPatternsService.getDefaultId();
@ -63,7 +64,8 @@ export const registerManageDefaultIndexPatternRoutes = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const newDefaultId = req.body.index_pattern_id;

View file

@ -40,7 +40,8 @@ export const registerDeleteIndexPatternRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;

View file

@ -64,7 +64,8 @@ export const registerUpdateFieldsRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const { fields } = req.body;

View file

@ -40,7 +40,8 @@ export const registerGetIndexPatternRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const indexPattern = await indexPatternsService.get(id);

View file

@ -29,7 +29,8 @@ export const registerHasUserIndexPatternRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
return res.ok({

View file

@ -48,7 +48,8 @@ export const registerCreateRuntimeFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const { name, runtimeField } = req.body;

View file

@ -44,7 +44,8 @@ export const registerDeleteRuntimeFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const name = req.params.name;

View file

@ -45,7 +45,8 @@ export const registerGetRuntimeFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const name = req.params.name;

View file

@ -47,7 +47,8 @@ export const registerPutRuntimeFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const { name, runtimeField } = req.body;

View file

@ -55,7 +55,8 @@ export const registerUpdateRuntimeFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const name = req.params.name;

View file

@ -47,7 +47,8 @@ export const registerCreateScriptedFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const { field } = req.body;

View file

@ -48,7 +48,8 @@ export const registerDeleteScriptedFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const name = req.params.name;

View file

@ -48,7 +48,8 @@ export const registerGetScriptedFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const name = req.params.name;

View file

@ -47,7 +47,8 @@ export const registerPutScriptedFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const { field } = req.body;

View file

@ -68,7 +68,8 @@ export const registerUpdateScriptedFieldRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;
const name = req.params.name;

View file

@ -68,7 +68,8 @@ export const registerUpdateIndexPatternRoute = (
const [, , { indexPatternsServiceFactory }] = await getStartServices();
const indexPatternsService = await indexPatternsServiceFactory(
savedObjectsClient,
elasticsearchClient
elasticsearchClient,
req
);
const id = req.params.id;

View file

@ -6,7 +6,12 @@
* Side Public License, v 1.
*/
import { Logger, SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server';
import {
Logger,
SavedObjectsClientContract,
ElasticsearchClient,
KibanaRequest,
} from 'kibana/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { DataViewsService } from '../common';
@ -14,7 +19,8 @@ import { FieldFormatsSetup, FieldFormatsStart } from '../../field_formats/server
type ServiceFactory = (
savedObjectsClient: SavedObjectsClientContract,
elasticsearchClient: ElasticsearchClient
elasticsearchClient: ElasticsearchClient,
request?: KibanaRequest
) => Promise<DataViewsService>;
export interface DataViewsServerPluginStart {
dataViewsServiceFactory: ServiceFactory;

View file

@ -57,7 +57,8 @@ export class DataSearchTestPlugin
// to look it up on the fly and insert it into the request.
const indexPatterns = await data.indexPatterns.indexPatternsServiceFactory(
savedObjectsClient,
clusterClient
clusterClient,
req
);
const ids = await indexPatterns.getIds();
// @ts-expect-error Force overwriting the request

View file

@ -38,7 +38,8 @@ export class IndexPatternsTestPlugin
const savedObjectsClient = savedObjects.getScopedClient(req);
const service = await data.indexPatterns.indexPatternsServiceFactory(
savedObjectsClient,
elasticsearch.client.asScoped(req).asCurrentUser
elasticsearch.client.asScoped(req).asCurrentUser,
req
);
const ids = await service.createAndSave(req.body);
return res.ok({ body: ids });
@ -52,7 +53,8 @@ export class IndexPatternsTestPlugin
const savedObjectsClient = savedObjects.getScopedClient(req);
const service = await data.indexPatterns.indexPatternsServiceFactory(
savedObjectsClient,
elasticsearch.client.asScoped(req).asCurrentUser
elasticsearch.client.asScoped(req).asCurrentUser,
req
);
const ids = await service.getIds(true);
return res.ok({ body: ids });
@ -74,7 +76,8 @@ export class IndexPatternsTestPlugin
const savedObjectsClient = savedObjects.getScopedClient(req);
const service = await data.indexPatterns.indexPatternsServiceFactory(
savedObjectsClient,
elasticsearch.client.asScoped(req).asCurrentUser
elasticsearch.client.asScoped(req).asCurrentUser,
req
);
const ip = await service.get(id);
return res.ok({ body: ip.toSpec() });
@ -96,7 +99,8 @@ export class IndexPatternsTestPlugin
const savedObjectsClient = savedObjects.getScopedClient(req);
const service = await data.indexPatterns.indexPatternsServiceFactory(
savedObjectsClient,
elasticsearch.client.asScoped(req).asCurrentUser
elasticsearch.client.asScoped(req).asCurrentUser,
req
);
const ip = await service.get(id);
await service.updateSavedObject(ip);
@ -119,7 +123,8 @@ export class IndexPatternsTestPlugin
const savedObjectsClient = savedObjects.getScopedClient(req);
const service = await data.indexPatterns.indexPatternsServiceFactory(
savedObjectsClient,
elasticsearch.client.asScoped(req).asCurrentUser
elasticsearch.client.asScoped(req).asCurrentUser,
req
);
await service.delete(id);
return res.ok();

View file

@ -52,7 +52,8 @@ export function initIndexingRoutes({
const { index, mappings } = request.body;
const indexPatternsService = await dataPlugin.indexPatterns.indexPatternsServiceFactory(
context.core.savedObjects.client,
context.core.elasticsearch.client.asCurrentUser
context.core.elasticsearch.client.asCurrentUser,
request
);
const result = await createDocSource(
index,

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import type { IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server';
import type {
IScopedClusterClient,
SavedObjectsClientContract,
KibanaRequest,
} from 'kibana/server';
import type { DataViewsService } from '../../../../../src/plugins/data_views/common';
import type { PluginStart as DataViewsPluginStart } from '../../../../../src/plugins/data_views/server';
@ -15,12 +19,14 @@ export type GetDataViewsService = () => Promise<DataViewsService>;
export function getDataViewsServiceFactory(
getDataViews: () => DataViewsPluginStart | null,
savedObjectClient: SavedObjectsClientContract,
scopedClient: IScopedClusterClient
scopedClient: IScopedClusterClient,
request: KibanaRequest
): GetDataViewsService {
const dataViews = getDataViews();
if (dataViews === null) {
throw Error('data views service has not been initialized');
}
return () => dataViews.dataViewsServiceFactory(savedObjectClient, scopedClient.asCurrentUser);
return () =>
dataViews.dataViewsServiceFactory(savedObjectClient, scopedClient.asCurrentUser, request);
}

View file

@ -114,7 +114,8 @@ export class RouteGuard {
getDataViewsService: getDataViewsServiceFactory(
this._getDataViews,
context.core.savedObjects.client,
client
client,
request
),
});
};

View file

@ -252,7 +252,8 @@ function getRequestItemsProvider(
const getDataViewsService = getDataViewsServiceFactory(
getDataViews,
savedObjectsClient,
scopedClient
scopedClient,
request
);
return {

View file

@ -42,7 +42,8 @@ export const createSourcererDataViewRoute = (
] = await getStartServices();
const dataViewService = await indexPatterns.indexPatternsServiceFactory(
context.core.savedObjects.client,
context.core.elasticsearch.client.asInternalUser
context.core.elasticsearch.client.asInternalUser,
request
);
let allDataViews = await dataViewService.getIdsWithTitle();