Skip to content

Commit

Permalink
Drilldown allow list (#85779) (#85935)
Browse files Browse the repository at this point in the history
* feat: ๐ŸŽธ add ROW_CLICK_TRIGGER

* feat: ๐ŸŽธ wire row click event to UI Actions trigger in Lens

* feat: ๐ŸŽธ add row click trigger to url drilldown

* feat: ๐ŸŽธ add datatable to row click context

* feat: ๐ŸŽธ pass in row index in row click trigger context

* feat: ๐ŸŽธ add columns to row click trigger context

* feat: ๐ŸŽธ fill values and keys event scope array

* feat: ๐ŸŽธ generate correct row scope variables

* fix: ๐Ÿ› report triggers from lens embeddable

* feat: ๐ŸŽธ add sample preview for row click trigger

* feat: ๐ŸŽธ remove url drilldown preview box

* chore: ๐Ÿค– remove mock variable generation functions

* feat: ๐ŸŽธ generate context and global variable lists

* feat: ๐ŸŽธ preview event variable list

* feat: ๐ŸŽธ show empty url error on blur

* feat: ๐ŸŽธ add ability to always show popup for executed actions

* refactor: ๐Ÿ’ก rename multiple action execution method

* fix: ๐Ÿ› don't add separator befor group on no main items

* feat: ๐ŸŽธ wire in uiActions service into datatable renderer

* feat: ๐ŸŽธ check each row if it has compatible row click actions

* feat: ๐ŸŽธ allow passing data to expression renderer

* feat: ๐ŸŽธ add isEmbeddable helper

* feat: ๐ŸŽธ pass embeddable to lens table renderer

* feat: ๐ŸŽธ hide lens table row actions which are empty

* feat: ๐ŸŽธ re-render lens embeddable when dynamic actions chagne

* feat: ๐ŸŽธ hide actions column if there are no row actions

* feat: ๐ŸŽธ re-render lens embeddable on view mode chagne

* fix: ๐Ÿ› fix TypeScript errors

* chore: ๐Ÿค– fix TypeScript errors

* docs: โœ๏ธ update auto-generated docs

* feat: ๐ŸŽธ add hasCompatibleActions to expression layer

* feat: ๐ŸŽธ remove "data" from expression renderer handlers

* fix: ๐Ÿ› fix TypeScript errors

* test: ๐Ÿ’ fix Jest tests

* docs: โœ๏ธ update autogenerated docs

* fix: ๐Ÿ› wrap event payload into data

* test: ๐Ÿ’ add "alwaysShowPopup" test

* chore: ๐Ÿค– add comment requested in review

#83167 (comment)

* test: ๐Ÿ’ add hasCompatibleActions test

* test: ๐Ÿ’ add datatable renderer test

* test: ๐Ÿ’ add Lens embeddable input change tests

* test: ๐Ÿ’ add embeddable row click test

* fix: ๐Ÿ› add url validation

* test: ๐Ÿ’ add url drilldown tests

* docs: โœ๏ธ remove url drilldown preview from docs

* docs: โœ๏ธ remove preview from url templating

* docs: โœ๏ธ add row click description

* chore: ๐Ÿค– move 36.5 KB bundle balance to url_drilldown

* test: ๐Ÿ’ simplify test case

* feat: ๐ŸŽธ check if external URL is valid before redirecting user

* test: ๐Ÿ’ check for external URL validity

* feat: ๐ŸŽธ check if external URL is allowed in exec and getHref

* test: ๐Ÿ’ fix test import

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
streamich and kibanamachine authored Dec 15, 2020
1 parent a73d98f commit e968d2b
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { IExternalUrl } from 'src/core/public';
import { UrlDrilldown, ActionContext, Config } from './url_drilldown';
import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public/lib/embeddables';
import { DatatableColumnType } from '../../../../../../src/plugins/expressions/common';
import { of } from '../../../../../../src/plugins/kibana_utils';
import { createPoint, rowClickData, TestEmbeddable } from './test/data';
import {
VALUE_CLICK_TRIGGER,
Expand Down Expand Up @@ -59,13 +61,27 @@ const mockEmbeddable = ({

const mockNavigateToUrl = jest.fn(() => Promise.resolve());

describe('UrlDrilldown', () => {
const urlDrilldown = new UrlDrilldown({
class TextExternalUrl implements IExternalUrl {
constructor(private readonly isCorrect: boolean = true) {}

public validateUrl(url: string): URL | null {
return this.isCorrect ? new URL(url) : null;
}
}

const createDrilldown = (isExternalUrlValid: boolean = true) => {
const drilldown = new UrlDrilldown({
externalUrl: new TextExternalUrl(isExternalUrlValid),
getGlobalScope: () => ({ kibanaUrl: 'http://localhost:5601/' }),
getSyntaxHelpDocsLink: () => 'http://localhost:5601/docs',
getVariablesHelpDocsLink: () => 'http://localhost:5601/docs',
navigateToUrl: mockNavigateToUrl,
});
return drilldown;
};

describe('UrlDrilldown', () => {
const urlDrilldown = createDrilldown();

test('license', () => {
expect(urlDrilldown.minimalLicense).toBe('gold');
Expand Down Expand Up @@ -125,6 +141,30 @@ describe('UrlDrilldown', () => {

await expect(urlDrilldown.isCompatible(config, context)).resolves.toBe(false);
});

test('not compatible if external URL is denied', async () => {
const drilldown1 = createDrilldown(true);
const drilldown2 = createDrilldown(false);
const config: Config = {
url: {
template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`,
},
openInNewTab: false,
};

const context: ActionContext = {
data: {
data: mockDataPoints,
},
embeddable: mockEmbeddable,
};

const result1 = await drilldown1.isCompatible(config, context);
const result2 = await drilldown2.isCompatible(config, context);

expect(result1).toBe(true);
expect(result2).toBe(false);
});
});

describe('getHref & execute', () => {
Expand Down Expand Up @@ -173,6 +213,42 @@ describe('UrlDrilldown', () => {
await expect(urlDrilldown.execute(config, context)).rejects.toThrowError();
expect(mockNavigateToUrl).not.toBeCalled();
});

test('should throw on denied external URL', async () => {
const drilldown1 = createDrilldown(true);
const drilldown2 = createDrilldown(false);
const config: Config = {
url: {
template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`,
},
openInNewTab: false,
};

const context: ActionContext = {
data: {
data: mockDataPoints,
},
embeddable: mockEmbeddable,
};

const url = await drilldown1.getHref(config, context);
await drilldown1.execute(config, context);

expect(url).toMatchInlineSnapshot(`"https://elasti.co/?test&(language:kuery,query:test)"`);
expect(mockNavigateToUrl).toBeCalledWith(url);

const [, error1] = await of(drilldown2.getHref(config, context));
const [, error2] = await of(drilldown2.execute(config, context));

expect(error1).toBeInstanceOf(Error);
expect(error1.message).toMatchInlineSnapshot(
`"External URL [https://elasti.co/?test&(language:kuery,query:test)] was denied by ExternalUrl service. You can configure external URL policies using \\"externalUrl.policy\\" setting in kibana.yml."`
);
expect(error2).toBeInstanceOf(Error);
expect(error2.message).toMatchInlineSnapshot(
`"External URL [https://elasti.co/?test&(language:kuery,query:test)] was denied by ExternalUrl service. You can configure external URL policies using \\"externalUrl.policy\\" setting in kibana.yml."`
);
});
});

describe('variables', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import React from 'react';
import { getFlattenedObject } from '@kbn/std';
import { IExternalUrl } from 'src/core/public';
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
import {
ChartActionContext,
Expand All @@ -31,6 +32,7 @@ import { getPanelVariables, getEventScope, getEventVariableList } from './url_dr
import { txtUrlDrilldownDisplayName } from './i18n';

interface UrlDrilldownDeps {
externalUrl: IExternalUrl;
getGlobalScope: () => UrlDrilldownGlobalScope;
navigateToUrl: (url: string) => Promise<void>;
getSyntaxHelpDocsLink: () => string;
Expand All @@ -55,7 +57,7 @@ const URL_DRILLDOWN = 'URL_DRILLDOWN';
export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactoryContext> {
public readonly id = URL_DRILLDOWN;

constructor(private deps: UrlDrilldownDeps) {}
constructor(private readonly deps: UrlDrilldownDeps) {}

public readonly order = 8;

Expand Down Expand Up @@ -109,18 +111,37 @@ export class UrlDrilldown implements Drilldown<Config, UrlTrigger, ActionFactory
console.warn(
`UrlDrilldown [${config.url.template}] is not valid. Error [${error}]. Skipping execution.`
);
return false;
}

return Promise.resolve(isValid);
const url = this.buildUrl(config, context);
const validUrl = this.deps.externalUrl.validateUrl(url);
if (!validUrl) {
return false;
}

return true;
};

public readonly getHref = async (config: Config, context: ActionContext) => {
const scope = this.getRuntimeVariables(context);
return urlDrilldownCompileUrl(config.url.template, scope);
private buildUrl(config: Config, context: ActionContext): string {
const url = urlDrilldownCompileUrl(config.url.template, this.getRuntimeVariables(context));
return url;
}

public readonly getHref = async (config: Config, context: ActionContext): Promise<string> => {
const url = this.buildUrl(config, context);
const validUrl = this.deps.externalUrl.validateUrl(url);
if (!validUrl) {
throw new Error(
`External URL [${url}] was denied by ExternalUrl service. ` +
`You can configure external URL policies using "externalUrl.policy" setting in kibana.yml.`
);
}
return url;
};

public readonly execute = async (config: Config, context: ActionContext) => {
const url = urlDrilldownCompileUrl(config.url.template, this.getRuntimeVariables(context));
const url = await this.getHref(config, context);
if (config.openInNewTab) {
window.open(url, '_blank', 'noopener');
} else {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class UrlDrilldownPlugin
const startServices = createStartServicesGetter(core.getStartServices);
plugins.uiActionsEnhanced.registerDrilldown(
new UrlDrilldown({
externalUrl: core.http.externalUrl,
getGlobalScope: urlDrilldownGlobalScopeProvider({ core }),
navigateToUrl: (url: string) =>
core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)),
Expand Down

0 comments on commit e968d2b

Please sign in to comment.