From a6b8572201fd02a0f25db61b624a1489177c7038 Mon Sep 17 00:00:00 2001 From: Aleksandr Gorodetskii <41908792+AleksandrGorodetskii@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:23:52 +0300 Subject: [PATCH] GUI Storage admin role (#3389): add storage admin role (#3392) * GUI Storage admin role (#3389): add storage admin role * GUI Storage admin role (#3389): restrict grant user permissions search input to minimum 3 character length * GUI Storage admin role (#3389): grant user permissions search input - fix placeholder * GUI Storage admin role (#3389): small fixes * GUI Storage admin role (#3389): grant user permissions search input - style adjustments --- .../components/pipelines/browser/Folder.js | 29 +++-- .../pipelines/browser/data-storage/index.js | 14 +- .../browser/forms/DataStorageEditDialog.js | 37 +++--- .../components/roleModel/PermissionsForm.js | 121 ++++++++++-------- .../components/settings/UserManagementForm.js | 2 +- client/src/utils/roleModel.js | 2 + 6 files changed, 118 insertions(+), 87 deletions(-) diff --git a/client/src/components/pipelines/browser/Folder.js b/client/src/components/pipelines/browser/Folder.js index fa960751db..149cede3c0 100644 --- a/client/src/components/pipelines/browser/Folder.js +++ b/client/src/components/pipelines/browser/Folder.js @@ -68,7 +68,7 @@ import ConfigurationDelete from '../../../models/configuration/ConfigurationDele import CreateDataStorage from '../../../models/dataStorage/DataStorageSave'; import UpdateDataStorage from '../../../models/dataStorage/DataStorageUpdate'; import DataStorageUpdateStoragePolicy - from '../../../models/dataStorage/DataStorageUpdateStoragePolicy'; +from '../../../models/dataStorage/DataStorageUpdateStoragePolicy'; import DataStorageDelete from '../../../models/dataStorage/DataStorageDelete'; import {METADATA_KEYS} from './metadata-controls/get-default-metadata-properties'; import Metadata, {SpecialTags} from '../../special/metadata/Metadata'; @@ -519,7 +519,10 @@ export default class Folder extends localization.LocalizedReactComponent { } break; case ItemTypes.storage: - if (roleModel.writeAllowed(item)) { + if ( + roleModel.isManager.storageAdmin(this) || + roleModel.writeAllowed(item) + ) { actions.push( diff --git a/client/src/components/pipelines/browser/data-storage/index.js b/client/src/components/pipelines/browser/data-storage/index.js index b337ae479e..f9910ad3d8 100644 --- a/client/src/components/pipelines/browser/data-storage/index.js +++ b/client/src/components/pipelines/browser/data-storage/index.js @@ -285,6 +285,7 @@ export default class DataStorage extends React.Component { const isAdmin = authenticatedUserInfo.value.admin; const isOwner = roleModel.isOwner(this.storage.info); return isAdmin || + roleModel.isManager.storageAdmin(this) || (isOwner && preferences.storagePolicyBackupVisibleNonAdmins); } return false; @@ -309,15 +310,15 @@ export default class DataStorage extends React.Component { const readAllowed = roleModel.readAllowed(this.storage.info); const writeAllowed = roleModel.writeAllowed(this.storage.info); return { - read: ( + read: roleModel.isManager.storageAdmin(this) || (( roleModel.isOwner(this.storage.info) || roleModel.isManager.archiveManager(this) || roleModel.isManager.archiveReader(this) - ) && readAllowed && isS3, - write: ( + ) && readAllowed && isS3), + write: roleModel.isManager.storageAdmin(this) || (( roleModel.isOwner(this.storage.info) || roleModel.isManager.archiveManager(this) - ) && writeAllowed && isS3 + ) && writeAllowed && isS3) }; } @@ -403,10 +404,11 @@ export default class DataStorage extends React.Component { ? authenticatedUserInfo.value.admin : false; // Whilst in the restricted tag access mode, only admins and users (including owners) with roles - // STORAGE_MANAGER or STORAGE_TAG_MANAGER are allowed to edit file's tags. + // STORAGE_MANAGER STORAGE_ADMIN or STORAGE_TAG_MANAGER are allowed to edit file's tags. const restrictedAccessCheck = isAdmin || roleModel.isManager.storage(this) || - roleModel.isManager.storageTag(this); + roleModel.isManager.storageTag(this) || + roleModel.isManager.storageAdmin(this); const storageFileTagsEditable = this.storageTagRestrictedAccess ? restrictedAccessCheck // If restricted tag access mode is off, all users with WRITE permissions are diff --git a/client/src/components/pipelines/browser/forms/DataStorageEditDialog.js b/client/src/components/pipelines/browser/forms/DataStorageEditDialog.js index ef99fa09d5..945efbf250 100644 --- a/client/src/components/pipelines/browser/forms/DataStorageEditDialog.js +++ b/client/src/components/pipelines/browser/forms/DataStorageEditDialog.js @@ -103,7 +103,8 @@ export class DataStorageEditDialog extends React.Component { authenticatedUserInfo && authenticatedUserInfo.loaded; if (loaded) { - const isAdmin = authenticatedUserInfo.value.admin; + const isAdmin = authenticatedUserInfo.value.admin || + roleModel.isManager.storageAdmin(this); const isOwner = roleModel.isOwner(dataStorage); return isAdmin || (isOwner && preferences.storagePolicyBackupVisibleNonAdmins); @@ -172,15 +173,15 @@ export class DataStorageEditDialog extends React.Component { const readAllowed = roleModel.readAllowed(dataStorage); const writeAllowed = roleModel.writeAllowed(dataStorage); return { - read: ( + read: roleModel.isManager.storageAdmin(this) || (( roleModel.isOwner(dataStorage) || roleModel.isManager.archiveManager(this) || roleModel.isManager.archiveReader(this) - ) && readAllowed, - write: ( + ) && readAllowed), + write: roleModel.isManager.storageAdmin(this) || (( roleModel.isOwner(dataStorage) || roleModel.isManager.archiveManager(this) - ) && writeAllowed + ) && writeAllowed) }; } @@ -253,7 +254,7 @@ export class DataStorageEditDialog extends React.Component { getEditFooter = () => { if ( - roleModel.isOwner(this.props.dataStorage) && + (roleModel.isManager.storageAdmin(this) || roleModel.isOwner(this.props.dataStorage)) && !this.state.restrictedAccess ) { return ( @@ -261,12 +262,17 @@ export class DataStorageEditDialog extends React.Component { { - roleModel.manager.storage( - - ) + roleModel.isManager.storage(this) || + roleModel.isManager.storageAdmin(this) + ? ( + + ) : null } @@ -371,9 +377,10 @@ export class DataStorageEditDialog extends React.Component { const isReadOnly = this.props.dataStorage ? ( this.props.dataStorage.locked || - !roleModel.isOwner(this.props.dataStorage) || - this.state.restrictedAccess - ) + this.state.restrictedAccess || ( + !roleModel.isOwner(this.props.dataStorage) && + !roleModel.isManager.storageAdmin(this) + )) : false; const modalFooter = this.props.pending || this.state.restrictedAccessCheckInProgress ? false : ( this.props.dataStorage ? this.getEditFooter() : this.getCreateFooter() diff --git a/client/src/components/roleModel/PermissionsForm.js b/client/src/components/roleModel/PermissionsForm.js index 0864fc9cef..bee30b61b5 100644 --- a/client/src/components/roleModel/PermissionsForm.js +++ b/client/src/components/roleModel/PermissionsForm.js @@ -27,9 +27,10 @@ import { Modal, Popover, Row, - Table + Table, + Select } from 'antd'; -import {isObservableArray, observable} from 'mobx'; +import {isObservableArray, observable, computed} from 'mobx'; import {inject, observer} from 'mobx-react'; import classNames from 'classnames'; import GrantGet from '../../models/grant/GrantGet'; @@ -86,14 +87,15 @@ export default class PermissionsForm extends React.Component { findGroupVisible: false, selectedPermission: null, groupSearchString: null, - selectedUser: null, + selectedUser: undefined, owner: null, ownerInput: null, fetching: false, fetchedUsers: [], roleName: null, operationInProgress: false, - subObjectsPermissions: [] + subObjectsPermissions: [], + searchUserTouched: false }; operationWrapper = (operation) => (...props) => { @@ -107,8 +109,6 @@ export default class PermissionsForm extends React.Component { }); }; - @observable - userFind; @observable groupFind; @@ -150,6 +150,14 @@ export default class PermissionsForm extends React.Component { showOwner: true } + @computed + get allUsers () { + if (this.props.usersInfo.loaded) { + return this.props.usersInfo.value || []; + } + return []; + } + lastFetchId = 0; findUser = (value) => { @@ -159,7 +167,7 @@ export default class PermissionsForm extends React.Component { ownerInput: value, owner: null, fetching: true, - selectedUser: null + selectedUser: undefined }, async () => { const request = new UserFind(value); await request.fetch(); @@ -194,7 +202,7 @@ export default class PermissionsForm extends React.Component { if (request.error) { message.error(request.error); this.setState({ - selectedUser: null, + selectedUser: undefined, fetchedUsers: [], owner: null, ownerInput: null @@ -202,7 +210,7 @@ export default class PermissionsForm extends React.Component { } else { await this.props.grant.fetch(); this.setState({ - selectedUser: null, + selectedUser: undefined, fetchedUsers: [], owner: null, ownerInput: null @@ -218,13 +226,7 @@ export default class PermissionsForm extends React.Component { }; onUserFindInputChanged = (value) => { - this.selectedUser = value; - if (value && value.length) { - this.userFind = new UserFind(value); - this.userFind.fetch(); - } else { - this.userFind = null; - } + this.setState({selectedUser: value}); }; onGroupFindInputChanged = (value) => { @@ -251,23 +253,6 @@ export default class PermissionsForm extends React.Component { ); }; - findUserDataSource = () => { - if (this.userFind && !this.userFind.pending && !this.userFind.error) { - const {permissions = []} = this.props.grant && this.props.grant.loaded - ? (this.props.grant.value || {}) - : {}; - const existingUsers = new Set( - permissions - .filter(p => p.sid && p.sid.principal) - .map(p => p.sid.name) - ); - return (this.userFind.value || []) - .filter(user => !existingUsers.has(user.userName)) - .map(user => user); - } - return []; - }; - splitRoleName = (name) => { if (name && name.toLowerCase().indexOf('role_') === 0) { return name.substring('role_'.length); @@ -296,16 +281,19 @@ export default class PermissionsForm extends React.Component { return [...roles]; }; - selectedUser = null; selectedGroup = null; openFindUserDialog = () => { - this.selectedUser = null; - this.setState({findUserVisible: true}); + this.setState({ + findUserVisible: true + }); }; closeFindUserDialog = () => { - this.setState({findUserVisible: false}); + this.setState({ + selectedUser: undefined, + findUserVisible: false + }); }; getDefaultMaskForSubject = (subject, isPrincipal) => { @@ -324,9 +312,9 @@ export default class PermissionsForm extends React.Component { onSelectUser = async () => { await this.grantPermission( - this.selectedUser, + this.state.selectedUser, true, - this.getDefaultMaskForSubject(this.selectedUser, true) + this.getDefaultMaskForSubject(this.state.selectedUser, true) ); this.closeFindUserDialog(); }; @@ -434,9 +422,6 @@ export default class PermissionsForm extends React.Component { renderSubObjectsWarnings = () => { const {subObjectsPermissionsErrorTitle} = this.props; - const users = this.props.usersInfo.loaded - ? (this.props.usersInfo.value || []).slice() - : []; const granted = this.props.grant.value && this.props.grant.value.permissions ? this.props.grant.value.permissions : []; @@ -454,7 +439,7 @@ export default class PermissionsForm extends React.Component { const {name, principal} = sid; const rolesToCheck = []; if (principal) { - const userInfo = users.find(u => u.name === name); + const userInfo = this.allUsers.find(u => u.name === name); if (userInfo && userInfo.roles) { rolesToCheck.push( ...(userInfo.roles || []).map(({name}) => ({name, principal: false})) @@ -909,22 +894,46 @@ export default class PermissionsForm extends React.Component { )} visible={this.state.findUserVisible}> - + showSearch + value={this.state.selectedUser} + onSelect={this.onUserFindInputChanged} + filterOption={(input, option) => option.props.attributes + .map(o => o.toLowerCase()) + .find(o => o.includes((input || '').toLowerCase())) + } + onSearch={(value) => this.setState({ + searchUserTouched: value.length > 2} + )} + onFocus={() => this.setState({searchUserTouched: false})} + notFoundContent={this.state.searchUserTouched + ? 'Not found' + : 'Start typing to filter users...' + } + > { - (this.findUserDataSource() || []).map(user => { - return ( - - {this.renderUserName(user)} - - ); - }) + this.state.searchUserTouched ? ( + this.allUsers + .map(user => ( + + + + )) + ) : null } - + roleModel.readAllowed(user)); + const userHasReadPermissions = (users || []).some((user) => roleModel.readAllowed(user)); if (!isReader && !isAdmin && !userHasReadPermissions) { return ( diff --git a/client/src/utils/roleModel.js b/client/src/utils/roleModel.js index d6c24515d7..7ae12a066c 100644 --- a/client/src/utils/roleModel.js +++ b/client/src/utils/roleModel.js @@ -364,6 +364,7 @@ const refreshAuthenticationInfo = async ({props}) => { const manager = { archiveReader: management('ROLE_STORAGE_ARCHIVE_READER'), archiveManager: management('ROLE_STORAGE_ARCHIVE_MANAGER'), + storageAdmin: management('ROLE_STORAGE_ADMIN'), pipeline: management('ROLE_PIPELINE_MANAGER'), versionedStorage: management('ROLE_VERSIONED_STORAGE_MANAGER'), folder: management('ROLE_FOLDER_MANAGER'), @@ -378,6 +379,7 @@ const manager = { const isManager = { archiveReader: hasRole('ROLE_STORAGE_ARCHIVE_READER'), archiveManager: hasRole('ROLE_STORAGE_ARCHIVE_MANAGER'), + storageAdmin: hasRole('ROLE_STORAGE_ADMIN'), pipeline: hasRole('ROLE_PIPELINE_MANAGER'), versionedStorage: hasRole('ROLE_VERSIONED_STORAGE_MANAGER'), folder: hasRole('ROLE_FOLDER_MANAGER'),