Fixed the UI related issues reported during testing for Shared Storage in Server Mode. #5014

This commit is contained in:
Nikhil Mohite
2023-03-24 11:08:27 +05:30
committed by GitHub
parent 8b815d4aac
commit e4eeba2aa7
10 changed files with 53 additions and 23 deletions
+7 -5
View File
@@ -53,12 +53,14 @@ Shared Storage
:alt: Other options :alt: Other options
:align: center :align: center
In shared storage the ``My Storage`` is the user's storage directory, and other directories are shared storage set by
the admin user. Using this shared storage users can share the files with other users through pgAdmin.
Admin users can mark the shared storage as restricted to restrict non-admin users from deleting, uploading,
adding, and renaming files/folders in shared storage by setting the restricted_access flag in config.
*NOTE: You must ensure the directories specified are writeable by the user that the web server processes will be running as, e.g. apache or www-data.* In the storage manager, ``My Storage`` is the pgAdmin users storage directory, and other listed directories are shared
storages set by the pgAdmin server administrator. Using these, pgAdmin users can have common storages to share files.
pgAdmin server administrator can configure the shared storages using the <link>config file</link>. Storages can be
marked as restricted to give read-only access to non-admin pgAdmin users.
.. note:: You must ensure the directories specified are writeable by the user that the web server processes will be running as, e.g. apache or www-data.*
Other Options Other Options
+8 -2
View File
@@ -559,8 +559,11 @@ def get_shared_storage_list():
config.SHARED_STORAGE = shared_storage_config config.SHARED_STORAGE = shared_storage_config
shared_storage_list = [sdir['name'] for sdir in shared_storage_config] shared_storage_list = [sdir['name'] for sdir in shared_storage_config]
restricted_shared_storage_list = [sdir['name'] for sdir in
shared_storage_config if
sdir['restricted_access']]
return shared_storage_list return shared_storage_list, restricted_shared_storage_list
@blueprint.route("/js/utils.js") @blueprint.route("/js/utils.js")
@@ -630,7 +633,8 @@ def utils():
auth_source = session['auth_source_manager'][ auth_source = session['auth_source_manager'][
'source_friendly_name'] 'source_friendly_name']
shared_storage_list = get_shared_storage_list() shared_storage_list, \
restricted_shared_storage_list = get_shared_storage_list()
return make_response( return make_response(
render_template( render_template(
@@ -665,6 +669,8 @@ def utils():
password_length_min=config.PASSWORD_LENGTH_MIN, password_length_min=config.PASSWORD_LENGTH_MIN,
current_ui_lock=current_ui_lock, current_ui_lock=current_ui_lock,
shared_storage_list=shared_storage_list, shared_storage_list=shared_storage_list,
restricted_shared_storage_list=[] if current_user.has_role(
"Administrator") else restricted_shared_storage_list,
), ),
200, {'Content-Type': MIMETYPE_APP_JS}) 200, {'Content-Type': MIMETYPE_APP_JS})
@@ -57,6 +57,7 @@ define('pgadmin.browser.utils',
/* GET PSQL Tool related config */ /* GET PSQL Tool related config */
pgAdmin['enable_psql'] = '{{enable_psql}}' == 'True'; pgAdmin['enable_psql'] = '{{enable_psql}}' == 'True';
pgAdmin['shared_storage'] = {{shared_storage_list}} pgAdmin['shared_storage'] = {{shared_storage_list}}
pgAdmin['restricted_shared_storage'] = {{restricted_shared_storage_list}}
pgAdmin['platform'] = '{{platform}}'; pgAdmin['platform'] = '{{platform}}';
pgAdmin['qt_default_placeholder'] = '{{qt_default_placeholder}}' pgAdmin['qt_default_placeholder'] = '{{qt_default_placeholder}}'
pgAdmin['vw_edt_default_placeholder'] = '{{vw_edt_default_placeholder}}' pgAdmin['vw_edt_default_placeholder'] = '{{vw_edt_default_placeholder}}'
+5 -5
View File
@@ -17,6 +17,7 @@ import time
from urllib.parse import unquote from urllib.parse import unquote
from sys import platform as _platform from sys import platform as _platform
from flask_security import current_user from flask_security import current_user
from pgadmin.utils.constants import ACCESS_DENIED_MESSAGE
import config import config
import codecs import codecs
import pathlib import pathlib
@@ -792,11 +793,10 @@ class Filemanager():
if selectedDir[ if selectedDir[
'restricted_access'] and not current_user.has_role( 'restricted_access'] and not current_user.has_role(
"Administrator"): "Administrator"):
raise PermissionError(gettext( return make_json_response(success=0,
"Access denied: This shared folder has restricted " errormsg=ACCESS_DENIED_MESSAGE,
"access, you are not allowed to rename, delete, " info='ACCESS_DENIED',
"or upload any files/folders. Please contact " status=403)
"Administrator to gain access."))
def rename(self, old=None, new=None): def rename(self, old=None, new=None):
""" """
@@ -75,6 +75,7 @@ export default class FileManagerModule {
onCancel={onCancel} onCancel={onCancel}
onOK={onOK} onOK={onOK}
sharedStorages={this.pgAdmin.server_mode == 'True' ? this.pgAdmin.shared_storage: []} sharedStorages={this.pgAdmin.server_mode == 'True' ? this.pgAdmin.shared_storage: []}
restrictedSharedStorage={this.pgAdmin.server_mode == 'True' ? this.pgAdmin.restricted_shared_storage: []}
/> />
); );
}, { }, {
@@ -402,7 +402,7 @@ ConfirmFile.propTypes = {
onNo: PropTypes.func onNo: PropTypes.func
}; };
export default function FileManager({params, closeModal, onOK, onCancel, sharedStorages=[]}) { export default function FileManager({params, closeModal, onOK, onCancel, sharedStorages=[], restrictedSharedStorage=[]}) {
const classes = useStyles(); const classes = useStyles();
const modalClasses = useModalStyles(); const modalClasses = useModalStyles();
const apiObj = useMemo(()=>getApiInstance(), []); const apiObj = useMemo(()=>getApiInstance(), []);
@@ -462,7 +462,7 @@ export default function FileManager({params, closeModal, onOK, onCancel, sharedS
let newItems = await fmUtilsObj.getFolder(dirPath || fmUtilsObj.currPath, changeStoragePath); let newItems = await fmUtilsObj.getFolder(dirPath || fmUtilsObj.currPath, changeStoragePath);
setItems(newItems); setItems(newItems);
setPath(fmUtilsObj.currPath); setPath(fmUtilsObj.currPath);
params.dialog_type == 'storage_dialog' && fmUtilsObj.setLastVisitedDir(fmUtilsObj.currPath, selectedSS); setTimeout(()=>{fmUtilsObj.setLastVisitedDir(dirPath || fmUtilsObj.currPath, changeStoragePath);}, 100);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setErrorMsg(parseApiError(error)); setErrorMsg(parseApiError(error));
@@ -726,7 +726,7 @@ export default function FileManager({params, closeModal, onOK, onCancel, sharedS
{params.dialog_type == 'storage_dialog' && {params.dialog_type == 'storage_dialog' &&
<PgIconButton title={gettext('Download')} icon={<GetAppRoundedIcon />} <PgIconButton title={gettext('Download')} icon={<GetAppRoundedIcon />}
onClick={onDownload} disabled={showUploader || isNoneSelected || selectedRow?.file_type == 'dir' || selectedRow?.file_type == 'drive'} />} onClick={onDownload} disabled={showUploader || isNoneSelected || selectedRow?.file_type == 'dir' || selectedRow?.file_type == 'drive'} />}
{fmUtilsObj.hasCapability('create') && <PgIconButton title={gettext('New Folder')} icon={<CreateNewFolderRoundedIcon />} {fmUtilsObj.hasCapability('create') && !restrictedSharedStorage.includes(selectedSS) && <PgIconButton title={gettext('New Folder')} icon={<CreateNewFolderRoundedIcon />}
onClick={onAddFolder} disabled={showUploader} />} onClick={onAddFolder} disabled={showUploader} />}
</PgButtonGroup> </PgButtonGroup>
<PgButtonGroup size="small" style={{marginLeft: '4px'}}> <PgButtonGroup size="small" style={{marginLeft: '4px'}}>
@@ -739,13 +739,13 @@ export default function FileManager({params, closeModal, onOK, onCancel, sharedS
onClose={onMenuClose} onClose={onMenuClose}
label={gettext('Options')} label={gettext('Options')}
> >
{fmUtilsObj.hasCapability('rename') && <PgMenuItem hasCheck onClick={renameSelectedItem} disabled={isNoneSelected}> {fmUtilsObj.hasCapability('rename') && !restrictedSharedStorage.includes(selectedSS) && <PgMenuItem hasCheck onClick={renameSelectedItem} disabled={isNoneSelected}>
{gettext('Rename')} {gettext('Rename')}
</PgMenuItem>} </PgMenuItem>}
{fmUtilsObj.hasCapability('delete') && <PgMenuItem hasCheck onClick={deleteSelectedItem} disabled={isNoneSelected}> {fmUtilsObj.hasCapability('delete') && !restrictedSharedStorage.includes(selectedSS) && <PgMenuItem hasCheck onClick={deleteSelectedItem} disabled={isNoneSelected}>
{gettext('Delete')} {gettext('Delete')}
</PgMenuItem>} </PgMenuItem>}
{fmUtilsObj.hasCapability('upload') && <> {fmUtilsObj.hasCapability('upload') && !restrictedSharedStorage.includes(selectedSS) && <>
<PgMenuDivider /> <PgMenuDivider />
<PgMenuItem hasCheck onClick={(e)=>{ <PgMenuItem hasCheck onClick={(e)=>{
e.keepOpen = false; e.keepOpen = false;
@@ -820,7 +820,7 @@ export default function FileManager({params, closeModal, onOK, onCancel, sharedS
onChange={(e)=>{ onChange={(e)=>{
let val = e.target.value; let val = e.target.value;
fmUtilsObj.setFileType(val); fmUtilsObj.setFileType(val);
openDir(fmUtilsObj.currPath); openDir(fmUtilsObj.currPath, selectedSS);
setFileType(val); setFileType(val);
}} }}
options={fmUtilsObj.allowedFileTypes?.map((type)=>({ options={fmUtilsObj.allowedFileTypes?.map((type)=>({
@@ -852,4 +852,5 @@ FileManager.propTypes = {
onOK: PropTypes.func, onOK: PropTypes.func,
onCancel: PropTypes.func, onCancel: PropTypes.func,
sharedStorages: PropTypes.array, sharedStorages: PropTypes.array,
restrictedSharedStorage: PropTypes.array,
}; };
+14 -2
View File
@@ -17,7 +17,7 @@ from threading import Lock
import json import json
from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT,\ from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT,\
ALLOW_SAVE_PASSWORD ALLOW_SAVE_PASSWORD, SHARED_STORAGE
from werkzeug.user_agent import UserAgent from werkzeug.user_agent import UserAgent
from flask import Response, url_for, render_template, session, current_app from flask import Response, url_for, render_template, session, current_app
from flask import request from flask import request
@@ -52,7 +52,7 @@ from pgadmin.tools.sqleditor.utils.macros import get_macros,\
get_user_macros, set_macros get_user_macros, set_macros
from pgadmin.utils.constants import MIMETYPE_APP_JS, \ from pgadmin.utils.constants import MIMETYPE_APP_JS, \
SERVER_CONNECTION_CLOSED, ERROR_MSG_TRANS_ID_NOT_FOUND, \ SERVER_CONNECTION_CLOSED, ERROR_MSG_TRANS_ID_NOT_FOUND, \
ERROR_FETCHING_DATA, MY_STORAGE ERROR_FETCHING_DATA, MY_STORAGE, ACCESS_DENIED_MESSAGE
from pgadmin.model import Server, ServerGroup from pgadmin.model import Server, ServerGroup
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.settings import get_setting from pgadmin.settings import get_setting
@@ -1856,6 +1856,18 @@ def save_file():
last_storage = Preferences.module('file_manager').preference( last_storage = Preferences.module('file_manager').preference(
'last_storage').get() 'last_storage').get()
if last_storage != MY_STORAGE: if last_storage != MY_STORAGE:
selectedDirList = [sdir for sdir in SHARED_STORAGE if
sdir['name'] == last_storage]
selectedDir = selectedDirList[0] if len(
selectedDirList) == 1 else None
if selectedDir:
if selectedDir['restricted_access'] and \
not current_user.has_role("Administrator"):
return make_json_response(success=0,
errormsg=ACCESS_DENIED_MESSAGE,
info='ACCESS_DENIED',
status=403)
storage_manager_path = get_storage_directory( storage_manager_path = get_storage_directory(
shared_storage=last_storage) shared_storage=last_storage)
else: else:
@@ -422,6 +422,8 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
}).catch((err)=>{ }).catch((err)=>{
eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err); eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, err);
}); });
} else if(error.response?.status == 403 && error.response?.data.info == 'ACCESS_DENIED') {
Notifier.error(error.response.data.errormsg);
}else { }else {
let msg = parseApiError(error); let msg = parseApiError(error);
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, msg, true); eventBus.current.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, msg, true);
+3
View File
@@ -122,3 +122,6 @@ PSYCOPG3 = 'psycopg3'
# Shared storage # Shared storage
MY_STORAGE = 'my_storage' MY_STORAGE = 'my_storage'
ACCESS_DENIED_MESSAGE = gettext(
"Access denied: Youre having limited access. Youre not allowed to "
"Rename, Delete or Create any files/folders")
@@ -42,7 +42,6 @@ const files = [
} }
} }
]; ];
const transId = 140391; const transId = 140391;
const configData = { const configData = {
'transId': transId, 'transId': transId,
@@ -87,6 +86,7 @@ const configData = {
}; };
const sharedStorageConfig = ['Shared Storage']; const sharedStorageConfig = ['Shared Storage'];
const restrictedSharedStorage = [];
const params={ const params={
dialog_type: 'select_file', dialog_type: 'select_file',
@@ -127,6 +127,7 @@ describe('FileManger', ()=>{
onOK={onOK} onOK={onOK}
onCancel={onCancel} onCancel={onCancel}
sharedStorages={sharedStorageConfig} sharedStorages={sharedStorageConfig}
restrictedSharedStorage={restrictedSharedStorage}
{...props} {...props}
/> />
</Theme>); </Theme>);
@@ -134,6 +135,7 @@ describe('FileManger', ()=>{
it('init', (done)=>{ it('init', (done)=>{
networkMock.onPost('/file_manager/init').reply(200, {'data': configData}); networkMock.onPost('/file_manager/init').reply(200, {'data': configData});
networkMock.onPost(`/file_manager/save_last_dir/${transId}`).reply(200, {'success':1,'errormsg':'','info':'','result':null,'data':null});
let ctrl = ctrlMount({}); let ctrl = ctrlMount({});
setTimeout(()=>{ setTimeout(()=>{
ctrl.update(); ctrl.update();