Expose anonymous access through a switch in sharing menu (#86965)

* feat: 🎸 add "Public URL" switch

* feat: 🎸 add url subtitle

* feat: 🎸 add public URL toggle state

* feat: 🎸 allow to dynamically enable anonymous access switch

* feat: 🎸 add anon access url parameters to share url

* fix: 🐛 correctly add params to url

* fix: 🐛 correctly add anon access to saved object URL

* fix: 🐛 don't generate anon access urls twice

* feat: 🎸 add ability to check anonymous user capabilities

* feat: 🎸 add capability checks to Discover and Visualize apps

* refactor: 💡 use early return

* test: 💍 use security_oss mocks

* feat: 🎸 add anon access url params to short url

* test: 💍 fix jest snapshots

* perf: ️ make capabilities check synchronous

* style: 💄 add stylistic review changes

* perf: ️ don't fetch anon user capabilities if anon not enabled

* fix: 🐛 in discover app check if discover exists in capabilities

* test: 💍 add tests for discover sharing check

* test: 💍 add tests for showPublicUrlSwitch checks

* feat: 🎸 make visualize capabilities props required

* style: 💄 remove unused import

* feat: 🎸 improve tooltip copy
This commit is contained in:
Vadim Dalecky 2021-01-28 18:46:14 +01:00 committed by GitHub
parent c73000f644
commit 10e6354d77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 584 additions and 146 deletions

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { Capabilities } from 'src/core/public';
import { showPublicUrlSwitch } from './show_share_modal';
describe('showPublicUrlSwitch', () => {
test('returns false if "dashboard" app is not available', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
};
const result = showPublicUrlSwitch(anonymousUserCapabilities);
expect(result).toBe(false);
});
test('returns false if "dashboard" app is not accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
dashboard: {
show: false,
},
};
const result = showPublicUrlSwitch(anonymousUserCapabilities);
expect(result).toBe(false);
});
test('returns true if "dashboard" app is not available an accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
dashboard: {
show: true,
},
};
const result = showPublicUrlSwitch(anonymousUserCapabilities);
expect(result).toBe(true);
});
});

View file

@ -6,6 +6,7 @@
* Public License, v 1.
*/
import { Capabilities } from 'src/core/public';
import { EuiCheckboxGroup } from '@elastic/eui';
import React from 'react';
import { ReactElement, useState } from 'react';
@ -27,6 +28,14 @@ interface ShowShareModalProps {
dashboardStateManager: DashboardStateManager;
}
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
if (!anonymousUserCapabilities.dashboard) return false;
const dashboard = (anonymousUserCapabilities.dashboard as unknown) as DashboardCapabilities;
return !!dashboard.show;
};
export function ShowShareModal({
share,
anchorElement,
@ -113,5 +122,6 @@ export function ShowShareModal({
component: EmbedUrlParamExtension,
},
],
showPublicUrlSwitch,
});
}

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { showOpenSearchPanel } from './show_open_search_panel';
import { getSharingData } from '../../helpers/get_sharing_data';
import { getSharingData, showPublicUrlSwitch } from '../../helpers/get_sharing_data';
import { unhashUrl } from '../../../../../kibana_utils/public';
import { DiscoverServices } from '../../../build_services';
import { Adapters } from '../../../../../inspector/common/adapters';
@ -108,6 +108,7 @@ export const getTopNavLinks = ({
title: savedSearch.title,
},
isDirty: !savedSearch.id || state.isAppStateDirty(),
showPublicUrlSwitch,
});
},
};

View file

