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/rollup_job/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/snapshot_restore/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/functional/apps/data_views @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/search_sessions_integration @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}
|
||||
indexPatterns={[dataView]}
|
||||
useDefaultBehaviors={true}
|
||||
showSaveQuery={true}
|
||||
saveQueryMenuVisibility="allowed_by_app_privilege" // allowed only for this example app, use `globally_managed` by default
|
||||
/>
|
||||
<EuiPageTemplate.Section>
|
||||
<EuiText>
|
||||
|
|
|
@ -68,7 +68,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
navigation: { TopNavMenu },
|
||||
embeddable: { getStateTransfer },
|
||||
initializerContext: { allowByValueEmbeddables },
|
||||
dashboardCapabilities: { saveQuery: showSaveQuery, showWriteControls },
|
||||
dashboardCapabilities: { saveQuery: allowSaveQuery, showWriteControls },
|
||||
} = pluginServices.getServices();
|
||||
const isLabsEnabled = uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI);
|
||||
const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext();
|
||||
|
@ -298,7 +298,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
|
|||
useDefaultBehaviors={true}
|
||||
savedQueryId={savedQueryId}
|
||||
indexPatterns={allDataViews}
|
||||
showSaveQuery={showSaveQuery}
|
||||
saveQueryMenuVisibility={allowSaveQuery ? 'allowed_by_app_privilege' : 'globally_managed'}
|
||||
appName={LEGACY_DASHBOARD_APP_ID}
|
||||
visible={viewMode !== ViewMode.PRINT}
|
||||
setMenuMountPoint={embedSettings || fullScreenMode ? undefined : setHeaderActionMenu}
|
||||
|
|
|
@ -88,7 +88,7 @@ describe('ContextApp test', () => {
|
|||
showSearchBar: true,
|
||||
showQueryInput: false,
|
||||
showFilterBar: true,
|
||||
showSaveQuery: false,
|
||||
saveQueryMenuVisibility: 'hidden' as const,
|
||||
showDatePicker: false,
|
||||
indexPatterns: [dataViewMock],
|
||||
useDefaultBehaviors: true,
|
||||
|
|
|
@ -207,7 +207,7 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
|
|||
showSearchBar: true,
|
||||
showQueryInput: false,
|
||||
showFilterBar: true,
|
||||
showSaveQuery: false,
|
||||
saveQueryMenuVisibility: 'hidden' as const,
|
||||
showDatePicker: false,
|
||||
indexPatterns: [dataView],
|
||||
useDefaultBehaviors: true,
|
||||
|
|
|
@ -53,8 +53,20 @@ jest.mock('../../../../customizations', () => ({
|
|||
useDiscoverCustomization: jest.fn(),
|
||||
}));
|
||||
|
||||
function getProps(savePermissions = true): DiscoverTopNavProps {
|
||||
mockDiscoverService.capabilities.discover!.save = savePermissions;
|
||||
const mockDefaultCapabilities = {
|
||||
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 });
|
||||
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', () => {
|
||||
const props = getProps(true);
|
||||
const props = getProps({ capabilities: { discover: { save: true } } });
|
||||
const component = mountWithIntl(
|
||||
<DiscoverMainProvider value={props.stateContainer}>
|
||||
<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', () => {
|
||||
const props = getProps(false);
|
||||
const props = getProps({ capabilities: { discover: { save: false } } });
|
||||
const component = mountWithIntl(
|
||||
<DiscoverMainProvider value={props.stateContainer}>
|
||||
<DiscoverTopNav {...props} />
|
||||
|
@ -116,6 +128,32 @@ describe('Discover topnav component', () => {
|
|||
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', () => {
|
||||
it('should call getMenuItems', () => {
|
||||
mockUseCustomizations = true;
|
||||
|
|
|
@ -220,7 +220,9 @@ export const DiscoverTopNav = ({
|
|||
savedQueryId={savedQuery}
|
||||
screenTitle={savedSearch.title}
|
||||
showDatePicker={showDatePicker}
|
||||
showSaveQuery={!isPlainRecord && Boolean(services.capabilities.discover.saveQuery)}
|
||||
saveQueryMenuVisibility={
|
||||
services.capabilities.discover.saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
|
||||
}
|
||||
showSearchBar={true}
|
||||
useDefaultBehaviors={true}
|
||||
dataViewPickerOverride={
|
||||
|
|
|
@ -20,7 +20,7 @@ import classNames from 'classnames';
|
|||
import { MountPoint } from '@kbn/core/public';
|
||||
import { MountPointPortal } from '@kbn/kibana-react-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 { TopNavMenuData } from './top_nav_menu_data';
|
||||
import { TopNavMenuItem } from './top_nav_menu_item';
|
||||
|
@ -30,9 +30,10 @@ type Badge = EuiBadgeProps & {
|
|||
toolTipProps?: Partial<EuiToolTipProps>;
|
||||
};
|
||||
|
||||
export type TopNavMenuProps<QT extends Query | AggregateQuery = Query> =
|
||||
StatefulSearchBarProps<QT> &
|
||||
Omit<SearchBarProps<QT>, 'kibana' | 'intl' | 'timeHistory'> & {
|
||||
export type TopNavMenuProps<QT extends Query | AggregateQuery = Query> = Omit<
|
||||
StatefulSearchBarProps<QT>,
|
||||
'kibana' | 'intl' | 'timeHistory'
|
||||
> & {
|
||||
config?: TopNavMenuData[];
|
||||
badges?: Badge[];
|
||||
showSearchBar?: boolean;
|
||||
|
@ -61,7 +62,7 @@ export type TopNavMenuProps<QT extends Query | AggregateQuery = Query> =
|
|||
* ```
|
||||
*/
|
||||
setMenuMountPoint?: (menuMount: MountPoint | undefined) => void;
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Top Nav Menu is a convenience wrapper component for:
|
||||
|
|
|
@ -174,11 +174,20 @@ const services = {
|
|||
},
|
||||
};
|
||||
|
||||
const defaultCapabilities = {
|
||||
savedObjectsManagement: {
|
||||
edit: true,
|
||||
},
|
||||
};
|
||||
|
||||
setIndexPatterns({
|
||||
get: () => Promise.resolve(mockIndexPatterns[0]),
|
||||
} as unknown as DataViewsContract);
|
||||
|
||||
function wrapSearchBarInContext(testProps: SearchBarProps<Query>) {
|
||||
function wrapSearchBarInContext(
|
||||
testProps: Partial<SearchBarProps<Query>>,
|
||||
capabilities: typeof defaultCapabilities = defaultCapabilities
|
||||
) {
|
||||
const defaultOptions = {
|
||||
appName: 'test',
|
||||
timeHistory: mockTimeHistory,
|
||||
|
@ -197,9 +206,16 @@ function wrapSearchBarInContext(testProps: SearchBarProps<Query>) {
|
|||
onFiltersUpdated: action('onFiltersUpdated'),
|
||||
} as unknown as SearchBarProps<Query>;
|
||||
|
||||
const kbnServices = {
|
||||
...services,
|
||||
application: {
|
||||
capabilities,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={services}>
|
||||
<KibanaContextProvider services={kbnServices}>
|
||||
<SearchBar<Query> {...defaultOptions} {...testProps} />
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
|
@ -219,7 +235,7 @@ storiesOf('SearchBar', module)
|
|||
},
|
||||
onChangeDataView: action('onChangeDataView'),
|
||||
},
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('with dataviewPicker enhanced', () =>
|
||||
wrapSearchBarInContext({
|
||||
|
@ -234,41 +250,56 @@ storiesOf('SearchBar', module)
|
|||
onAddField: action('onAddField'),
|
||||
onDataViewCreated: action('onDataViewCreated'),
|
||||
},
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('with filterBar off', () =>
|
||||
wrapSearchBarInContext({
|
||||
showFilterBar: false,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('with query input off', () =>
|
||||
wrapSearchBarInContext({
|
||||
showQueryInput: false,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('with date picker off', () =>
|
||||
wrapSearchBarInContext({
|
||||
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', () =>
|
||||
wrapSearchBarInContext({
|
||||
showDatePicker: true,
|
||||
onRefreshChange: action('onRefreshChange'),
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('with the default date picker auto refresh interval off', () =>
|
||||
wrapSearchBarInContext({
|
||||
showDatePicker: true,
|
||||
isAutoRefreshDisabled: true,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('with only the date picker on', () =>
|
||||
wrapSearchBarInContext({
|
||||
showDatePicker: true,
|
||||
showFilterBar: false,
|
||||
showQueryInput: false,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('with additional filters used for suggestions', () =>
|
||||
wrapSearchBarInContext({
|
||||
|
@ -470,12 +501,12 @@ storiesOf('SearchBar', module)
|
|||
/>
|
||||
),
|
||||
showQueryInput: true,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('without switch query language', () =>
|
||||
wrapSearchBarInContext({
|
||||
disableQueryLanguageSwitcher: true,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('show only query bar without submit', () =>
|
||||
wrapSearchBarInContext({
|
||||
|
@ -484,7 +515,7 @@ storiesOf('SearchBar', module)
|
|||
showAutoRefreshOnly: false,
|
||||
showQueryInput: true,
|
||||
showSubmitButton: false,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('show only datepicker without submit', () =>
|
||||
wrapSearchBarInContext({
|
||||
|
@ -493,7 +524,7 @@ storiesOf('SearchBar', module)
|
|||
showAutoRefreshOnly: false,
|
||||
showQueryInput: false,
|
||||
showSubmitButton: false,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('show only query bar and timepicker without submit', () =>
|
||||
wrapSearchBarInContext({
|
||||
|
@ -502,7 +533,7 @@ storiesOf('SearchBar', module)
|
|||
showAutoRefreshOnly: false,
|
||||
showQueryInput: true,
|
||||
showSubmitButton: false,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('with filter bar on but pinning option is hidden from menus', () =>
|
||||
wrapSearchBarInContext({
|
||||
|
@ -621,7 +652,7 @@ storiesOf('SearchBar', module)
|
|||
onChangeDataView: action('onChangeDataView'),
|
||||
},
|
||||
isDisabled: true,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('no submit button', () =>
|
||||
wrapSearchBarInContext({
|
||||
|
@ -635,7 +666,7 @@ storiesOf('SearchBar', module)
|
|||
onChangeDataView: action('onChangeDataView'),
|
||||
},
|
||||
showSubmitButton: false,
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('submit button always as icon', () =>
|
||||
wrapSearchBarInContext({
|
||||
|
@ -649,7 +680,7 @@ storiesOf('SearchBar', module)
|
|||
onChangeDataView: action('onChangeDataView'),
|
||||
},
|
||||
submitButtonStyle: 'iconOnly',
|
||||
} as SearchBarProps)
|
||||
})
|
||||
)
|
||||
.add('submit button always as a full button', () =>
|
||||
wrapSearchBarInContext({
|
||||
|
@ -663,5 +694,5 @@ storiesOf('SearchBar', module)
|
|||
onChangeDataView: action('onChangeDataView'),
|
||||
},
|
||||
submitButtonStyle: 'full',
|
||||
} as SearchBarProps)
|
||||
})
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ import { useFilterManager } from './lib/use_filter_manager';
|
|||
import { useTimefilter } from './lib/use_timefilter';
|
||||
import { useSavedQuery } from './lib/use_saved_query';
|
||||
import { useQueryStringManager } from './lib/use_query_string_manager';
|
||||
import { type SavedQueryMenuVisibility, canShowSavedQuery } from './lib/can_show_saved_query';
|
||||
import type { UnifiedSearchPublicPluginStart } from '../types';
|
||||
|
||||
export interface StatefulSearchBarDeps {
|
||||
|
@ -32,14 +33,17 @@ export interface StatefulSearchBarDeps {
|
|||
unifiedSearch: Omit<UnifiedSearchPublicPluginStart, 'ui'>;
|
||||
}
|
||||
|
||||
export type StatefulSearchBarProps<QT extends Query | AggregateQuery = Query> =
|
||||
SearchBarOwnProps<QT> & {
|
||||
export type StatefulSearchBarProps<QT extends Query | AggregateQuery = Query> = Omit<
|
||||
SearchBarOwnProps<QT>,
|
||||
'showSaveQuery'
|
||||
> & {
|
||||
appName: string;
|
||||
useDefaultBehaviors?: boolean;
|
||||
savedQueryId?: string;
|
||||
saveQueryMenuVisibility?: SavedQueryMenuVisibility;
|
||||
onSavedQueryIdChange?: (savedQueryId?: string) => void;
|
||||
onFiltersUpdated?: (filters: Filter[]) => void;
|
||||
};
|
||||
};
|
||||
|
||||
// Respond to user changing the filters
|
||||
const defaultFiltersUpdated = (
|
||||
|
@ -194,6 +198,12 @@ export function createSearchBar({
|
|||
);
|
||||
}, [query, timeRange, useDefaultBehaviors]);
|
||||
|
||||
const showSaveQuery = canShowSavedQuery({
|
||||
saveQueryMenuVisibility: props.saveQueryMenuVisibility,
|
||||
query,
|
||||
core,
|
||||
});
|
||||
|
||||
return (
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
|
@ -212,7 +222,7 @@ export function createSearchBar({
|
|||
showFilterBar={props.showFilterBar}
|
||||
showQueryMenu={props.showQueryMenu}
|
||||
showQueryInput={props.showQueryInput}
|
||||
showSaveQuery={props.showSaveQuery}
|
||||
showSaveQuery={showSaveQuery}
|
||||
showSubmitButton={props.showSubmitButton}
|
||||
submitButtonStyle={props.submitButtonStyle}
|
||||
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;
|
||||
// Query bar - should be in SearchBarInjectedDeps
|
||||
query?: QT | Query;
|
||||
// Show when user has privileges to save
|
||||
// Show when user has privileges to save. See `canShowSavedQuery(...)` lib.
|
||||
showSaveQuery?: boolean;
|
||||
savedQuery?: SavedQuery;
|
||||
onQueryChange?: (payload: { dateRange: TimeRange; query?: QT | Query }) => void;
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
"@kbn/text-based-languages",
|
||||
"@kbn/text-based-editor",
|
||||
"@kbn/core-doc-links-browser",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -307,7 +307,9 @@ const TopNav = ({
|
|||
showDatePicker={showDatePicker()}
|
||||
showFilterBar={showFilterBar}
|
||||
showQueryInput={showQueryInput}
|
||||
showSaveQuery={Boolean(services.visualizeCapabilities.saveQuery)}
|
||||
saveQueryMenuVisibility={
|
||||
services.visualizeCapabilities.saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
|
||||
}
|
||||
dataViewPickerComponentProps={
|
||||
shouldShowDataViewPicker && vis.data.indexPattern
|
||||
? {
|
||||
|
@ -346,7 +348,7 @@ const TopNav = ({
|
|||
setMenuMountPoint={setHeaderActionMenu}
|
||||
indexPatterns={indexPatterns}
|
||||
showSearchBar
|
||||
showSaveQuery={false}
|
||||
saveQueryMenuVisibility="hidden"
|
||||
showDatePicker={false}
|
||||
showQueryInput={false}
|
||||
/>
|
||||
|
|
|
@ -48,7 +48,7 @@ export const FindingsSearchBar = ({
|
|||
showFilterBar={true}
|
||||
showQueryInput={true}
|
||||
showDatePicker={false}
|
||||
showSaveQuery={false}
|
||||
saveQueryMenuVisibility="hidden"
|
||||
isLoading={loading}
|
||||
indexPatterns={[dataView]}
|
||||
onQuerySubmit={setQuery}
|
||||
|
|
|
@ -177,6 +177,10 @@ Array [
|
|||
"id": "savedObjectsManagement",
|
||||
"subFeatures": undefined,
|
||||
},
|
||||
Object {
|
||||
"id": "savedQueryManagement",
|
||||
"subFeatures": undefined,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
|
@ -469,6 +473,10 @@ Array [
|
|||
"id": "savedObjectsManagement",
|
||||
"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`] = `
|
||||
Array [
|
||||
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`] = `
|
||||
Array [
|
||||
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[];
|
||||
};
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ describe('Features Plugin', () => {
|
|||
"filesManagement",
|
||||
"filesSharedImage",
|
||||
"savedObjectsManagement",
|
||||
"savedQueryManagement",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -57,7 +57,11 @@ export const UnifiedSearchBar = () => {
|
|||
defaultMessage: 'Search hosts (E.g. cloud.provider:gcp AND system.load.1 > 0.5)',
|
||||
})}
|
||||
onQuerySubmit={handleRefresh}
|
||||
showSaveQuery={Boolean(application?.capabilities?.visualize?.saveQuery)}
|
||||
saveQueryMenuVisibility={
|
||||
application?.capabilities?.visualize?.saveQuery
|
||||
? 'allowed_by_app_privilege'
|
||||
: 'globally_managed'
|
||||
}
|
||||
showDatePicker
|
||||
showFilterBar
|
||||
showQueryInput
|
||||
|
|
|
@ -930,6 +930,38 @@ describe('Lens App', () => {
|
|||
instance.update();
|
||||
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 });
|
||||
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({});
|
||||
expect(services.navigation.ui.AggregateQueryTopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
showSaveQuery: true,
|
||||
saveQueryMenuVisibility: 'allowed_by_app_privilege',
|
||||
savedQuery: undefined,
|
||||
onSaved: expect.any(Function),
|
||||
onSavedQueryUpdated: expect.any(Function),
|
||||
|
|
|
@ -1054,7 +1054,11 @@ export const LensTopNavMenu = ({
|
|||
<AggregateQueryTopNavMenu
|
||||
setMenuMountPoint={setHeaderActionMenu}
|
||||
config={topNavConfig}
|
||||
showSaveQuery={Boolean(application.capabilities.visualize.saveQuery)}
|
||||
saveQueryMenuVisibility={
|
||||
application.capabilities.visualize.saveQuery
|
||||
? 'allowed_by_app_privilege'
|
||||
: 'globally_managed'
|
||||
}
|
||||
savedQuery={savedQuery}
|
||||
onQuerySubmit={onQuerySubmitWrapped}
|
||||
onSaved={onSavedWrapped}
|
||||
|
|
|
@ -522,7 +522,9 @@ export class MapApp extends React.Component<Props, State> {
|
|||
showSearchBar={true}
|
||||
showFilterBar={true}
|
||||
showDatePicker={true}
|
||||
showSaveQuery={!!getMapsCapabilities().saveQuery}
|
||||
saveQueryMenuVisibility={
|
||||
getMapsCapabilities().saveQuery ? 'allowed_by_app_privilege' : 'globally_managed'
|
||||
}
|
||||
savedQuery={this.state.savedQuery}
|
||||
onSaved={this._updateStateFromSavedQuery}
|
||||
onSavedQueryUpdated={this._updateStateFromSavedQuery}
|
||||
|
|
|
@ -329,7 +329,7 @@ export const SearchBarComponent = memo<SiemSearchBarProps & PropsFromRedux>(
|
|||
showFilterBar={!hideFilterBar}
|
||||
showDatePicker={true}
|
||||
showQueryInput={!hideQueryInput}
|
||||
showSaveQuery={true}
|
||||
saveQueryMenuVisibility="allowed_by_app_privilege"
|
||||
dataTestSubj={dataTestSubj}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -328,7 +328,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
|
|||
onClearSavedQuery={onClearSavedQuery}
|
||||
onSavedQueryUpdated={onSavedQuery}
|
||||
onSaved={onSavedQuery}
|
||||
showSaveQuery
|
||||
saveQueryMenuVisibility="allowed_by_app_privilege"
|
||||
showQueryInput
|
||||
showFilterBar
|
||||
showDatePicker={false}
|
||||
|
|
|
@ -101,7 +101,7 @@ export function AlertsSearchBar({
|
|||
onRefresh={onRefresh}
|
||||
showDatePicker={showDatePicker}
|
||||
showQueryInput={true}
|
||||
showSaveQuery={true}
|
||||
saveQueryMenuVisibility="allowed_by_app_privilege"
|
||||
showSubmitButton={showSubmitButton}
|
||||
submitOnBlur={submitOnBlur}
|
||||
onQueryChange={onSearchQueryChange}
|
||||
|
|
|
@ -88,7 +88,7 @@ export const KqlSearchBar = React.memo<KqlSearchBarProps>(({ onQuerySubmit }) =>
|
|||
indexPatterns={loading || error ? NO_INDEX_PATTERNS : dataView}
|
||||
showAutoRefreshOnly={false}
|
||||
showDatePicker={false}
|
||||
showSaveQuery={false}
|
||||
saveQueryMenuVisibility="hidden"
|
||||
showQueryInput={true}
|
||||
showQueryMenu={false}
|
||||
showFilterBar={true}
|
||||
|
|
|
@ -110,6 +110,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'observabilityAIAssistant',
|
||||
'observabilityCases',
|
||||
'savedObjectsManagement',
|
||||
'savedQueryManagement',
|
||||
'savedObjectsTagging',
|
||||
'ml',
|
||||
'apm',
|
||||
|
|
|
@ -82,6 +82,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
advancedSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
indexPatterns: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
savedQueryManagement: ['all', 'minimal_all'],
|
||||
osquery: [
|
||||
'all',
|
||||
'read',
|
||||
|
|
|
@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
advancedSettings: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
indexPatterns: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
savedQueryManagement: ['all', 'minimal_all'],
|
||||
savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
graph: ['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'],
|
||||
filesSharedImage: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
savedObjectsManagement: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
savedQueryManagement: ['all', 'minimal_all'],
|
||||
osquery: [
|
||||
'all',
|
||||
'read',
|
||||
|
|
|
@ -33,6 +33,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
shouldLoginIfPrompted: false,
|
||||
};
|
||||
|
||||
// more tests are in x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts
|
||||
|
||||
describe('dashboard feature controls security', () => {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
|
||||
|
|
|
@ -7,8 +7,11 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
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 esSupertest = getService('esSupertest');
|
||||
const dataGrid = getService('dataGrid');
|
||||
|
@ -31,8 +34,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
]);
|
||||
const testSubjects = getService('testSubjects');
|
||||
const appsMenu = getService('appsMenu');
|
||||
const queryBar = getService('queryBar');
|
||||
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const logstashIndexName = 'logstash-2015.09.22';
|
||||
|
||||
|
@ -40,6 +41,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
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', () => {
|
||||
before(async () => {
|
||||
await kibanaServer.importExport.load(
|
||||
|
@ -129,53 +132,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await PageObjects.share.clickShareTopNavButton();
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
savedQuerySecurityUtils.shouldAllowSavingQueries();
|
||||
});
|
||||
|
||||
describe('global discover read-only privileges', () => {
|
||||
|
@ -245,33 +202,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await PageObjects.share.clickShareTopNavButton();
|
||||
});
|
||||
|
||||
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');
|
||||
savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
await security.role.create('global_discover_read_url_create_role', {
|
||||
elasticsearch: {
|
||||
|
@ -338,30 +272,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await PageObjects.share.clickShareTopNavButton();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
savedQuerySecurityUtils.shouldDisallowSavingButAllowLoadingSavedQueries();
|
||||
});
|
||||
|
||||
describe('discover and visualize privileges', () => {
|
||||
|
|
|
@ -17,6 +17,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const queryBar = getService('queryBar');
|
||||
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', () => {
|
||||
after(async () => {
|
||||
// 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 savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
||||
|
||||
// more tests are in x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts
|
||||
|
||||
describe('visualize feature controls security', () => {
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue