/* * Copyright OpenSearch Contributors * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://www.apache.org/licenses/LICENSE-2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ import { EuiBasicTableColumn, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiGlobalToastList, EuiIcon, EuiInMemoryTable, EuiPageBody, EuiPageContent, EuiPageContentHeader, EuiPageContentHeaderSection, EuiPageHeader, EuiSearchBarProps, EuiText, EuiTitle, RIGHT_ALIGNMENT, EuiButtonEmpty, Query, } from '@elastic/eui'; import { difference } from 'lodash'; import React, { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react'; import { AppDependencies } from '../../../types'; import { Action, DataObject, ActionGroupItem, ExpandedRowMapInterface } from '../../types'; import { PermissionListingItem, requestDeleteActionGroups, updateActionGroup, fetchActionGroups, mergeAllPermissions, } from '../../utils/action-groups-utils'; import { stringToComboBoxOption } from '../../utils/combo-box-utils'; import { ExternalLink, renderCustomization, tableItemsUIProps } from '../../utils/display-utils'; import { useToastState, getSuccessToastMessage } from '../../utils/toast-utils'; import { PermissionEditModal } from './edit-modal'; import { PermissionTree } from '../permission-tree'; import { showTableStatusMessage } from '../../utils/loading-spinner-utils'; import { useDeleteConfirmState } from '../../utils/delete-confirm-modal-utils'; import { useContextMenuState } from '../../utils/context-menu'; import { generateResourceName } from '../../utils/resource-utils'; import { DocLinks } from '../../constants'; export function renderBooleanToCheckMark(value: boolean): React.ReactNode { return value ? : ''; } export function toggleRowDetails( item: PermissionListingItem, actionGroupDict: DataObject, setItemIdToExpandedRowMap: Dispatch> ) { setItemIdToExpandedRowMap((prevState) => { const itemIdToExpandedRowMapValues = { ...prevState }; if (itemIdToExpandedRowMapValues[item.name]) { delete itemIdToExpandedRowMapValues[item.name]; } else { itemIdToExpandedRowMapValues[item.name] = ( ); } return itemIdToExpandedRowMapValues; }); } export function renderRowExpanstionArrow( itemIdToExpandedRowMap: ExpandedRowMapInterface, actionGroupDict: DataObject, setItemIdToExpandedRowMap: Dispatch> ) { return (item: PermissionListingItem) => item.type === 'Action group' && ( toggleRowDetails(item, actionGroupDict, setItemIdToExpandedRowMap)} aria-label={itemIdToExpandedRowMap[item.name] ? 'Collapse' : 'Expand'} iconType={itemIdToExpandedRowMap[item.name] ? 'arrowUp' : 'arrowDown'} /> ); } function getColumns( itemIdToExpandedRowMap: ExpandedRowMapInterface, actionGroupDict: DataObject, setItemIdToExpandedRowMap: Dispatch> ): Array> { return [ { field: 'name', name: 'Name', sortable: true, }, { field: 'type', name: 'Type', sortable: true, }, { field: 'hasClusterPermission', name: 'Cluster permission', render: renderBooleanToCheckMark, }, { field: 'hasIndexPermission', name: 'Index permission', render: renderBooleanToCheckMark, }, { field: 'reserved', name: 'Customization', render: (reserved: boolean) => { return renderCustomization(reserved, tableItemsUIProps); }, }, { align: RIGHT_ALIGNMENT, width: '40px', isExpander: true, render: renderRowExpanstionArrow( itemIdToExpandedRowMap, actionGroupDict, setItemIdToExpandedRowMap ), }, ]; } const SEARCH_OPTIONS: EuiSearchBarProps = { box: { placeholder: 'Search for action group name or permission name' }, filters: [ { type: 'field_value_selection', field: 'type', name: 'All types', options: [{ value: 'Action group' }, { value: 'Single permission' }], multiSelect: false, }, { type: 'field_value_selection', name: 'All permissions', options: [ { field: 'hasClusterPermission', name: 'Cluster permissions', value: true }, { field: 'hasIndexPermission', name: 'Index permissions', value: true }, ], }, { type: 'field_value_selection', field: 'reserved', name: 'Customization', multiSelect: false, options: [ { value: true, view: renderCustomization(true, tableItemsUIProps), }, { value: false, view: renderCustomization(false, tableItemsUIProps), }, ], }, ], }; export function PermissionList(props: AppDependencies) { const [permissionList, setPermissionList] = useState([]); const [actionGroupDict, setActionGroupDict] = useState>({}); const [errorFlag, setErrorFlag] = useState(false); const [selection, setSelection] = React.useState([]); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState({}); // Modal state const [editModal, setEditModal] = useState(null); const [toasts, addToast, removeToast] = useToastState(); const [loading, setLoading] = useState(false); const [query, setQuery] = useState(null); const fetchData = useCallback(async () => { try { setLoading(true); const actionGroups = await fetchActionGroups(props.coreStart.http); setActionGroupDict(actionGroups); setPermissionList(await mergeAllPermissions(actionGroups)); } catch (e) { console.log(e); setErrorFlag(true); } finally { setLoading(false); } }, [props.coreStart.http]); React.useEffect(() => { fetchData(); }, [props.coreStart.http, fetchData]); const handleDelete = async () => { const groupsToDelete: string[] = selection.map((r) => r.name); try { await requestDeleteActionGroups(props.coreStart.http, groupsToDelete); setPermissionList(difference(permissionList, selection)); setSelection([]); } catch (e) { console.log(e); } finally { closeActionsMenu(); } }; const [showDeleteConfirmModal, deleteConfirmModal] = useDeleteConfirmState( handleDelete, 'action group(s)' ); const actionsMenuItems = [ showEditModal(selection[0].name, Action.edit, selection[0].allowedActions)} disabled={selection.length !== 1 || selection[0].reserved} > Edit , showEditModal( generateResourceName(Action.duplicate, selection[0].name), Action.duplicate, selection[0].allowedActions ) } disabled={selection.length !== 1 || selection[0].type !== 'Action group'} > Duplicate , group.reserved)} > Delete , ]; const [actionsMenu, closeActionsMenu] = useContextMenuState('Actions', {}, actionsMenuItems); const showEditModal = ( initialGroupName: string, action: Action, initialAllowedAction: string[] ) => { setEditModal( stringToComboBoxOption(group.name))} handleClose={() => setEditModal(null)} handleSave={async (groupName, allowedAction) => { try { await updateActionGroup(props.coreStart.http, groupName, { allowed_actions: allowedAction, }); setEditModal(null); fetchData(); addToast({ id: 'saveSucceeded', title: getSuccessToastMessage('Action group', action, groupName), color: 'success', }); } catch (e) { console.log(e); setEditModal(null); addToast({ id: 'saveFailed', title: `Failed to save ${action} group. You may refresh the page to retry or see browser console for more information.`, color: 'danger', }); } }} /> ); }; const createActionGroupMenuItems = [ showEditModal('', Action.create, [])}> Create from blank , showEditModal( '', Action.create, selection.map((item) => item.name) ) } disabled={selection.length === 0} > Create from selection , ]; const [createActionGroupMenu] = useContextMenuState( 'Create action group', { fill: true }, createActionGroupMenuItems ); return ( <>

Permissions

Permissions {' '} ({Query.execute(query || '', permissionList).length})

Permissions are individual actions, such as cluster:admin/snapshot/restore, which lets you restore snapshots. Action groups are reusable collections of permissions, such as MANAGE_SNAPSHOTS, which lets you view, take, delete, and restore snapshots. You can often meet your security needs using the default action groups, but you might find it convenient to create your own.
{actionsMenu} {createActionGroupMenu}
{ setQuery(arg.query); return true; }, }} selection={{ onSelectionChange: setSelection }} sorting={{ sort: { field: 'type', direction: 'asc' } }} error={errorFlag ? 'Load data failed, please check console log for more detail.' : ''} isExpandable={true} itemIdToExpandedRowMap={itemIdToExpandedRowMap} message={showTableStatusMessage(loading, permissionList)} />
{editModal} {deleteConfirmModal} ); }