@ -6,7 +6,8 @@
* Public License, v 1.
*/
import { getSharingData } from './get_sharing_data';
import { Capabilities } from 'kibana/public';
import { getSharingData, showPublicUrlSwitch } from './get_sharing_data';
import { IUiSettingsClient } from 'kibana/public';
import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks';
import { indexPatternMock } from '../../__mocks__/index_pattern';
@ -68,3 +69,44 @@ describe('getSharingData', () => {
`);
});
});
describe('showPublicUrlSwitch', () => {
test('returns false if "discover" app is not available', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
};
const result = showPublicUrlSwitch(anonymousUserCapabilities);
expect(result).toBe(false);
});
test('returns false if "discover" app is not accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
discover: {
show: false,
},
};
const result = showPublicUrlSwitch(anonymousUserCapabilities);
expect(result).toBe(false);
});
test('returns true if "discover" app is not available an accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
discover: {
show: true,
},
};
const result = showPublicUrlSwitch(anonymousUserCapabilities);
expect(result).toBe(true);
});
});

View file

@ -6,7 +6,7 @@
* Public License, v 1.
*/
import { IUiSettingsClient } from 'kibana/public';
import { Capabilities, IUiSettingsClient } from 'kibana/public';
import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common';
import { getSortForSearchSource } from '../angular/doc_table';
import { SearchSource } from '../../../../data/common';
@ -76,3 +76,19 @@ export async function getSharingData(
indexPatternId: index.id,
};
}
export interface DiscoverCapabilities {
createShortUrl?: boolean;
save?: boolean;
saveQuery?: boolean;
show?: boolean;
storeSearchSession?: boolean;
}
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
if (!anonymousUserCapabilities.discover) return false;
const discover = (anonymousUserCapabilities.discover as unknown) as DiscoverCapabilities;
return !!discover.show;
};

View file

@ -7,6 +7,7 @@
*/
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import { InsecureClusterServiceStart } from './insecure_cluster_service';
import { mockInsecureClusterService } from './insecure_cluster_service/insecure_cluster_service.mock';
import { SecurityOssPluginSetup, SecurityOssPluginStart } from './plugin';
@ -18,7 +19,11 @@ export const mockSecurityOssPlugin = {
},
createStart: () => {
return {
insecureCluster: mockInsecureClusterService.createStart(),
insecureCluster: mockInsecureClusterService.createStart() as jest.Mocked<InsecureClusterServiceStart>,
anonymousAccess: {
getAccessURLParameters: jest.fn().mockResolvedValue(null),
getCapabilities: jest.fn().mockResolvedValue({}),
},
} as DeeplyMockedKeys<SecurityOssPluginStart>;
},
};

View file

@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
"requiredBundles": ["kibanaUtils"]
"requiredBundles": ["kibanaUtils"],
"optionalPlugins": ["securityOss"]
}

View file

@ -115,49 +115,68 @@ exports[`share url panel content render 1`] = `
/>
</EuiFormRow>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="URL"
id="share.urlPanel.urlGroupTitle"
values={Object {}}
/>
}
labelType="label"
>
<EuiFlexGroup
gutterSize="none"
responsive={false}
<EuiSpacer
size="s"
/>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiFlexItem
grow={false}
<EuiFlexGroup
gutterSize="none"
responsive={false}
>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem
grow={false}
>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiFormRow>
<EuiSpacer
size="m"
@ -277,49 +296,68 @@ exports[`share url panel content should enable saved object export option when o
/>
</EuiFormRow>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="URL"
id="share.urlPanel.urlGroupTitle"
values={Object {}}
/>
}
labelType="label"
>
<EuiFlexGroup
gutterSize="none"
responsive={false}
<EuiSpacer
size="s"
/>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiFlexItem
grow={false}
<EuiFlexGroup
gutterSize="none"
responsive={false}
>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem
grow={false}
>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiFormRow>
<EuiSpacer
size="m"
@ -438,6 +476,25 @@ exports[`share url panel content should hide short url section when allowShortUr
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="URL"
id="share.urlPanel.urlGroupTitle"
values={Object {}}
/>
}
labelType="label"
>
<EuiSpacer
size="s"
/>
</EuiFormRow>
<EuiSpacer
size="m"
/>
@ -569,49 +626,68 @@ exports[`should show url param extensions 1`] = `
/>
</EuiFormRow>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
label={
<FormattedMessage
defaultMessage="URL"
id="share.urlPanel.urlGroupTitle"
values={Object {}}
/>
}
labelType="label"
>
<EuiFlexGroup
gutterSize="none"
responsive={false}
<EuiSpacer
size="s"
/>
<EuiFormRow
data-test-subj="createShortUrl"
describedByIds={Array []}
display="row"
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiFlexItem
grow={false}
<EuiFlexGroup
gutterSize="none"
responsive={false}
>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem
grow={false}
>
<EuiSwitch
checked={false}
data-test-subj="useShortUrl"
label={
<FormattedMessage
defaultMessage="Short URL"
id="share.urlPanel.shortUrlLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="We recommend sharing shortened snapshot URLs for maximum compatibility. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the snapshot URL, but the short URL should work great."
id="share.urlPanel.shortUrlHelpText"
values={Object {}}
/>
}
position="bottom"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</EuiFormRow>
<EuiSpacer
size="m"

View file

@ -13,9 +13,11 @@ import { i18n } from '@kbn/i18n';
import { EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui';
import { HttpStart } from 'kibana/public';
import type { Capabilities } from 'src/core/public';
import { UrlPanelContent } from './url_panel_content';
import { ShareMenuItem, ShareContextMenuPanelItem, UrlParamExtension } from '../types';
import type { SecurityOssPluginStart } from '../../../security_oss/public';
interface Props {
allowEmbed: boolean;
@ -29,6 +31,8 @@ interface Props {
basePath: string;
post: HttpStart['post'];
embedUrlParamExtensions?: UrlParamExtension[];
anonymousAccess?: SecurityOssPluginStart['anonymousAccess'];
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
}
export class ShareContextMenu extends Component<Props> {
@ -62,6 +66,8 @@ export class ShareContextMenu extends Component<Props> {
basePath={this.props.basePath}
post={this.props.post}
shareableUrl={this.props.shareableUrl}
anonymousAccess={this.props.anonymousAccess}
showPublicUrlSwitch={this.props.showPublicUrlSwitch}
/>
),
};
@ -91,6 +97,8 @@ export class ShareContextMenu extends Component<Props> {
post={this.props.post}
shareableUrl={this.props.shareableUrl}
urlParamExtensions={this.props.embedUrlParamExtensions}
anonymousAccess={this.props.anonymousAccess}
showPublicUrlSwitch={this.props.showPublicUrlSwitch}
/>
),
};

View file

@ -28,9 +28,11 @@ import { format as formatUrl, parse as parseUrl } from 'url';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { HttpStart } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import type { Capabilities } from 'src/core/public';
import { shortenUrl } from '../lib/url_shortener';
import { UrlParamExtension } from '../types';
import type { SecurityOssPluginStart } from '../../../security_oss/public';
interface Props {
allowShortUrl: boolean;
@ -41,6 +43,8 @@ interface Props {
basePath: string;
post: HttpStart['post'];
urlParamExtensions?: UrlParamExtension[];
anonymousAccess?: SecurityOssPluginStart['anonymousAccess'];
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
}
export enum ExportUrlAsType {
@ -57,10 +61,13 @@ interface UrlParams {
interface State {
exportUrlAs: ExportUrlAsType;
useShortUrl: boolean;
usePublicUrl: boolean;
isCreatingShortUrl: boolean;
url?: string;
shortUrlErrorMsg?: string;
urlParams?: UrlParams;
anonymousAccessParameters: Record<string, string> | null;
showPublicUrlSwitch: boolean;
}
export class UrlPanelContent extends Component<Props, State> {
@ -75,8 +82,11 @@ export class UrlPanelContent extends Component<Props, State> {
this.state = {
exportUrlAs: ExportUrlAsType.EXPORT_URL_AS_SNAPSHOT,
useShortUrl: false,
usePublicUrl: false,
isCreatingShortUrl: false,
url: '',
anonymousAccessParameters: null,
showPublicUrlSwitch: false,
};
}
@ -91,6 +101,41 @@ export class UrlPanelContent extends Component<Props, State> {
this.setUrl();
window.addEventListener('hashchange', this.resetUrl, false);
if (this.props.anonymousAccess) {
(async () => {
const anonymousAccessParameters = await this.props.anonymousAccess!.getAccessURLParameters();
if (!this.mounted) {
return;
}
if (!anonymousAccessParameters) {
return;
}
let showPublicUrlSwitch: boolean = false;
if (this.props.showPublicUrlSwitch) {
const anonymousUserCapabilities = await this.props.anonymousAccess!.getCapabilities();
if (!this.mounted) {
return;
}
try {
showPublicUrlSwitch = this.props.showPublicUrlSwitch!(anonymousUserCapabilities);
} catch {
showPublicUrlSwitch = false;
}
}
this.setState({
anonymousAccessParameters,
showPublicUrlSwitch,
});
})();
}
}
public render() {
@ -99,7 +144,16 @@ export class UrlPanelContent extends Component<Props, State> {
<EuiForm className="kbnShareContextMenu__finalPanel" data-test-subj="shareUrlForm">
{this.renderExportAsRadioGroup()}
{this.renderUrlParamExtensions()}
{this.renderShortUrlSwitch()}
<EuiFormRow
label={<FormattedMessage id="share.urlPanel.urlGroupTitle" defaultMessage="URL" />}
>
<>
<EuiSpacer size={'s'} />
{this.renderShortUrlSwitch()}
{this.renderPublicUrlSwitch()}
</>
</EuiFormRow>
<EuiSpacer size="m" />
@ -150,10 +204,10 @@ export class UrlPanelContent extends Component<Props, State> {
};
private updateUrlParams = (url: string) => {
const embedUrl = this.props.isEmbedded ? this.makeUrlEmbeddable(url) : url;
const extendUrl = this.state.urlParams ? this.getUrlParamExtensions(embedUrl) : embedUrl;
url = this.props.isEmbedded ? this.makeUrlEmbeddable(url) : url;
url = this.state.urlParams ? this.getUrlParamExtensions(url) : url;
return extendUrl;
return url;
};
private getSavedObjectUrl = () => {
@ -206,6 +260,20 @@ export class UrlPanelContent extends Component<Props, State> {
return `${url}${embedParam}`;
};
private addUrlAnonymousAccessParameters = (url: string): string => {
if (!this.state.anonymousAccessParameters || !this.state.usePublicUrl) {
return url;
}
const parsedUrl = new URL(url);
for (const [name, value] of Object.entries(this.state.anonymousAccessParameters)) {
parsedUrl.searchParams.set(name, value);
}
return parsedUrl.toString();
};
private getUrlParamExtensions = (url: string): string => {
const { urlParams } = this.state;
return urlParams
@ -232,7 +300,8 @@ export class UrlPanelContent extends Component<Props, State> {
};
private setUrl = () => {
let url;
let url: string | undefined;
if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) {
url = this.getSavedObjectUrl();
} else if (this.state.useShortUrl) {
@ -241,6 +310,10 @@ export class UrlPanelContent extends Component<Props, State> {
url = this.getSnapshotUrl();
}
if (url) {
url = this.addUrlAnonymousAccessParameters(url);
}
if (this.props.isEmbedded) {
url = this.makeIframeTag(url);
}
@ -269,6 +342,14 @@ export class UrlPanelContent extends Component<Props, State> {
this.createShortUrl();
};
private handlePublicUrlChange = () => {
this.setState(({ usePublicUrl }) => {
return {
usePublicUrl: !usePublicUrl,
};
}, this.setUrl);
};
private createShortUrl = async () => {
this.setState({
isCreatingShortUrl: true,
@ -280,33 +361,38 @@ export class UrlPanelContent extends Component<Props, State> {
basePath: this.props.basePath,
post: this.props.post,
});
if (this.mounted) {
this.shortUrlCache = shortUrl;
this.setState(
{
isCreatingShortUrl: false,
useShortUrl: true,
},
this.setUrl
);
if (!this.mounted) {
return;
}
this.shortUrlCache = shortUrl;
this.setState(
{
isCreatingShortUrl: false,
useShortUrl: true,
},
this.setUrl
);
} catch (fetchError) {
if (this.mounted) {
this.shortUrlCache = undefined;
this.setState(
{
useShortUrl: false,
isCreatingShortUrl: false,
shortUrlErrorMsg: i18n.translate('share.urlPanel.unableCreateShortUrlErrorMessage', {
defaultMessage: 'Unable to create short URL. Error: {errorMessage}',
values: {
errorMessage: fetchError.message,
},
}),
},
this.setUrl
);
if (!this.mounted) {
return;
}
this.shortUrlCache = undefined;
this.setState(
{
useShortUrl: false,
isCreatingShortUrl: false,
shortUrlErrorMsg: i18n.translate('share.urlPanel.unableCreateShortUrlErrorMessage', {
defaultMessage: 'Unable to create short URL. Error: {errorMessage}',
values: {
errorMessage: fetchError.message,
},
}),
},
this.setUrl
);
}
};
@ -421,6 +507,36 @@ export class UrlPanelContent extends Component<Props, State> {
);
};
private renderPublicUrlSwitch = () => {
if (!this.state.anonymousAccessParameters || !this.state.showPublicUrlSwitch) {
return null;
}
const switchLabel = (
<FormattedMessage id="share.urlPanel.publicUrlLabel" defaultMessage="Public URL" />
);
const switchComponent = (
<EuiSwitch
label={switchLabel}
checked={this.state.usePublicUrl}
onChange={this.handlePublicUrlChange}
data-test-subj="usePublicUrl"
/>
);
const tipContent = (
<FormattedMessage
id="share.urlPanel.publicUrlHelpText"
defaultMessage="Use public URL to share with anyone. It enables one-step anonymous access by removing the login prompt."
/>
);
return (
<EuiFormRow data-test-subj="createPublicUrl">
{this.renderWithIconTip(switchComponent, tipContent)}
</EuiFormRow>
);
};
private renderUrlParamExtensions = (): ReactElement | void => {
if (!this.props.urlParamExtensions) {
return;

View file

@ -10,6 +10,7 @@ import { registryMock, managerMock } from './plugin.test.mocks';
import { SharePlugin } from './plugin';
import { CoreStart } from 'kibana/public';
import { coreMock } from '../../../core/public/mocks';
import { mockSecurityOssPlugin } from '../../security_oss/public/mocks';
describe('SharePlugin', () => {
beforeEach(() => {
@ -21,14 +22,20 @@ describe('SharePlugin', () => {
describe('setup', () => {
test('wires up and returns registry', async () => {
const coreSetup = coreMock.createSetup();
const setup = await new SharePlugin().setup(coreSetup);
const plugins = {
securityOss: mockSecurityOssPlugin.createSetup(),
};
const setup = await new SharePlugin().setup(coreSetup, plugins);
expect(registryMock.setup).toHaveBeenCalledWith();
expect(setup.register).toBeDefined();
});
test('registers redirect app', async () => {
const coreSetup = coreMock.createSetup();
await new SharePlugin().setup(coreSetup);
const plugins = {
securityOss: mockSecurityOssPlugin.createSetup(),
};
await new SharePlugin().setup(coreSetup, plugins);
expect(coreSetup.application.register).toHaveBeenCalledWith(
expect.objectContaining({
id: 'short_url_redirect',
@ -40,13 +47,22 @@ describe('SharePlugin', () => {
describe('start', () => {
test('wires up and returns show function, but not registry', async () => {
const coreSetup = coreMock.createSetup();
const pluginsSetup = {
securityOss: mockSecurityOssPlugin.createSetup(),
};
const service = new SharePlugin();
await service.setup(coreSetup);
const start = await service.start({} as CoreStart);
await service.setup(coreSetup, pluginsSetup);
const pluginsStart = {
securityOss: mockSecurityOssPlugin.createStart(),
};
const start = await service.start({} as CoreStart, pluginsStart);
expect(registryMock.start).toHaveBeenCalled();
expect(managerMock.start).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ getShareMenuItems: expect.any(Function) })
expect.objectContaining({
getShareMenuItems: expect.any(Function),
}),
expect.anything()
);
expect(start.toggleShareContextMenu).toBeDefined();
});

View file

@ -10,6 +10,7 @@ import './index.scss';
import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { ShareMenuManager, ShareMenuManagerStart } from './services';
import type { SecurityOssPluginSetup, SecurityOssPluginStart } from '../../security_oss/public';
import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services';
import { createShortUrlRedirectApp } from './services/short_url_redirect_app';
import {
@ -18,12 +19,20 @@ import {
UrlGeneratorsStart,
} from './url_generators/url_generator_service';
export interface ShareSetupDependencies {
securityOss?: SecurityOssPluginSetup;
}
export interface ShareStartDependencies {
securityOss?: SecurityOssPluginStart;
}
export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
private readonly shareMenuRegistry = new ShareMenuRegistry();
private readonly shareContextMenu = new ShareMenuManager();
private readonly urlGeneratorsService = new UrlGeneratorsService();
public setup(core: CoreSetup): SharePluginSetup {
public setup(core: CoreSetup, plugins: ShareSetupDependencies): SharePluginSetup {
core.application.register(createShortUrlRedirectApp(core, window.location));
return {
...this.shareMenuRegistry.setup(),
@ -31,9 +40,13 @@ export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
};
}
public start(core: CoreStart): SharePluginStart {
public start(core: CoreStart, plugins: ShareStartDependencies): SharePluginStart {
return {
...this.shareContextMenu.start(core, this.shareMenuRegistry.start()),
...this.shareContextMenu.start(
core,
this.shareMenuRegistry.start(),
plugins.securityOss?.anonymousAccess
),
urlGenerators: this.urlGeneratorsService.start(core),
};
}

View file

@ -15,13 +15,18 @@ import { CoreStart, HttpStart } from 'kibana/public';
import { ShareContextMenu } from '../components/share_context_menu';
import { ShareMenuItem, ShowShareMenuOptions } from '../types';
import { ShareMenuRegistryStart } from './share_menu_registry';
import type { SecurityOssPluginStart } from '../../../security_oss/public';
export class ShareMenuManager {
private isOpen = false;
private container = document.createElement('div');
start(core: CoreStart, shareRegistry: ShareMenuRegistryStart) {
start(
core: CoreStart,
shareRegistry: ShareMenuRegistryStart,
anonymousAccess?: SecurityOssPluginStart['anonymousAccess']
) {
return {
/**
* Collects share menu items from registered providers and mounts the share context menu under
@ -35,6 +40,7 @@ export class ShareMenuManager {
menuItems,
post: core.http.post,
basePath: core.http.basePath.get(),
anonymousAccess,
});
},
};
@ -57,10 +63,13 @@ export class ShareMenuManager {
post,
basePath,
embedUrlParamExtensions,
anonymousAccess,
showPublicUrlSwitch,
}: ShowShareMenuOptions & {
menuItems: ShareMenuItem[];
post: HttpStart['post'];
basePath: string;
anonymousAccess?: SecurityOssPluginStart['anonymousAccess'];
}) {
if (this.isOpen) {
this.onClose();
@ -92,6 +101,8 @@ export class ShareMenuManager {
post={post}
basePath={basePath}
embedUrlParamExtensions={embedUrlParamExtensions}
anonymousAccess={anonymousAccess}
showPublicUrlSwitch={showPublicUrlSwitch}
/>
</EuiWrappingPopover>
</I18nProvider>

View file

@ -9,6 +9,7 @@
import { ComponentType } from 'react';
import { EuiContextMenuPanelDescriptor } from '@elastic/eui';
import { EuiContextMenuPanelItemDescriptorEntry } from '@elastic/eui/src/components/context_menu/context_menu';
import type { Capabilities } from 'src/core/public';
/**
* @public
@ -35,6 +36,7 @@ export interface ShareContext {
sharingData: { [key: string]: unknown };
isDirty: boolean;
onClose: () => void;
showPublicUrlSwitch?: (anonymousUserCapabilities: Capabilities) => boolean;
}
/**

View file

@ -10,6 +10,7 @@
"include": ["common/**/*", "public/**/*", "server/**/*"],
"references": [
{ "path": "../../core/tsconfig.json" },
{ "path": "../../plugins/kibana_utils/tsconfig.json" }
{ "path": "../kibana_utils/tsconfig.json" },
{ "path": "../security_oss/tsconfig.json" }
]
}

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { Capabilities } from 'src/core/public';
import { showPublicUrlSwitch } from './get_top_nav_config';
describe('showPublicUrlSwitch', () => {
test('returns false if "visualize" app is not available', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
};
const result = showPublicUrlSwitch(anonymousUserCapabilities);
expect(result).toBe(false);
});
test('returns false if "visualize" app is not accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
visualize: {
show: false,
},
};
const result = showPublicUrlSwitch(anonymousUserCapabilities);
expect(result).toBe(false);
});
test('returns true if "visualize" app is not available an accessible', () => {
const anonymousUserCapabilities: Capabilities = {
catalogue: {},
management: {},
navLinks: {},
visualize: {
show: true,
},
};
const result = showPublicUrlSwitch(anonymousUserCapabilities);
expect(result).toBe(true);
});
});

View file

@ -9,6 +9,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { Capabilities } from 'src/core/public';
import { TopNavMenuData } from 'src/plugins/navigation/public';
import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from '../../../../visualizations/public';
import {
@ -30,6 +31,14 @@ import { VisualizeConstants } from '../visualize_constants';
import { getEditBreadcrumbs } from './breadcrumbs';
import { EmbeddableStateTransfer } from '../../../../embeddable/public';
interface VisualizeCapabilities {
createShortUrl: boolean;
delete: boolean;
save: boolean;
saveQuery: boolean;
show: boolean;
}
interface TopNavConfigParams {
hasUnsavedChanges: boolean;
setHasUnsavedChanges: (value: boolean) => void;
@ -45,6 +54,14 @@ interface TopNavConfigParams {
embeddableId?: string;
}
export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
if (!anonymousUserCapabilities.visualize) return false;
const visualize = (anonymousUserCapabilities.visualize as unknown) as VisualizeCapabilities;
return !!visualize.show;
};
export const getTopNavConfig = (
{
hasUnsavedChanges,
@ -243,6 +260,7 @@ export const getTopNavConfig = (
title: savedVis?.title,
},
isDirty: hasUnappliedChanges || hasUnsavedChanges,
showPublicUrlSwitch,
});
}
},