mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Kibana] New "Saved Query Management" privilege to allow saving queries across Kibana (#166937)
- Resolves https://github.com/elastic/kibana/issues/158173
Based on PoC https://github.com/elastic/kibana/pull/166260
## Summary
This PR adds a new "Saved Query Management" privilege with 2 options:
- `All` will override any per app privilege and will allow users to save
queries from any Kibana page
- `None` will default to per app privileges (backward-compatible option)
<img width="600" alt="Screenshot 2023-09-21 at 15 26 25"
src="6d53548e
-5c5a-4d6d-a86a-1e639cb77202">
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: Matthias Wilhelm <matthias.wilhelm@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
d0a0a1f9e6
commit
7fa04e92bc
40 changed files with 798 additions and 183 deletions
|
@ -310,6 +310,7 @@ enabled:
|
||||||
- x-pack/test/functional/apps/reporting_management/config.ts
|
- x-pack/test/functional/apps/reporting_management/config.ts
|
||||||
- x-pack/test/functional/apps/rollup_job/config.ts
|
- x-pack/test/functional/apps/rollup_job/config.ts
|
||||||
- x-pack/test/functional/apps/saved_objects_management/config.ts
|
- x-pack/test/functional/apps/saved_objects_management/config.ts
|
||||||
|
- x-pack/test/functional/apps/saved_query_management/config.ts
|
||||||
- x-pack/test/functional/apps/security/config.ts
|
- x-pack/test/functional/apps/security/config.ts
|
||||||
- x-pack/test/functional/apps/snapshot_restore/config.ts
|
- x-pack/test/functional/apps/snapshot_restore/config.ts
|
||||||
- x-pack/test/functional/apps/spaces/config.ts
|
- x-pack/test/functional/apps/spaces/config.ts
|
||||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -853,6 +853,7 @@ packages/kbn-yarn-lock-validator @elastic/kibana-operations
|
||||||
/x-pack/test/examples/search_examples @elastic/kibana-data-discovery
|
/x-pack/test/examples/search_examples @elastic/kibana-data-discovery
|
||||||
/x-pack/test/functional/apps/data_views @elastic/kibana-data-discovery
|
/x-pack/test/functional/apps/data_views @elastic/kibana-data-discovery
|
||||||
/x-pack/test/functional/apps/discover @elastic/kibana-data-discovery
|
/x-pack/test/functional/apps/discover @elastic/kibana-data-discovery
|
||||||
|
/x-pack/test/functional/apps/saved_query_management @elastic/kibana-data-discovery
|
||||||
/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover @elastic/kibana-data-discovery
|
/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover @elastic/kibana-data-discovery
|
||||||
/x-pack/test/search_sessions_integration @elastic/kibana-data-discovery
|
/x-pack/test/search_sessions_integration @elastic/kibana-data-discovery
|
||||||
/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @elastic/kibana-data-discovery
|
/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js @elastic/kibana-data-discovery
|
||||||
|
|
|
@ -97,7 +97,7 @@ export const App = ({
|
||||||
showSearchBar={true}
|
showSearchBar={true}
|
||||||
indexPatterns={[dataView]}
|
indexPatterns={[dataView]}
|
||||||
useDefaultBehaviors={true}
|
useDefaultBehaviors={true}
|
||||||
showSaveQuery={true}
|
saveQueryMenuVisibility="allowed_by_app_privilege" // allowed only for this example app, use `globally_managed` by default
|
||||||
/>
|
/>
|
||||||
<EuiPageTemplate.Section>
|
<EuiPageTemplate.Section>
|
||||||
<EuiText>
|
<EuiText>
|
||||||
|
|
|
@ -68,7 +68,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
navigation: { TopNavMenu },
|
navigation: { TopNavMenu },
|
||||||
embeddable: { getStateTransfer },
|
embeddable: { getStateTransfer },
|
||||||
initializerContext: { allowByValueEmbeddables },
|
initializerContext: { allowByValueEmbeddables },
|
||||||
dashboardCapabilities: { saveQuery: showSaveQuery, showWriteControls },
|
dashboardCapabilities: { saveQuery: allowSaveQuery, showWriteControls },
|
||||||
} = pluginServices.getServices();
|
} = pluginServices.getServices();
|
||||||
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
|
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
|
||||||
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
|
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
|
||||||
|
@ -298,7 +298,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
||||||
useDefaultBehaviors={true}
|
useDefaultBehaviors={true}
|
||||||
savedQueryId={savedQueryId}
|
savedQueryId={savedQueryId}
|
||||||
indexPatterns={allDataViews}
|
indexPatterns={allDataViews}
|
||||||
showSaveQuery={showSaveQuery}
|
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
|
||||||
appName={LEGACY_DASHBOARD_APP_ID}
|
appName={LEGACY_DASHBOARD_APP_ID}
|
||||||
visible={viewMode !== ViewMode.PRINT}
|
visible={viewMode !== ViewMode.PRINT}
|
||||||
setMenuMountPoint={embedSettings || fullScreenMode ? undefined : setHeaderActionMenu}
|
setMenuMountPoint={embedSettings || fullScreenMode ? undefined : setHeaderActionMenu}
|
||||||
|
|
|
@ -88,7 +88,7 @@ describe('ContextApp test', () => {
|
||||||
showSearchBar: true,
|
showSearchBar: true,
|
||||||
showQueryInput: false,
|
showQueryInput: false,
|
||||||
showFilterBar: true,
|
showFilterBar: true,
|
||||||
showSaveQuery: false,
|
saveQueryMenuVisibility: 'hidden' as const,
|
||||||
showDatePicker: false,
|
showDatePicker: false,
|
||||||
indexPatterns: [dataViewMock],
|
indexPatterns: [dataViewMock],
|
||||||
useDefaultBehaviors: true,
|
useDefaultBehaviors: true,
|
||||||
|
|
|
@ -207,7 +207,7 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
|
||||||
showSearchBar: true,
|
showSearchBar: true,
|
||||||
showQueryInput: false,
|
showQueryInput: false,
|
||||||
showFilterBar: true,
|
showFilterBar: true,
|
||||||
showSaveQuery: false,
|
saveQueryMenuVisibility: 'hidden' as const,
|
||||||
showDatePicker: false,
|
showDatePicker: false,
|
||||||
indexPatterns: [dataView],
|
indexPatterns: [dataView],
|
||||||
useDefaultBehaviors: true,
|
useDefaultBehaviors: true,
|
||||||
|
|
|
@ -53,8 +53,20 @@ jest.mock('../../../../customizations', () => ({
|
||||||
useDiscoverCustomization: jest.fn(),
|
useDiscoverCustomization: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function getProps(savePermissions = true): DiscoverTopNavProps {
|
const mockDefaultCapabilities = {
|
||||||
mockDiscoverService.capabilities.discover!.save = savePermissions;
|
discover: { save: true },
|
||||||
|
} as unknown as typeof mockDiscoverService.capabilities;
|
||||||
|
|
||||||
|
function getProps(
|
||||||
|
{
|
||||||
|
capabilities,
|
||||||
|
}: {
|
||||||
|
capabilities?: Partial<typeof mockDiscoverService.capabilities>;
|
||||||
|
} = { capabilities: mockDefaultCapabilities }
|
||||||
|
): DiscoverTopNavProps {
|
||||||
|
if (capabilities) {
|
||||||
|
mockDiscoverService.capabilities = capabilities as typeof mockDiscoverService.capabilities;
|
||||||
|
}
|
||||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||||
stateContainer.internalState.transitions.setDataView(dataViewMock);
|
stateContainer.internalState.transitions.setDataView(dataViewMock);
|
||||||
|
|
||||||
|
@ -93,7 +105,7 @@ describe('Discover topnav component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generated config of TopNavMenu config is correct when discover save permissions are assigned', () => {
|
test('generated config of TopNavMenu config is correct when discover save permissions are assigned', () => {
|
||||||
const props = getProps(true);
|
const props = getProps({ capabilities: { discover: { save: true } } });
|
||||||
const component = mountWithIntl(
|
const component = mountWithIntl(
|
||||||
<DiscoverMainProvider value={props.stateContainer}>
|
<DiscoverMainProvider value={props.stateContainer}>
|
||||||
<DiscoverTopNav {...props} />
|
<DiscoverTopNav {...props} />
|
||||||
|
@ -105,7 +117,7 @@ describe('Discover topnav component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generated config of TopNavMenu config is correct when no discover save permissions are assigned', () => {
|
test('generated config of TopNavMenu config is correct when no discover save permissions are assigned', () => {
|
||||||
const props = getProps(false);
|
const props = getProps({ capabilities: { discover: { save: false } } });
|
||||||
const component = mountWithIntl(
|
const component = mountWithIntl(
|
||||||
<DiscoverMainProvider value={props.stateContainer}>
|
<DiscoverMainProvider value={props.stateContainer}>
|
||||||
<DiscoverTopNav {...props} />
|
<DiscoverTopNav {...props} />
|
||||||
|
@ -116,6 +128,32 @@ describe('Discover topnav component', () => {
|
||||||
expect(topMenuConfig).toEqual(['new', 'open', 'share', 'inspect']);
|
expect(topMenuConfig).toEqual(['new', 'open', 'share', 'inspect']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('top nav is correct when discover saveQuery permission is granted', () => {
|
||||||
|
const props = getProps({ capabilities: { discover: { saveQuery: true } } });
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<DiscoverMainProvider value={props.stateContainer}>
|
||||||
|
<DiscoverTopNav {...props} />
|
||||||
|
</DiscoverMainProvider>
|
||||||
|
);
|
||||||
|
const statefulSearchBar = component.find(
|
||||||
|
mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu
|
||||||
|
);
|
||||||
|
expect(statefulSearchBar.props().saveQueryMenuVisibility).toBe('allowed_by_app_privilege');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('top nav is correct when discover saveQuery permission is not granted', () => {
|
||||||
|
const props = getProps({ capabilities: { discover: { saveQuery: false } } });
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<DiscoverMainProvider value={props.stateContainer}>
|
||||||
|
<DiscoverTopNav {...props} />
|
||||||
|
</DiscoverMainProvider>
|
||||||
|
);
|
||||||
|
const statefulSearchBar = component.find(
|
||||||
|
mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu
|
||||||
|
);
|
||||||
|
expect(statefulSearchBar.props().saveQueryMenuVisibility).toBe('globally_managed');
|
||||||
|
});
|
||||||
|
|
||||||
describe('top nav customization', () => {
|
describe('top nav customization', () => {
|
||||||
it('should call getMenuItems', () => {
|
it('should call getMenuItems', () => {
|
||||||
mockUseCustomizations = true;
|
mockUseCustomizations = true;
|
||||||
|
|
|
@ -220,7 +220,9 @@ export const DiscoverTopNav = ({
|
||||||
savedQueryId={savedQuery}
|
savedQueryId={savedQuery}
|
||||||
screenTitle={savedSearch.title}
|
screenTitle={savedSearch.title}
|
||||||
showDatePicker={showDatePicker}
|
showDatePicker={showDatePicker}
|
||||||
showSaveQuery={!isPlainRecord && Boolean(services.capabilities.discover.saveQuery)}
|
saveQueryMenuVisibility={
|
||||||
|
services.capabilities.discover.saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
|
||||||
|
}
|
||||||
showSearchBar={true}
|
showSearchBar={true}
|
||||||
useDefaultBehaviors={true}
|
useDefaultBehaviors={true}
|
||||||
dataViewPickerOverride={
|
dataViewPickerOverride={
|
||||||
|
|
|
@ -20,7 +20,7 @@ import classNames from 'classnames';
|
||||||
import { MountPoint } from '@kbn/core/public';
|
import { MountPoint } from '@kbn/core/public';
|
||||||
import { MountPointPortal } from '@kbn/kibana-react-plugin/public';
|
import { MountPointPortal } from '@kbn/kibana-react-plugin/public';
|
||||||
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||||
import { StatefulSearchBarProps, SearchBarProps } from '@kbn/unified-search-plugin/public';
|
import { StatefulSearchBarProps } from '@kbn/unified-search-plugin/public';
|
||||||
import { AggregateQuery, Query } from '@kbn/es-query';
|
import { AggregateQuery, Query } from '@kbn/es-query';
|
||||||
import { TopNavMenuData } from './top_nav_menu_data';
|
import { TopNavMenuData } from './top_nav_menu_data';
|
||||||
import { TopNavMenuItem } from './top_nav_menu_item';
|
import { TopNavMenuItem } from './top_nav_menu_item';
|
||||||
|
@ -30,38 +30,39 @@ type Badge = EuiBadgeProps & {
|
||||||
toolTipProps?: Partial<EuiToolTipProps>;
|
toolTipProps?: Partial<EuiToolTipProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TopNavMenuProps<QT extends Query | AggregateQuery = Query> =
|
export type TopNavMenuProps<QT extends Query | AggregateQuery = Query> = Omit<
|
||||||
StatefulSearchBarProps<QT> &
|
StatefulSearchBarProps<QT>,
|
||||||
Omit<SearchBarProps<QT>, 'kibana' | 'intl' | 'timeHistory'> & {
|
'kibana' | 'intl' | 'timeHistory'
|
||||||
config?: TopNavMenuData[];
|
> & {
|
||||||
badges?: Badge[];
|
config?: TopNavMenuData[];
|
||||||
showSearchBar?: boolean;
|
badges?: Badge[];
|
||||||
showQueryInput?: boolean;
|
showSearchBar?: boolean;
|
||||||
showDatePicker?: boolean;
|
showQueryInput?: boolean;
|
||||||
showFilterBar?: boolean;
|
showDatePicker?: boolean;
|
||||||
unifiedSearch?: UnifiedSearchPublicPluginStart;
|
showFilterBar?: boolean;
|
||||||
className?: string;
|
unifiedSearch?: UnifiedSearchPublicPluginStart;
|
||||||
visible?: boolean;
|
className?: string;
|
||||||
/**
|
visible?: boolean;
|
||||||
* If provided, the menu part of the component will be rendered as a portal inside the given mount point.
|
/**
|
||||||
*
|
* If provided, the menu part of the component will be rendered as a portal inside the given mount point.
|
||||||
* This is meant to be used with the `setHeaderActionMenu` core API.
|
*
|
||||||
*
|
* This is meant to be used with the `setHeaderActionMenu` core API.
|
||||||
* @example
|
*
|
||||||
* ```ts
|
* @example
|
||||||
* export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => {
|
* ```ts
|
||||||
* const topNavConfig = ...; // TopNavMenuProps
|
* export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => {
|
||||||
* return (
|
* const topNavConfig = ...; // TopNavMenuProps
|
||||||
* <Router history=history>
|
* return (
|
||||||
* <TopNavMenu {...topNavConfig} setMenuMountPoint={setHeaderActionMenu}>
|
* <Router history=history>
|
||||||
* <MyRoutes />
|
* <TopNavMenu {...topNavConfig} setMenuMountPoint={setHeaderActionMenu}>
|
||||||
* </Router>
|
* <MyRoutes />
|
||||||
* )
|
* </Router>
|
||||||
* }
|
* )
|
||||||
* ```
|
* }
|
||||||
*/
|
* ```
|
||||||
setMenuMountPoint?: (menuMount: MountPoint | undefined) => void;
|
*/
|
||||||
};
|
setMenuMountPoint?: (menuMount: MountPoint | undefined) => void;
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Top Nav Menu is a convenience wrapper component for:
|
* Top Nav Menu is a convenience wrapper component for:
|
||||||
|
|
|
@ -174,11 +174,20 @@ const services = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultCapabilities = {
|
||||||
|
savedObjectsManagement: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
setIndexPatterns({
|
setIndexPatterns({
|
||||||
get: () => Promise.resolve(mockIndexPatterns[0]),
|
get: () => Promise.resolve(mockIndexPatterns[0]),
|
||||||
} as unknown as DataViewsContract);
|
} as unknown as DataViewsContract);
|
||||||
|
|
||||||
function wrapSearchBarInContext(testProps: SearchBarProps<Query>) {
|
function wrapSearchBarInContext(
|
||||||
|
testProps: Partial<SearchBarProps<Query>>,
|
||||||
|
capabilities: typeof defaultCapabilities = defaultCapabilities
|
||||||
|
) {
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
appName: 'test',
|
appName: 'test',
|
||||||
timeHistory: mockTimeHistory,
|
timeHistory: mockTimeHistory,
|
||||||
|
@ -197,9 +206,16 @@ function wrapSearchBarInContext(testProps: SearchBarProps<Query>) {
|
||||||
onFiltersUpdated: action('onFiltersUpdated'),
|
onFiltersUpdated: action('onFiltersUpdated'),
|
||||||
} as unknown as SearchBarProps<Query>;
|
} as unknown as SearchBarProps<Query>;
|
||||||
|
|
||||||
|
const kbnServices = {
|
||||||
|
...services,
|
||||||
|
application: {
|
||||||
|
capabilities,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<KibanaContextProvider services={services}>
|
<KibanaContextProvider services={kbnServices}>
|
||||||
<SearchBar<Query> {...defaultOptions} {...testProps} />
|
<SearchBar<Query> {...defaultOptions} {...testProps} />
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
|
@ -219,7 +235,7 @@ storiesOf('SearchBar', module)
|
||||||
},
|
},
|
||||||
onChangeDataView: action('onChangeDataView'),
|
onChangeDataView: action('onChangeDataView'),
|
||||||
},
|
},
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('with dataviewPicker enhanced', () =>
|
.add('with dataviewPicker enhanced', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
|
@ -234,41 +250,56 @@ storiesOf('SearchBar', module)
|
||||||
onAddField: action('onAddField'),
|
onAddField: action('onAddField'),
|
||||||
onDataViewCreated: action('onDataViewCreated'),
|
onDataViewCreated: action('onDataViewCreated'),
|
||||||
},
|
},
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('with filterBar off', () =>
|
.add('with filterBar off', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
showFilterBar: false,
|
showFilterBar: false,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('with query input off', () =>
|
.add('with query input off', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
showQueryInput: false,
|
showQueryInput: false,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('with date picker off', () =>
|
.add('with date picker off', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
showDatePicker: false,
|
showDatePicker: false,
|
||||||
} as SearchBarProps)
|
})
|
||||||
|
)
|
||||||
|
.add('with disabled "Save query" menu', () =>
|
||||||
|
wrapSearchBarInContext({
|
||||||
|
showSaveQuery: false,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.add('with hidden "Manage saved objects" link in "Load saved query" menu', () =>
|
||||||
|
wrapSearchBarInContext(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
savedObjectsManagement: {
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.add('with the default date picker auto refresh interval on', () =>
|
.add('with the default date picker auto refresh interval on', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
showDatePicker: true,
|
showDatePicker: true,
|
||||||
onRefreshChange: action('onRefreshChange'),
|
onRefreshChange: action('onRefreshChange'),
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('with the default date picker auto refresh interval off', () =>
|
.add('with the default date picker auto refresh interval off', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
showDatePicker: true,
|
showDatePicker: true,
|
||||||
isAutoRefreshDisabled: true,
|
isAutoRefreshDisabled: true,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('with only the date picker on', () =>
|
.add('with only the date picker on', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
showDatePicker: true,
|
showDatePicker: true,
|
||||||
showFilterBar: false,
|
showFilterBar: false,
|
||||||
showQueryInput: false,
|
showQueryInput: false,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('with additional filters used for suggestions', () =>
|
.add('with additional filters used for suggestions', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
|
@ -470,12 +501,12 @@ storiesOf('SearchBar', module)
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
showQueryInput: true,
|
showQueryInput: true,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('without switch query language', () =>
|
.add('without switch query language', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
disableQueryLanguageSwitcher: true,
|
disableQueryLanguageSwitcher: true,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('show only query bar without submit', () =>
|
.add('show only query bar without submit', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
|
@ -484,7 +515,7 @@ storiesOf('SearchBar', module)
|
||||||
showAutoRefreshOnly: false,
|
showAutoRefreshOnly: false,
|
||||||
showQueryInput: true,
|
showQueryInput: true,
|
||||||
showSubmitButton: false,
|
showSubmitButton: false,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('show only datepicker without submit', () =>
|
.add('show only datepicker without submit', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
|
@ -493,7 +524,7 @@ storiesOf('SearchBar', module)
|
||||||
showAutoRefreshOnly: false,
|
showAutoRefreshOnly: false,
|
||||||
showQueryInput: false,
|
showQueryInput: false,
|
||||||
showSubmitButton: false,
|
showSubmitButton: false,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('show only query bar and timepicker without submit', () =>
|
.add('show only query bar and timepicker without submit', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
|
@ -502,7 +533,7 @@ storiesOf('SearchBar', module)
|
||||||
showAutoRefreshOnly: false,
|
showAutoRefreshOnly: false,
|
||||||
showQueryInput: true,
|
showQueryInput: true,
|
||||||
showSubmitButton: false,
|
showSubmitButton: false,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('with filter bar on but pinning option is hidden from menus', () =>
|
.add('with filter bar on but pinning option is hidden from menus', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
|
@ -621,7 +652,7 @@ storiesOf('SearchBar', module)
|
||||||
onChangeDataView: action('onChangeDataView'),
|
onChangeDataView: action('onChangeDataView'),
|
||||||
},
|
},
|
||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('no submit button', () =>
|
.add('no submit button', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
|
@ -635,7 +666,7 @@ storiesOf('SearchBar', module)
|
||||||
onChangeDataView: action('onChangeDataView'),
|
onChangeDataView: action('onChangeDataView'),
|
||||||
},
|
},
|
||||||
showSubmitButton: false,
|
showSubmitButton: false,
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('submit button always as icon', () =>
|
.add('submit button always as icon', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
|
@ -649,7 +680,7 @@ storiesOf('SearchBar', module)
|
||||||
onChangeDataView: action('onChangeDataView'),
|
onChangeDataView: action('onChangeDataView'),
|
||||||
},
|
},
|
||||||
submitButtonStyle: 'iconOnly',
|
submitButtonStyle: 'iconOnly',
|
||||||
} as SearchBarProps)
|
})
|
||||||
)
|
)
|
||||||
.add('submit button always as a full button', () =>
|
.add('submit button always as a full button', () =>
|
||||||
wrapSearchBarInContext({
|
wrapSearchBarInContext({
|
||||||
|
@ -663,5 +694,5 @@ storiesOf('SearchBar', module)
|
||||||
onChangeDataView: action('onChangeDataView'),
|
onChangeDataView: action('onChangeDataView'),
|
||||||
},
|
},
|
||||||
submitButtonStyle: 'full',
|
submitButtonStyle: 'full',
|
||||||
} as SearchBarProps)
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { useFilterManager } from './lib/use_filter_manager';
|
||||||
import { useTimefilter } from './lib/use_timefilter';
|
import { useTimefilter } from './lib/use_timefilter';
|
||||||
import { useSavedQuery } from './lib/use_saved_query';
|
import { useSavedQuery } from './lib/use_saved_query';
|
||||||
import { useQueryStringManager } from './lib/use_query_string_manager';
|
import { useQueryStringManager } from './lib/use_query_string_manager';
|
||||||
|
import { type SavedQueryMenuVisibility, canShowSavedQuery } from './lib/can_show_saved_query';
|
||||||
import type { UnifiedSearchPublicPluginStart } from '../types';
|
import type { UnifiedSearchPublicPluginStart } from '../types';
|
||||||
|
|
||||||
export interface StatefulSearchBarDeps {
|
export interface StatefulSearchBarDeps {
|
||||||
|
@ -32,14 +33,17 @@ export interface StatefulSearchBarDeps {
|
||||||
unifiedSearch: Omit<UnifiedSearchPublicPluginStart, 'ui'>;
|
unifiedSearch: Omit<UnifiedSearchPublicPluginStart, 'ui'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StatefulSearchBarProps<QT extends Query | AggregateQuery = Query> =
|
export type StatefulSearchBarProps<QT extends Query | AggregateQuery = Query> = Omit<
|
||||||
SearchBarOwnProps<QT> & {
|
SearchBarOwnProps<QT>,
|
||||||
appName: string;
|
'showSaveQuery'
|
||||||
useDefaultBehaviors?: boolean;
|
> & {
|
||||||
savedQueryId?: string;
|
appName: string;
|
||||||
onSavedQueryIdChange?: (savedQueryId?: string) => void;
|
useDefaultBehaviors?: boolean;
|
||||||
onFiltersUpdated?: (filters: Filter[]) => void;
|
savedQueryId?: string;
|
||||||
};
|
saveQueryMenuVisibility?: SavedQueryMenuVisibility;
|
||||||
|
onSavedQueryIdChange?: (savedQueryId?: string) => void;
|
||||||
|
onFiltersUpdated?: (filters: Filter[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
// Respond to user changing the filters
|
// Respond to user changing the filters
|
||||||
const defaultFiltersUpdated = (
|
const defaultFiltersUpdated = (
|
||||||
|
@ -194,6 +198,12 @@ export function createSearchBar({
|
||||||
);
|
);
|
||||||
}, [query, timeRange, useDefaultBehaviors]);
|
}, [query, timeRange, useDefaultBehaviors]);
|
||||||
|
|
||||||
|
const showSaveQuery = canShowSavedQuery({
|
||||||
|
saveQueryMenuVisibility: props.saveQueryMenuVisibility,
|
||||||
|
query,
|
||||||
|
core,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KibanaContextProvider
|
<KibanaContextProvider
|
||||||
services={{
|
services={{
|
||||||
|
@ -212,7 +222,7 @@ export function createSearchBar({
|
||||||
showFilterBar={props.showFilterBar}
|
showFilterBar={props.showFilterBar}
|
||||||
showQueryMenu={props.showQueryMenu}
|
showQueryMenu={props.showQueryMenu}
|
||||||
showQueryInput={props.showQueryInput}
|
showQueryInput={props.showQueryInput}
|
||||||
showSaveQuery={props.showSaveQuery}
|
showSaveQuery={showSaveQuery}
|
||||||
showSubmitButton={props.showSubmitButton}
|
showSubmitButton={props.showSubmitButton}
|
||||||
submitButtonStyle={props.submitButtonStyle}
|
submitButtonStyle={props.submitButtonStyle}
|
||||||
isDisabled={props.isDisabled}
|
isDisabled={props.isDisabled}
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { coreMock } from '@kbn/core/public/mocks';
|
||||||
|
import { canShowSavedQuery } from './can_show_saved_query';
|
||||||
|
|
||||||
|
const core = coreMock.createStart();
|
||||||
|
|
||||||
|
function getCore(saveQueryGloballyAllowed: boolean): typeof core {
|
||||||
|
return {
|
||||||
|
...core,
|
||||||
|
application: {
|
||||||
|
...core.application,
|
||||||
|
capabilities: {
|
||||||
|
...core.application.capabilities,
|
||||||
|
savedQueryManagement: { saveQuery: saveQueryGloballyAllowed },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const coreWithoutGlobalPrivilege = getCore(false);
|
||||||
|
const coreWithGlobalPrivilege = getCore(true);
|
||||||
|
|
||||||
|
const kqlQuery = {
|
||||||
|
query: 'response:200',
|
||||||
|
language: 'kuery',
|
||||||
|
};
|
||||||
|
|
||||||
|
const esqlQuery = {
|
||||||
|
esql: 'from test | limit 10',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('canShowSaveQuery', () => {
|
||||||
|
it('should allow when allowed_by_app_privilege', async () => {
|
||||||
|
expect(
|
||||||
|
canShowSavedQuery({
|
||||||
|
core: coreWithoutGlobalPrivilege,
|
||||||
|
query: kqlQuery,
|
||||||
|
saveQueryMenuVisibility: 'allowed_by_app_privilege',
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow for text-based queries when allowed_by_app_privilege', async () => {
|
||||||
|
expect(
|
||||||
|
canShowSavedQuery({
|
||||||
|
core: coreWithoutGlobalPrivilege,
|
||||||
|
query: esqlQuery,
|
||||||
|
saveQueryMenuVisibility: 'allowed_by_app_privilege',
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow for text-based queries when globally_managed', async () => {
|
||||||
|
expect(
|
||||||
|
canShowSavedQuery({
|
||||||
|
core: coreWithGlobalPrivilege,
|
||||||
|
query: esqlQuery,
|
||||||
|
saveQueryMenuVisibility: 'globally_managed',
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow when globally allowed', async () => {
|
||||||
|
expect(
|
||||||
|
canShowSavedQuery({
|
||||||
|
core: coreWithGlobalPrivilege,
|
||||||
|
query: kqlQuery,
|
||||||
|
saveQueryMenuVisibility: 'globally_managed',
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow when globally disallowed', async () => {
|
||||||
|
expect(
|
||||||
|
canShowSavedQuery({
|
||||||
|
core: coreWithoutGlobalPrivilege,
|
||||||
|
query: kqlQuery,
|
||||||
|
saveQueryMenuVisibility: 'globally_managed',
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow when hidden', async () => {
|
||||||
|
expect(
|
||||||
|
canShowSavedQuery({
|
||||||
|
core: coreWithGlobalPrivilege,
|
||||||
|
query: kqlQuery,
|
||||||
|
saveQueryMenuVisibility: 'hidden',
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
canShowSavedQuery({
|
||||||
|
core: coreWithGlobalPrivilege,
|
||||||
|
query: kqlQuery,
|
||||||
|
saveQueryMenuVisibility: undefined,
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { AggregateQuery, Query } from '@kbn/es-query';
|
||||||
|
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||||
|
import { isOfAggregateQueryType } from '@kbn/es-query';
|
||||||
|
|
||||||
|
export type SavedQueryMenuVisibility =
|
||||||
|
| 'hidden'
|
||||||
|
| 'globally_managed' // managed by "Saved Query Management" global privilege
|
||||||
|
| 'allowed_by_app_privilege'; // use only if your Kibana app grants this privilege, otherwise default to `globally_managed`
|
||||||
|
|
||||||
|
export const canShowSavedQuery = ({
|
||||||
|
saveQueryMenuVisibility = 'hidden',
|
||||||
|
query,
|
||||||
|
core,
|
||||||
|
}: {
|
||||||
|
saveQueryMenuVisibility?: SavedQueryMenuVisibility;
|
||||||
|
query: AggregateQuery | Query | { [key: string]: any };
|
||||||
|
core: CoreStart;
|
||||||
|
}): boolean => {
|
||||||
|
// don't show Saved Query menu by default
|
||||||
|
if (!saveQueryMenuVisibility || saveQueryMenuVisibility === 'hidden') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saved Queries are not supported for text-based languages (only Saved Searches)
|
||||||
|
if (isOfAggregateQueryType(query)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAllowedGlobally = Boolean(core.application.capabilities.savedQueryManagement?.saveQuery);
|
||||||
|
|
||||||
|
// users can allow saving queries globally or grant permission per app
|
||||||
|
if (saveQueryMenuVisibility === 'allowed_by_app_privilege') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAllowedGlobally;
|
||||||
|
};
|
|
@ -68,7 +68,7 @@ export interface SearchBarOwnProps<QT extends AggregateQuery | Query = Query> {
|
||||||
dateRangeTo?: string;
|
dateRangeTo?: string;
|
||||||
// Query bar - should be in SearchBarInjectedDeps
|
// Query bar - should be in SearchBarInjectedDeps
|
||||||
query?: QT | Query;
|
query?: QT | Query;
|
||||||
// Show when user has privileges to save
|
// Show when user has privileges to save. See `canShowSavedQuery(...)` lib.
|
||||||
showSaveQuery?: boolean;
|
showSaveQuery?: boolean;
|
||||||
savedQuery?: SavedQuery;
|
savedQuery?: SavedQuery;
|
||||||
onQueryChange?: (payload: { dateRange: TimeRange; query?: QT | Query }) => void;
|
onQueryChange?: (payload: { dateRange: TimeRange; query?: QT | Query }) => void;
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
"@kbn/text-based-languages",
|
"@kbn/text-based-languages",
|
||||||
"@kbn/text-based-editor",
|
"@kbn/text-based-editor",
|
||||||
"@kbn/core-doc-links-browser",
|
"@kbn/core-doc-links-browser",
|
||||||
|
"@kbn/core-lifecycle-browser",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -307,7 +307,9 @@ const TopNav = ({
|
||||||
showDatePicker={showDatePicker()}
|
showDatePicker={showDatePicker()}
|
||||||
showFilterBar={showFilterBar}
|
showFilterBar={showFilterBar}
|
||||||
showQueryInput={showQueryInput}
|
showQueryInput={showQueryInput}
|
||||||
showSaveQuery={Boolean(services.visualizeCapabilities.saveQuery)}
|
saveQueryMenuVisibility={
|
||||||
|
services.visualizeCapabilities.saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
|
||||||
|
}
|
||||||
dataViewPickerComponentProps={
|
dataViewPickerComponentProps={
|
||||||
shouldShowDataViewPicker && vis.data.indexPattern
|
shouldShowDataViewPicker && vis.data.indexPattern
|
||||||
? {
|
? {
|
||||||
|
@ -346,7 +348,7 @@ const TopNav = ({
|
||||||
setMenuMountPoint={setHeaderActionMenu}
|
setMenuMountPoint={setHeaderActionMenu}
|
||||||
indexPatterns={indexPatterns}
|
indexPatterns={indexPatterns}
|
||||||
showSearchBar
|
showSearchBar
|
||||||
showSaveQuery={false}
|
saveQueryMenuVisibility="hidden"
|
||||||
showDatePicker={false}
|
showDatePicker={false}
|
||||||
showQueryInput={false}
|
showQueryInput={false}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const FindingsSearchBar = ({
|
||||||
showFilterBar={true}
|
showFilterBar={true}
|
||||||
showQueryInput={true}
|
showQueryInput={true}
|
||||||
showDatePicker={false}
|
showDatePicker={false}
|
||||||
showSaveQuery={false}
|
saveQueryMenuVisibility="hidden"
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
indexPatterns={[dataView]}
|
indexPatterns={[dataView]}
|
||||||
onQuerySubmit={setQuery}
|
onQuerySubmit={setQuery}
|
||||||
|
|
|
@ -177,6 +177,10 @@ Array [
|
||||||
"id": "savedObjectsManagement",
|
"id": "savedObjectsManagement",
|
||||||
"subFeatures": undefined,
|
"subFeatures": undefined,
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"id": "savedQueryManagement",
|
||||||
|
"subFeatures": undefined,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -469,6 +473,10 @@ Array [
|
||||||
"id": "savedObjectsManagement",
|
"id": "savedObjectsManagement",
|
||||||
"subFeatures": undefined,
|
"subFeatures": undefined,
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"id": "savedQueryManagement",
|
||||||
|
"subFeatures": undefined,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -975,6 +983,29 @@ Array [
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`buildOSSFeatures with a basic license returns the savedQueryManagement feature augmented with appropriate sub feature privileges 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"privilege": Object {
|
||||||
|
"app": Array [
|
||||||
|
"kibana",
|
||||||
|
],
|
||||||
|
"catalogue": Array [],
|
||||||
|
"savedObject": Object {
|
||||||
|
"all": Array [
|
||||||
|
"query",
|
||||||
|
],
|
||||||
|
"read": Array [],
|
||||||
|
},
|
||||||
|
"ui": Array [
|
||||||
|
"saveQuery",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"privilegeId": "all",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`buildOSSFeatures with a basic license returns the visualize feature augmented with appropriate sub feature privileges 1`] = `
|
exports[`buildOSSFeatures with a basic license returns the visualize feature augmented with appropriate sub feature privileges 1`] = `
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -1563,6 +1594,29 @@ Array [
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`buildOSSFeatures with a enterprise license returns the savedQueryManagement feature augmented with appropriate sub feature privileges 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"privilege": Object {
|
||||||
|
"app": Array [
|
||||||
|
"kibana",
|
||||||
|
],
|
||||||
|
"catalogue": Array [],
|
||||||
|
"savedObject": Object {
|
||||||
|
"all": Array [
|
||||||
|
"query",
|
||||||
|
],
|
||||||
|
"read": Array [],
|
||||||
|
},
|
||||||
|
"ui": Array [
|
||||||
|
"saveQuery",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"privilegeId": "all",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`buildOSSFeatures with a enterprise license returns the visualize feature augmented with appropriate sub feature privileges 1`] = `
|
exports[`buildOSSFeatures with a enterprise license returns the visualize feature augmented with appropriate sub feature privileges 1`] = `
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
|
|
|
@ -518,6 +518,31 @@ export const buildOSSFeatures = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'savedQueryManagement',
|
||||||
|
name: i18n.translate('xpack.features.savedQueryManagementFeatureName', {
|
||||||
|
defaultMessage: 'Saved Query Management',
|
||||||
|
}),
|
||||||
|
order: 1750,
|
||||||
|
category: DEFAULT_APP_CATEGORIES.management,
|
||||||
|
app: ['kibana'],
|
||||||
|
catalogue: [],
|
||||||
|
privilegesTooltip: i18n.translate('xpack.features.savedQueryManagementTooltip', {
|
||||||
|
defaultMessage:
|
||||||
|
'If set to "All", saved queries can be managed across Kibana in all applications that support them. If set to "None", saved query privileges will be determined independently by each application.',
|
||||||
|
}),
|
||||||
|
privileges: {
|
||||||
|
all: {
|
||||||
|
app: ['kibana'],
|
||||||
|
catalogue: [],
|
||||||
|
savedObject: {
|
||||||
|
all: ['query'],
|
||||||
|
read: [],
|
||||||
|
},
|
||||||
|
ui: ['saveQuery'],
|
||||||
|
}, // No read-only mode supported
|
||||||
|
},
|
||||||
|
},
|
||||||
] as KibanaFeatureConfig[];
|
] as KibanaFeatureConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,7 @@ describe('Features Plugin', () => {
|
||||||
"filesManagement",
|
"filesManagement",
|
||||||
"filesSharedImage",
|
"filesSharedImage",
|
||||||
"savedObjectsManagement",
|
"savedObjectsManagement",
|
||||||
|
"savedQueryManagement",
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -57,7 +57,11 @@ export const UnifiedSearchBar = () => {
|
||||||
defaultMessage: 'Search hosts (E.g. cloud.provider:gcp AND system.load.1 > 0.5)',
|
defaultMessage: 'Search hosts (E.g. cloud.provider:gcp AND system.load.1 > 0.5)',
|
||||||
})}
|
})}
|
||||||
onQuerySubmit={handleRefresh}
|
onQuerySubmit={handleRefresh}
|
||||||
showSaveQuery={Boolean(application?.capabilities?.visualize?.saveQuery)}
|
saveQueryMenuVisibility={
|
||||||
|
application?.capabilities?.visualize?.saveQuery
|
||||||
|
? 'allowed_by_app_privilege'
|
||||||
|
: 'globally_managed'
|
||||||
|
}
|
||||||
showDatePicker
|
showDatePicker
|
||||||
showFilterBar
|
showFilterBar
|
||||||
showQueryInput
|
showQueryInput
|
||||||
|
|
|
@ -930,6 +930,38 @@ describe('Lens App', () => {
|
||||||
instance.update();
|
instance.update();
|
||||||
expect(instance.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false);
|
expect(instance.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('enables Save Query UI when user has app-level permissions', async () => {
|
||||||
|
const services = makeDefaultServicesForApp();
|
||||||
|
services.application = {
|
||||||
|
...services.application,
|
||||||
|
capabilities: {
|
||||||
|
...services.application.capabilities,
|
||||||
|
visualize: { saveQuery: true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { instance } = await mountWith({ services });
|
||||||
|
await act(async () => {
|
||||||
|
const topNavMenu = instance.find(services.navigation.ui.AggregateQueryTopNavMenu);
|
||||||
|
expect(topNavMenu.props().saveQueryMenuVisibility).toBe('allowed_by_app_privilege');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checks global save query permission when user does not have app-level permissions', async () => {
|
||||||
|
const services = makeDefaultServicesForApp();
|
||||||
|
services.application = {
|
||||||
|
...services.application,
|
||||||
|
capabilities: {
|
||||||
|
...services.application.capabilities,
|
||||||
|
visualize: { saveQuery: false },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { instance } = await mountWith({ services });
|
||||||
|
await act(async () => {
|
||||||
|
const topNavMenu = instance.find(services.navigation.ui.AggregateQueryTopNavMenu);
|
||||||
|
expect(topNavMenu.props().saveQueryMenuVisibility).toBe('globally_managed');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1187,7 +1219,7 @@ describe('Lens App', () => {
|
||||||
};
|
};
|
||||||
await mountWith({ services });
|
await mountWith({ services });
|
||||||
expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith(
|
expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ showSaveQuery: false }),
|
expect.objectContaining({ saveQueryMenuVisibility: 'globally_managed' }),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1196,7 +1228,7 @@ describe('Lens App', () => {
|
||||||
const { instance, services } = await mountWith({});
|
const { instance, services } = await mountWith({});
|
||||||
expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith(
|
expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
showSaveQuery: true,
|
saveQueryMenuVisibility: 'allowed_by_app_privilege',
|
||||||
savedQuery: undefined,
|
savedQuery: undefined,
|
||||||
onSaved: expect.any(Function),
|
onSaved: expect.any(Function),
|
||||||
onSavedQueryUpdated: expect.any(Function),
|
onSavedQueryUpdated: expect.any(Function),
|
||||||
|
|
|
@ -1054,7 +1054,11 @@ export const LensTopNavMenu = ({
|
||||||
<AggregateQueryTopNavMenu
|
<AggregateQueryTopNavMenu
|
||||||
setMenuMountPoint={setHeaderActionMenu}
|
setMenuMountPoint={setHeaderActionMenu}
|
||||||
config={topNavConfig}
|
config={topNavConfig}
|
||||||
showSaveQuery={Boolean(application.capabilities.visualize.saveQuery)}
|
saveQueryMenuVisibility={
|
||||||
|
application.capabilities.visualize.saveQuery
|
||||||
|
? 'allowed_by_app_privilege'
|
||||||
|
: 'globally_managed'
|
||||||
|
}
|
||||||
savedQuery={savedQuery}
|
savedQuery={savedQuery}
|
||||||
onQuerySubmit={onQuerySubmitWrapped}
|
onQuerySubmit={onQuerySubmitWrapped}
|
||||||
onSaved={onSavedWrapped}
|
onSaved={onSavedWrapped}
|
||||||
|
|
|
@ -522,7 +522,9 @@ export class MapApp extends React.Component<Props, State> {
|
||||||
showSearchBar={true}
|
showSearchBar={true}
|
||||||
showFilterBar={true}
|
showFilterBar={true}
|
||||||
showDatePicker={true}
|
showDatePicker={true}
|
||||||
showSaveQuery={!!getMapsCapabilities().saveQuery}
|
saveQueryMenuVisibility={
|
||||||
|
getMapsCapabilities().saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
|
||||||
|
}
|
||||||
savedQuery={this.state.savedQuery}
|
savedQuery={this.state.savedQuery}
|
||||||
onSaved={this._updateStateFromSavedQuery}
|
onSaved={this._updateStateFromSavedQuery}
|
||||||
onSavedQueryUpdated={this._updateStateFromSavedQuery}
|
onSavedQueryUpdated={this._updateStateFromSavedQuery}
|
||||||
|
|
|
@ -329,7 +329,7 @@ export const SearchBarComponent = memo<SiemSearchBarProps & PropsFromRedux>(
|
||||||
showFilterBar={!hideFilterBar}
|
showFilterBar={!hideFilterBar}
|
||||||
showDatePicker={true}
|
showDatePicker={true}
|
||||||
showQueryInput={!hideQueryInput}
|
showQueryInput={!hideQueryInput}
|
||||||
showSaveQuery={true}
|
saveQueryMenuVisibility="allowed_by_app_privilege"
|
||||||
dataTestSubj={dataTestSubj}
|
dataTestSubj={dataTestSubj}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -328,7 +328,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
|
||||||
onClearSavedQuery={onClearSavedQuery}
|
onClearSavedQuery={onClearSavedQuery}
|
||||||
onSavedQueryUpdated={onSavedQuery}
|
onSavedQueryUpdated={onSavedQuery}
|
||||||
onSaved={onSavedQuery}
|
onSaved={onSavedQuery}
|
||||||
showSaveQuery
|
saveQueryMenuVisibility="allowed_by_app_privilege"
|
||||||
showQueryInput
|
showQueryInput
|
||||||
showFilterBar
|
showFilterBar
|
||||||
showDatePicker={false}
|
showDatePicker={false}
|
||||||
|
|
|
@ -101,7 +101,7 @@ export function AlertsSearchBar({
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
showDatePicker={showDatePicker}
|
showDatePicker={showDatePicker}
|
||||||
showQueryInput={true}
|
showQueryInput={true}
|
||||||
showSaveQuery={true}
|
saveQueryMenuVisibility="allowed_by_app_privilege"
|
||||||
showSubmitButton={showSubmitButton}
|
showSubmitButton={showSubmitButton}
|
||||||
submitOnBlur={submitOnBlur}
|
submitOnBlur={submitOnBlur}
|
||||||
onQueryChange={onSearchQueryChange}
|
onQueryChange={onSearchQueryChange}
|
||||||
|
|
|
@ -88,7 +88,7 @@ export const KqlSearchBar = React.memo<KqlSearchBarProps>(({ onQuerySubmit }) =>
|
||||||
indexPatterns={loading || error ? NO_INDEX_PATTERNS : dataView}
|
indexPatterns={loading || error ? NO_INDEX_PATTERNS : dataView}
|
||||||
showAutoRefreshOnly={false}
|
showAutoRefreshOnly={false}
|
||||||
showDatePicker={false}
|
showDatePicker={false}
|
||||||
showSaveQuery={false}
|
saveQueryMenuVisibility="hidden"
|
||||||
showQueryInput={true}
|
showQueryInput={true}
|
||||||
showQueryMenu={false}
|
showQueryMenu={false}
|
||||||
showFilterBar={true}
|
showFilterBar={true}
|
||||||
|
|
|
@ -110,6 +110,7 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
'observabilityAIAssistant',
|
'observabilityAIAssistant',
|
||||||
'observabilityCases',
|
'observabilityCases',
|
||||||
'savedObjectsManagement',
|
'savedObjectsManagement',
|
||||||
|
'savedQueryManagement',
|
||||||
'savedObjectsTagging',
|
'savedObjectsTagging',
|
||||||
'ml',
|
'ml',
|
||||||
'apm',
|
'apm',
|
||||||
|
|
|
@ -82,6 +82,7 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
advancedSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
advancedSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
indexPatterns: ['all', 'read', 'minimal_all', 'minimal_read'],
|
indexPatterns: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
savedQueryManagement: ['all', 'minimal_all'],
|
||||||
osquery: [
|
osquery: [
|
||||||
'all',
|
'all',
|
||||||
'read',
|
'read',
|
||||||
|
|
|
@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
advancedSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
advancedSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
indexPatterns: ['all', 'read', 'minimal_all', 'minimal_read'],
|
indexPatterns: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
savedQueryManagement: ['all', 'minimal_all'],
|
||||||
savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'],
|
savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
graph: ['all', 'read', 'minimal_all', 'minimal_read'],
|
graph: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
|
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
@ -165,6 +166,7 @@ export default function ({ getService }: FtrProviderContext) {
|
||||||
filesManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
filesManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
filesSharedImage: ['all', 'read', 'minimal_all', 'minimal_read'],
|
filesSharedImage: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||||
|
savedQueryManagement: ['all', 'minimal_all'],
|
||||||
osquery: [
|
osquery: [
|
||||||
'all',
|
'all',
|
||||||
'read',
|
'read',
|
||||||
|
|
|
@ -33,6 +33,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
shouldLoginIfPrompted: false,
|
shouldLoginIfPrompted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// more tests are in x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts
|
||||||
|
|
||||||
describe('dashboard feature controls security', () => {
|
describe('dashboard feature controls security', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
|
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
|
||||||
|
|
|
@ -7,8 +7,11 @@
|
||||||
|
|
||||||
import expect from '@kbn/expect';
|
import expect from '@kbn/expect';
|
||||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||||
|
import { getSavedQuerySecurityUtils } from '../../saved_query_management/utils/saved_query_security';
|
||||||
|
|
||||||
export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
export default function (ctx: FtrProviderContext) {
|
||||||
|
const { getPageObjects, getService } = ctx;
|
||||||
|
const savedQuerySecurityUtils = getSavedQuerySecurityUtils(ctx);
|
||||||
const esArchiver = getService('esArchiver');
|
const esArchiver = getService('esArchiver');
|
||||||
const esSupertest = getService('esSupertest');
|
const esSupertest = getService('esSupertest');
|
||||||
const dataGrid = getService('dataGrid');
|
const dataGrid = getService('dataGrid');
|
||||||
|
@ -31,8 +34,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
]);
|
]);
|
||||||
const testSubjects = getService('testSubjects');
|
const testSubjects = getService('testSubjects');
|
||||||
const appsMenu = getService('appsMenu');
|
const appsMenu = getService('appsMenu');
|
||||||
const queryBar = getService('queryBar');
|
|
||||||
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
|
||||||
const kibanaServer = getService('kibanaServer');
|
const kibanaServer = getService('kibanaServer');
|
||||||
const logstashIndexName = 'logstash-2015.09.22';
|
const logstashIndexName = 'logstash-2015.09.22';
|
||||||
|
|
||||||
|
@ -40,6 +41,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// more tests are in x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts
|
||||||
|
|
||||||
describe('discover feature controls security', () => {
|
describe('discover feature controls security', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await kibanaServer.importExport.load(
|
await kibanaServer.importExport.load(
|
||||||
|
@ -129,53 +132,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
await PageObjects.share.clickShareTopNavButton();
|
await PageObjects.share.clickShareTopNavButton();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows saving via the saved query management component popover with no saved query loaded', async () => {
|
savedQuerySecurityUtils.shouldAllowSavingQueries();
|
||||||
await queryBar.setQuery('response:200');
|
|
||||||
await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false);
|
|
||||||
await savedQueryManagementComponent.savedQueryExistOrFail('foo');
|
|
||||||
await savedQueryManagementComponent.closeSavedQueryManagementComponent();
|
|
||||||
|
|
||||||
await savedQueryManagementComponent.deleteSavedQuery('foo');
|
|
||||||
await savedQueryManagementComponent.savedQueryMissingOrFail('foo');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allow saving changes to a currently loaded query via the saved query management component', async () => {
|
|
||||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
|
||||||
await queryBar.setQuery('response:404');
|
|
||||||
await savedQueryManagementComponent.updateCurrentlyLoadedQuery(
|
|
||||||
'new description',
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
|
||||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
|
||||||
const queryString = await queryBar.getQueryString();
|
|
||||||
expect(queryString).to.eql('response:404');
|
|
||||||
|
|
||||||
// Reset after changing
|
|
||||||
await queryBar.setQuery('response:200');
|
|
||||||
await savedQueryManagementComponent.updateCurrentlyLoadedQuery(
|
|
||||||
'Ok responses for jpg files',
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allow saving currently loaded query as a copy', async () => {
|
|
||||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
|
||||||
await queryBar.setQuery('response:404');
|
|
||||||
await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery(
|
|
||||||
'ok2',
|
|
||||||
'description',
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
|
||||||
await savedQueryManagementComponent.savedQueryExistOrFail('ok2');
|
|
||||||
await savedQueryManagementComponent.closeSavedQueryManagementComponent();
|
|
||||||
await testSubjects.click('showQueryBarMenu');
|
|
||||||
await savedQueryManagementComponent.deleteSavedQuery('ok2');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('global discover read-only privileges', () => {
|
describe('global discover read-only privileges', () => {
|
||||||
|
@ -245,33 +202,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
await PageObjects.share.clickShareTopNavButton();
|
await PageObjects.share.clickShareTopNavButton();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows loading a saved query via the saved query management component', async () => {
|
savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries();
|
||||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
|
||||||
const queryString = await queryBar.getQueryString();
|
|
||||||
expect(queryString).to.eql('response:200');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not allow saving via the saved query management component popover with no query loaded', async () => {
|
|
||||||
await savedQueryManagementComponent.saveNewQueryMissingOrFail();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not allow saving changes to saved query from the saved query management component', async () => {
|
|
||||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
|
||||||
await queryBar.setQuery('response:404');
|
|
||||||
await savedQueryManagementComponent.updateCurrentlyLoadedQueryMissingOrFail();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not allow deleting a saved query from the saved query management component', async () => {
|
|
||||||
await savedQueryManagementComponent.deleteSavedQueryMissingOrFail('OKJpgs');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows clearing the currently loaded saved query', async () => {
|
|
||||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
|
||||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('global discover read-only privileges with url_create', () => {
|
describe('discover read-only privileges with url_create', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await security.role.create('global_discover_read_url_create_role', {
|
await security.role.create('global_discover_read_url_create_role', {
|
||||||
elasticsearch: {
|
elasticsearch: {
|
||||||
|
@ -338,30 +272,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
await PageObjects.share.clickShareTopNavButton();
|
await PageObjects.share.clickShareTopNavButton();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows loading a saved query via the saved query management component', async () => {
|
savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries();
|
||||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
|
||||||
const queryString = await queryBar.getQueryString();
|
|
||||||
expect(queryString).to.eql('response:200');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not allow saving via the saved query management component popover with no query loaded', async () => {
|
|
||||||
await savedQueryManagementComponent.saveNewQueryMissingOrFail();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not allow saving changes to saved query from the saved query management component', async () => {
|
|
||||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
|
||||||
await queryBar.setQuery('response:404');
|
|
||||||
await savedQueryManagementComponent.updateCurrentlyLoadedQueryMissingOrFail();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not allow deleting a saved query from the saved query management component', async () => {
|
|
||||||
await savedQueryManagementComponent.deleteSavedQueryMissingOrFail('OKJpgs');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows clearing the currently loaded saved query', async () => {
|
|
||||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
|
||||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('discover and visualize privileges', () => {
|
describe('discover and visualize privileges', () => {
|
||||||
|
|
|
@ -17,6 +17,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
const queryBar = getService('queryBar');
|
const queryBar = getService('queryBar');
|
||||||
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
||||||
|
|
||||||
|
// more tests are in x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts
|
||||||
|
|
||||||
describe('maps security feature controls', () => {
|
describe('maps security feature controls', () => {
|
||||||
after(async () => {
|
after(async () => {
|
||||||
// logout, so the other tests don't accidentally run as the custom users we're testing below
|
// logout, so the other tests don't accidentally run as the custom users we're testing below
|
||||||
|
|
17
x-pack/test/functional/apps/saved_query_management/config.ts
Normal file
17
x-pack/test/functional/apps/saved_query_management/config.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* 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 { FtrConfigProviderContext } from '@kbn/test';
|
||||||
|
|
||||||
|
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||||
|
const functionalConfig = await readConfigFile(require.resolve('../../config.base.js'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...functionalConfig.getAll(),
|
||||||
|
testFiles: [require.resolve('.')],
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
|
||||||
|
|
||||||
|
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||||
|
describe('Feature controls', function () {
|
||||||
|
this.tags('skipFirefox');
|
||||||
|
loadTestFile(require.resolve('./security'));
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
|
||||||
|
import { getSavedQuerySecurityUtils } from '../utils/saved_query_security';
|
||||||
|
|
||||||
|
type AppName = 'discover' | 'dashboard' | 'maps' | 'visualize';
|
||||||
|
|
||||||
|
const apps: AppName[] = ['discover', 'dashboard', 'maps', 'visualize'];
|
||||||
|
|
||||||
|
export default function (ctx: FtrProviderContext) {
|
||||||
|
const { getPageObjects, getService } = ctx;
|
||||||
|
const savedQuerySecurityUtils = getSavedQuerySecurityUtils(ctx);
|
||||||
|
const esArchiver = getService('esArchiver');
|
||||||
|
const security = getService('security');
|
||||||
|
const globalNav = getService('globalNav');
|
||||||
|
const PageObjects = getPageObjects([
|
||||||
|
'common',
|
||||||
|
'discover',
|
||||||
|
'security',
|
||||||
|
'dashboard',
|
||||||
|
'maps',
|
||||||
|
'visualize',
|
||||||
|
]);
|
||||||
|
const kibanaServer = getService('kibanaServer');
|
||||||
|
|
||||||
|
async function login(
|
||||||
|
appName: AppName,
|
||||||
|
appPrivilege: 'read' | 'all',
|
||||||
|
globalPrivilege: 'none' | 'all'
|
||||||
|
) {
|
||||||
|
const name = `global_saved_query_${appName}`;
|
||||||
|
const password = `password_${name}_${appPrivilege}_${globalPrivilege}`;
|
||||||
|
|
||||||
|
await security.role.create(name, {
|
||||||
|
elasticsearch: {
|
||||||
|
indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }],
|
||||||
|
},
|
||||||
|
kibana: [
|
||||||
|
{
|
||||||
|
feature: {
|
||||||
|
[appName]: [appPrivilege],
|
||||||
|
savedQueryManagement: [globalPrivilege],
|
||||||
|
},
|
||||||
|
spaces: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await security.user.create(`${name}-user`, {
|
||||||
|
password,
|
||||||
|
roles: [name],
|
||||||
|
full_name: 'test user',
|
||||||
|
});
|
||||||
|
|
||||||
|
await PageObjects.security.login(`${name}-user`, password, {
|
||||||
|
expectSpaceSelector: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout(appName: AppName) {
|
||||||
|
const name = `global_saved_query_${appName}`;
|
||||||
|
await PageObjects.security.forceLogout();
|
||||||
|
await security.role.delete(name);
|
||||||
|
await security.user.delete(`${name}-user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigateToApp(appName: AppName) {
|
||||||
|
switch (appName) {
|
||||||
|
case 'discover':
|
||||||
|
await PageObjects.common.navigateToApp('discover');
|
||||||
|
await PageObjects.discover.selectIndexPattern('logstash-*');
|
||||||
|
break;
|
||||||
|
case 'dashboard':
|
||||||
|
await PageObjects.dashboard.navigateToApp();
|
||||||
|
await PageObjects.dashboard.gotoDashboardEditMode('A Dashboard');
|
||||||
|
break;
|
||||||
|
case 'maps':
|
||||||
|
await PageObjects.maps.openNewMap();
|
||||||
|
break;
|
||||||
|
case 'visualize':
|
||||||
|
await PageObjects.visualize.navigateToNewVisualization();
|
||||||
|
await PageObjects.visualize.clickVisType('lens');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Security: App vs Global privilege', () => {
|
||||||
|
apps.forEach((appName) => {
|
||||||
|
before(async () => {
|
||||||
|
await kibanaServer.savedObjects.cleanStandardList();
|
||||||
|
|
||||||
|
await kibanaServer.importExport.load(
|
||||||
|
'x-pack/test/functional/fixtures/kbn_archiver/dashboard/feature_controls/security/security.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
|
||||||
|
|
||||||
|
// ensure we're logged out, so we can log in as the appropriate users
|
||||||
|
await PageObjects.security.forceLogout();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
// logout, so the other tests don't accidentally run as the custom users we're testing below
|
||||||
|
// NOTE: Logout needs to happen before anything else to avoid flaky behavior
|
||||||
|
await PageObjects.security.forceLogout();
|
||||||
|
|
||||||
|
await kibanaServer.importExport.unload(
|
||||||
|
'x-pack/test/functional/fixtures/kbn_archiver/dashboard/feature_controls/security/security.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
await kibanaServer.savedObjects.cleanStandardList();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`${appName} read-only privileges with enabled savedQueryManagement.saveQuery privilege`, () => {
|
||||||
|
before(async () => {
|
||||||
|
await login(appName, 'read', 'all');
|
||||||
|
await navigateToApp(appName);
|
||||||
|
await PageObjects.common.waitForTopNavToBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await logout(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows read-only badge', async () => {
|
||||||
|
await globalNav.badgeExistsOrFail('Read only');
|
||||||
|
});
|
||||||
|
|
||||||
|
savedQuerySecurityUtils.shouldAllowSavingQueries();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`${appName} read-only privileges with disabled savedQueryManagement.saveQuery privilege`, () => {
|
||||||
|
before(async () => {
|
||||||
|
await login(appName, 'read', 'none');
|
||||||
|
await navigateToApp(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await logout(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows read-only badge', async () => {
|
||||||
|
await globalNav.badgeExistsOrFail('Read only');
|
||||||
|
});
|
||||||
|
|
||||||
|
savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`${appName} all privileges with enabled savedQueryManagement.saveQuery privilege`, () => {
|
||||||
|
before(async () => {
|
||||||
|
await login(appName, 'all', 'all');
|
||||||
|
await navigateToApp(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await logout(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't show read-only badge", async () => {
|
||||||
|
await globalNav.badgeMissingOrFail();
|
||||||
|
});
|
||||||
|
|
||||||
|
savedQuerySecurityUtils.shouldAllowSavingQueries();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`${appName} all privileges with disabled savedQueryManagement.saveQuery privilege`, () => {
|
||||||
|
before(async () => {
|
||||||
|
await login(appName, 'all', 'none');
|
||||||
|
await navigateToApp(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await logout(appName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't show read-only badge", async () => {
|
||||||
|
await globalNav.badgeMissingOrFail();
|
||||||
|
});
|
||||||
|
|
||||||
|
savedQuerySecurityUtils.shouldAllowSavingQueries();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
14
x-pack/test/functional/apps/saved_query_management/index.ts
Normal file
14
x-pack/test/functional/apps/saved_query_management/index.ts
Normal 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; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
|
|
||||||
|
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||||
|
describe('Saved query management', function () {
|
||||||
|
loadTestFile(require.resolve('./feature_controls'));
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* 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 expect from '@kbn/expect';
|
||||||
|
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||||
|
|
||||||
|
export function getSavedQuerySecurityUtils({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
|
const PageObjects = getPageObjects(['header']);
|
||||||
|
const testSubjects = getService('testSubjects');
|
||||||
|
const queryBar = getService('queryBar');
|
||||||
|
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldAllowSavingQueries: () => {
|
||||||
|
{
|
||||||
|
it('allows saving via the saved query management component popover with no saved query loaded', async () => {
|
||||||
|
await queryBar.setQuery('response:200');
|
||||||
|
await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false);
|
||||||
|
await savedQueryManagementComponent.savedQueryExistOrFail('foo');
|
||||||
|
await savedQueryManagementComponent.closeSavedQueryManagementComponent();
|
||||||
|
|
||||||
|
await savedQueryManagementComponent.deleteSavedQuery('foo');
|
||||||
|
await savedQueryManagementComponent.savedQueryMissingOrFail('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow saving changes to a currently loaded query via the saved query management component', async () => {
|
||||||
|
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||||
|
await queryBar.setQuery('response:404');
|
||||||
|
await savedQueryManagementComponent.updateCurrentlyLoadedQuery(
|
||||||
|
'new description',
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||||
|
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||||
|
const queryString = await queryBar.getQueryString();
|
||||||
|
expect(queryString).to.eql('response:404');
|
||||||
|
|
||||||
|
// Reset after changing
|
||||||
|
await queryBar.setQuery('response:200');
|
||||||
|
await savedQueryManagementComponent.updateCurrentlyLoadedQuery(
|
||||||
|
'Ok responses for jpg files',
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow saving currently loaded query as a copy', async () => {
|
||||||
|
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||||
|
await queryBar.setQuery('response:404');
|
||||||
|
await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery(
|
||||||
|
'ok2',
|
||||||
|
'description',
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||||
|
await savedQueryManagementComponent.savedQueryExistOrFail('ok2');
|
||||||
|
await savedQueryManagementComponent.closeSavedQueryManagementComponent();
|
||||||
|
await testSubjects.click('showQueryBarMenu');
|
||||||
|
await savedQueryManagementComponent.deleteSavedQuery('ok2');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shouldDisallowSavingButAllowLoadingSavedQueries: () => {
|
||||||
|
it('allows loading a saved query via the saved query management component', async () => {
|
||||||
|
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||||
|
const queryString = await queryBar.getQueryString();
|
||||||
|
expect(queryString).to.eql('response:200');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not allow saving via the saved query management component popover with no query loaded', async () => {
|
||||||
|
await savedQueryManagementComponent.saveNewQueryMissingOrFail();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not allow saving changes to saved query from the saved query management component', async () => {
|
||||||
|
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||||
|
await queryBar.setQuery('response:404');
|
||||||
|
await savedQueryManagementComponent.updateCurrentlyLoadedQueryMissingOrFail();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not allow deleting a saved query from the saved query management component', async () => {
|
||||||
|
await savedQueryManagementComponent.deleteSavedQueryMissingOrFail('OKJpgs');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows clearing the currently loaded saved query', async () => {
|
||||||
|
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||||
|
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -29,6 +29,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
const queryBar = getService('queryBar');
|
const queryBar = getService('queryBar');
|
||||||
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
||||||
|
|
||||||
|
// more tests are in x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts
|
||||||
|
|
||||||
describe('visualize feature controls security', () => {
|
describe('visualize feature controls security', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await kibanaServer.savedObjects.cleanStandardList();
|
await kibanaServer.savedObjects.cleanStandardList();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue