def _parse_form_xfer(xfer, user_args, client_id, configuration): """Parse xfer request (i.e. copy, move or upload) file/dir entries from user_args. """ _logger = configuration.logger files, rejected = [], [] i = 0 client_dir = client_id_dir(client_id) base_dir = os.path.abspath(os.path.join(configuration.user_home, client_dir)) + os.sep xfer_pattern = 'freeze_%s_%%d' % xfer for i in xrange(max_freeze_files): if user_args.has_key(xfer_pattern % i): source_path = user_args[xfer_pattern % i][-1].strip() source_path = os.path.normpath(source_path).lstrip(os.sep) _logger.debug('found %s entry: %s' % (xfer, source_path)) if not source_path: continue try: valid_path(source_path) except Exception, exc: rejected.append('invalid path: %s (%s)' % (source_path, exc)) continue # IMPORTANT: path must be expanded to abs for proper chrooting abs_path = os.path.abspath( os.path.join(base_dir, source_path)) # Prevent out-of-bounds, and restrict some greedy targets if not valid_user_path(configuration, abs_path, base_dir, True): _logger.error('found illegal directory traversal %s entry: %s' % (xfer, source_path)) rejected.append('invalid path: %s (%s)' % (source_path, 'illegal path!')) continue elif os.path.exists(abs_path) and os.path.samefile(abs_path, base_dir): _logger.warning('refusing archival of entire user home %s: %s' % (xfer, source_path)) rejected.append('invalid path: %s (%s)' % (source_path, 'entire home not allowed!')) continue elif in_vgrid_share(configuration, abs_path) == source_path: _logger.warning( 'refusing archival of entire vgrid shared folder %s: %s' % (xfer, source_path)) rejected.append('invalid path: %s (%s)' % (source_path, 'entire %s share not allowed!' % configuration.site_vgrid_label)) continue # expand any dirs recursively if os.path.isdir(abs_path): for (root, dirnames, filenames) in os.walk(abs_path): for subname in filenames: abs_sub = os.path.join(root, subname) sub_base = root.replace(abs_path, source_path) sub_path = os.path.join(sub_base, subname) files.append((abs_sub, sub_path.lstrip(os.sep))) else: files.append((abs_path, source_path.lstrip(os.sep)))
if verbose(flags): output_objects.append( {'object_type': 'file', 'name': relative_path}) # Prevent vgrid share copy which would create read-only dot dirs # Generally refuse handling symlinks including root vgrid shares if os.path.islink(abs_path): output_objects.append( {'object_type': 'warning', 'text': """You're not allowed to copy entire special folders like %s shared folders!""" % configuration.site_vgrid_label}) status = returnvalues.CLIENT_ERROR continue # Additionally refuse operations on inherited subvgrid share roots elif in_vgrid_share(configuration, abs_path) == relative_path: output_objects.append( {'object_type': 'warning', 'text': """You're not allowed to copy entire %s shared folders!""" % configuration.site_vgrid_label}) status = returnvalues.CLIENT_ERROR continue elif os.path.realpath(abs_path) == os.path.realpath(base_dir): logger.error("%s: refusing copy home dir: %s" % (op_name, abs_path)) output_objects.append( {'object_type': 'warning', 'text': "You're not allowed to copy your entire home directory!" }) status = returnvalues.CLIENT_ERROR continue
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False) client_dir = client_id_dir(client_id) defaults = signature()[1] (validate_status, accepted) = validate_input_and_cert( user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, ) if not validate_status: WARNING_MSG = str(accepted) output_objects.append({'object_type': 'warning', 'text': WARNING_MSG}) return (accepted, returnvalues.CLIENT_ERROR) # Convert accpeted values to string and filter out NON-set values accepted_joined_values = { key: ''.join(value) for (key, value) in accepted.iteritems() if len(value) > 0 } action = accepted_joined_values['action'] flags = accepted_joined_values['flags'] path = accepted_joined_values['path'] extension = accepted['extension'][-1].strip() logger.debug('%s from %s: %s' % (op_name, client_id, accepted)) if not action in valid_actions: output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid action "%s" (supported: %s)' % (action, ', '.join(valid_actions)) }) return (output_objects, returnvalues.CLIENT_ERROR) if action in post_actions: if not safe_handler( configuration, 'post', op_name, client_id, get_csrf_limit(configuration), accepted, ): logger.info('checkpoint2') output_objects.append({ 'object_type': 'error_text', 'text': '''Only accepting CSRF-filtered POST requests to prevent unintended updates''' }) return (output_objects, returnvalues.CLIENT_ERROR) # Please note that base_dir must end in slash to avoid access to other # user dirs when own name is a prefix of another user name base_dir = os.path.abspath( os.path.join(configuration.user_home, client_dir)) + os.sep abs_path = os.path.join(base_dir, path) title_entry = find_entry(output_objects, 'title') title_entry['text'] = 'IMAGEPREVIEW Management' output_objects.append({ 'object_type': 'header', 'text': 'IMAGEPREVIEW Management' }) status = returnvalues.ERROR vgrid_name = in_vgrid_share(configuration, abs_path) vgrid_owner = vgrid_is_owner(vgrid_name, client_id, configuration) status = returnvalues.OK if vgrid_name is None: status = returnvalues.ERROR ERROR_MSG = "No vgrid found for path: '%s'" % path output_objects.append({'object_type': 'error_text', 'text': ERROR_MSG}) if status == returnvalues.OK: if action == 'list_settings': status = list_settings(configuration, abs_path, path, output_objects) logger.debug('list exit status: %s' % str(status)) elif action == 'remove_setting': if vgrid_owner == False: status = returnvalues.ERROR ERROR_MSG = \ "Ownership of vgrid: '%s' required to change imagepreview settings" \ % vgrid_name output_objects.append({ 'object_type': 'error_text', 'text': ERROR_MSG }) else: status = remove_setting(configuration, abs_path, path, extension, output_objects) logger.debug('remove_setting exit status: %s' % str(status)) elif action == 'get_setting': status = get_setting(configuration, abs_path, path, extension, output_objects) logger.debug('get_setting exit status: %s' % str(status)) elif action == 'update_setting': if vgrid_owner == False: status = returnvalues.ERROR ERROR_MSG = \ "Ownership of vgrid: '%s' required to change imagepreview settings" \ % vgrid_name output_objects.append({ 'object_type': 'error_text', 'text': ERROR_MSG }) else: status = update_setting( configuration, base_dir, abs_path, path, extension, accepted_joined_values, output_objects, ) logger.debug('update_setting exit status: %s' % str(status)) elif action == 'create_setting': if vgrid_owner == False: status = returnvalues.ERROR ERROR_MSG = \ "Ownership of vgrid: '%s' required to change imagepreview settings" \ % vgrid_name output_objects.append({ 'object_type': 'error_text', 'text': ERROR_MSG }) else: status = create_setting( configuration, client_id, base_dir, abs_path, path, extension, accepted_joined_values, output_objects, ) status = returnvalues.OK logger.debug('create_setting exit status: %s' % str(status)) elif action == 'reset_setting': if vgrid_owner == False: status = returnvalues.ERROR ERROR_MSG = \ "Ownership of vgrid: '%s' required to change imagepreview settings" \ % vgrid_name output_objects.append({ 'object_type': 'error_text', 'text': ERROR_MSG }) else: status = reset_settings(configuration, abs_path, path, output_objects, extension) logger.debug('reset exit status: %s' % str(status)) elif action == 'get': status = get(configuration, base_dir, path, output_objects) logger.debug('get exit status: %s' % str(status)) elif action == 'remove': if vgrid_owner == False: status = returnvalues.ERROR ERROR_MSG = \ "Ownership of vgrid: '%s' required to change imagepreview settings" \ % vgrid_name output_objects.append({ 'object_type': 'error_text', 'text': ERROR_MSG }) else: status = remove(configuration, base_dir, abs_path, path, output_objects) logger.debug('remove exit status: %s' % str(status)) elif action == 'clean': if vgrid_owner == False: status = returnvalues.ERROR ERROR_MSG = \ "Ownership of vgrid: '%s' required to change imagepreview settings" \ % vgrid_name output_objects.append({ 'object_type': 'error_text', 'text': ERROR_MSG }) else: status = clean(configuration, base_dir, abs_path, path, output_objects) logger.debug('clean exit status: %s' % str(status)) elif action == 'cleanrecursive': if vgrid_owner == False: status = returnvalues.ERROR ERROR_MSG = \ "Ownership of vgrid: '%s' required to change imagepreview settings" \ % vgrid_name output_objects.append({ 'object_type': 'error_text', 'text': ERROR_MSG }) else: status = clean( configuration, base_dir, abs_path, path, output_objects, recursive=True, ) logger.debug('cleanrecursive exit status: %s' % str(status)) elif action == 'refresh': if vgrid_owner == False: status = returnvalues.ERROR ERROR_MSG = \ "Ownership of vgrid: '%s' required to change imagepreview settings" \ % vgrid_name output_objects.append({ 'object_type': 'error_text', 'text': ERROR_MSG }) else: status = refresh( configuration, client_id, base_dir, abs_path, path, output_objects, ) logger.debug('refresh exit status: %s' % str(status)) else: ERROR_MSG = "action: '%s' _NOT_ implemented yet" \ % str(action) output_objects.append({ 'object_type': 'error_text', 'text': ERROR_MSG }) logger.debug('output_objects: %s' % str(output_objects)) logger.debug('status: %s' % str(status)) return (output_objects, status)
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False) client_dir = client_id_dir(client_id) defaults = signature()[1] (validate_status, accepted) = validate_input_and_cert( user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) flags = ''.join(accepted['flags']) dst = accepted['dst'][-1].lstrip(os.sep) pattern_list = accepted['src'] current_dir = accepted['current_dir'][-1].lstrip(os.sep) # All paths are relative to current_dir pattern_list = [os.path.join(current_dir, i) for i in pattern_list] dst = os.path.join(current_dir, dst) if not safe_handler(configuration, 'post', op_name, client_id, get_csrf_limit(configuration), accepted): output_objects.append( {'object_type': 'error_text', 'text': '''Only accepting CSRF-filtered POST requests to prevent unintended updates''' }) return (output_objects, returnvalues.CLIENT_ERROR) # Please note that base_dir must end in slash to avoid access to other # user dirs when own name is a prefix of another user name base_dir = os.path.abspath(os.path.join(configuration.user_home, client_dir)) + os.sep title_entry = find_entry(output_objects, 'title') title_entry['text'] = 'ZIP/TAR archiver' output_objects.append( {'object_type': 'header', 'text': 'ZIP/TAR archiver'}) if verbose(flags): for flag in flags: output_objects.append({'object_type': 'text', 'text': '%s using flag: %s' % (op_name, flag)}) if 'h' in flags: usage(output_objects) # IMPORTANT: path must be expanded to abs for proper chrooting abs_dir = os.path.abspath(os.path.join(base_dir, current_dir.lstrip(os.sep))) if not valid_user_path(configuration, abs_dir, base_dir, True): # out of bounds output_objects.append({'object_type': 'error_text', 'text': "You're not allowed to work in %s!" % current_dir}) logger.warning('%s tried to %s restricted path %s ! (%s)' % (client_id, op_name, abs_dir, current_dir)) return (output_objects, returnvalues.CLIENT_ERROR) output_objects.append( {'object_type': 'text', 'text': "working in %s" % current_dir}) # NOTE: dst already incorporates current_dir prefix here # IMPORTANT: path must be expanded to abs for proper chrooting abs_dest = os.path.abspath(os.path.join(base_dir, dst)) logger.info('pack in %s' % abs_dest) # Don't use abs_path in output as it may expose underlying # fs layout. relative_dest = abs_dest.replace(base_dir, '') if not valid_user_path(configuration, abs_dest, base_dir, True): # out of bounds output_objects.append( {'object_type': 'error_text', 'text': "Invalid path! (%s expands to an illegal path)" % dst}) logger.warning('%s tried to %s restricted path %s !(%s)' % (client_id, op_name, abs_dest, dst)) return (output_objects, returnvalues.CLIENT_ERROR) if not os.path.isdir(os.path.dirname(abs_dest)): output_objects.append({'object_type': 'error_text', 'text': "No such destination directory: %s" % os.path.dirname(relative_dest)}) return (output_objects, returnvalues.CLIENT_ERROR) if not check_write_access(abs_dest, parent_dir=True): logger.warning('%s called without write access: %s' % (op_name, abs_dest)) output_objects.append( {'object_type': 'error_text', 'text': 'cannot pack to "%s": inside a read-only location!' % relative_dest}) return (output_objects, returnvalues.CLIENT_ERROR) status = returnvalues.OK for pattern in pattern_list: # Check directory traversal attempts before actual handling to avoid # leaking information about file system layout while allowing # consistent error messages unfiltered_match = glob.glob(base_dir + pattern) match = [] for server_path in unfiltered_match: logger.debug("%s: inspecting: %s" % (op_name, server_path)) # IMPORTANT: path must be expanded to abs for proper chrooting abs_path = os.path.abspath(server_path) if not valid_user_path(configuration, abs_path, base_dir, True): # out of bounds - save user warning for later to allow # partial match: # ../*/* is technically allowed to match own files. logger.warning('%s tried to %s restricted path %s ! (%s)' % (client_id, op_name, abs_path, pattern)) continue else: match.append(abs_path) # Now actually treat list of allowed matchings and notify if no # (allowed) match if not match: output_objects.append({'object_type': 'error_text', 'text': "%s: cannot pack '%s': no valid src paths" % (op_name, pattern)}) status = returnvalues.CLIENT_ERROR continue for abs_path in match: relative_path = abs_path.replace(base_dir, '') # Generally refuse handling symlinks including root vgrid shares if os.path.islink(abs_path): output_objects.append( {'object_type': 'warning', 'text': """You're not allowed to pack entire special folders like %s shared folders!""" % configuration.site_vgrid_label}) status = returnvalues.CLIENT_ERROR continue # Additionally refuse operations on inherited subvgrid share roots elif in_vgrid_share(configuration, abs_path) == relative_path: output_objects.append( {'object_type': 'warning', 'text': """You're not allowed to pack entire %s shared folders!""" % configuration.site_vgrid_label}) status = returnvalues.CLIENT_ERROR continue elif os.path.realpath(abs_path) == os.path.realpath(base_dir): logger.error("%s: refusing pack home dir: %s" % (op_name, abs_path)) output_objects.append( {'object_type': 'warning', 'text': "You're not allowed to pack your entire home directory!" }) status = returnvalues.CLIENT_ERROR continue elif os.path.realpath(abs_dest) == os.path.realpath(abs_path): output_objects.append({'object_type': 'warning', 'text': 'overlap in source and destination %s' % relative_dest}) status = returnvalues.CLIENT_ERROR continue if verbose(flags): output_objects.append( {'object_type': 'file', 'name': relative_path}) (pack_status, msg) = pack_archive(configuration, client_id, relative_path, relative_dest) if not pack_status: output_objects.append({'object_type': 'error_text', 'text': 'Error: %s' % msg}) status = returnvalues.CLIENT_ERROR continue output_objects.append({'object_type': 'text', 'text': 'Added %s to %s' % (relative_path, relative_dest)}) output_objects.append({'object_type': 'text', 'text': 'Packed archive of %s is now available in %s' % (', '.join(pattern_list), relative_dest)}) output_objects.append({'object_type': 'link', 'text': 'Download archive', 'destination': os.path.join('..', client_dir, relative_dest)}) return (output_objects, status)
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False) client_dir = client_id_dir(client_id) defaults = signature()[1] (validate_status, accepted) = validate_input_and_cert( user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) action = accepted['action'][-1] share_id = accepted['share_id'][-1] path = accepted['path'][-1] read_access = accepted['read_access'][-1].lower() in enabled_strings write_access = accepted['write_access'][-1].lower() in enabled_strings expire = accepted['expire'][-1] # Merge and split invite to make sure 'a@b, c@d' entries are handled invite_list = ','.join(accepted['invite']).split(',') invite_list = [i for i in invite_list if i] invite_msg = accepted['msg'] title_entry = find_entry(output_objects, 'title') title_entry['text'] = 'Share Link' # jquery support for tablesorter and confirmation on delete/redo: # table initially sorted by 5, 4 reversed (active first and in growing age) table_spec = {'table_id': 'sharelinkstable', 'sort_order': '[[5,1],[4,1]]'} (add_import, add_init, add_ready) = man_base_js(configuration, [table_spec], {'width': 600}) title_entry['script']['advanced'] += add_import title_entry['script']['init'] += add_init title_entry['script']['ready'] += add_ready output_objects.append({ 'object_type': 'html_form', 'text': man_base_html(configuration) }) header_entry = {'object_type': 'header', 'text': 'Manage share links'} output_objects.append(header_entry) if not configuration.site_enable_sharelinks: output_objects.append({ 'object_type': 'text', 'text': ''' Share links are disabled on this site. Please contact the site admins %s if you think they should be enabled. ''' % configuration.admin_email }) return (output_objects, returnvalues.OK) logger.info('sharelink %s from %s' % (action, client_id)) logger.debug('sharelink from %s: %s' % (client_id, accepted)) if not action in valid_actions: output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid action "%s" (supported: %s)' % (action, ', '.join(valid_actions)) }) return (output_objects, returnvalues.CLIENT_ERROR) if action in post_actions: if not safe_handler(configuration, 'post', op_name, client_id, get_csrf_limit(configuration), accepted): output_objects.append({ 'object_type': 'error_text', 'text': '''Only accepting CSRF-filtered POST requests to prevent unintended updates''' }) return (output_objects, returnvalues.CLIENT_ERROR) (load_status, share_map) = load_share_links(configuration, client_id) if not load_status: share_map = {} form_method = 'post' csrf_limit = get_csrf_limit(configuration) target_op = 'sharelink' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) if action in get_actions: if action == "show": # Table columns to skip skip_list = ['owner', 'single_file', 'expire'] sharelinks = [] for (saved_id, share_dict) in share_map.items(): share_item = build_sharelinkitem_object( configuration, share_dict) js_name = 'delete%s' % hexlify(saved_id) helper = html_post_helper( js_name, '%s.py' % target_op, { 'share_id': saved_id, 'action': 'delete', csrf_field: csrf_token }) output_objects.append({ 'object_type': 'html_form', 'text': helper }) share_item['delsharelink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really remove %s?' % saved_id), 'class': 'removelink iconspace', 'title': 'Remove share link %s' % saved_id, 'text': '' } sharelinks.append(share_item) # Display share links and form to add new ones output_objects.append({ 'object_type': 'sectionheader', 'text': 'Share Links' }) output_objects.append({ 'object_type': 'table_pager', 'entry_name': 'share links', 'default_entries': default_pager_entries }) output_objects.append({ 'object_type': 'sharelinks', 'sharelinks': sharelinks, 'skip_list': skip_list }) output_objects.append({ 'object_type': 'html_form', 'text': '<br/>' }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Create Share Link' }) submit_button = '''<span> <input type=submit value="Create share link" /> </span>''' sharelink_html = create_share_link_form(configuration, client_id, 'html', submit_button, csrf_token) output_objects.append({ 'object_type': 'html_form', 'text': sharelink_html }) elif action == "edit": header_entry['text'] = 'Edit Share Link' share_dict = share_map.get(share_id, {}) if not share_dict: output_objects.append({ 'object_type': 'error_text', 'text': 'existing share link is required for edit' }) return (output_objects, returnvalues.CLIENT_ERROR) output_objects.append({ 'object_type': 'html_form', 'text': ''' <p> Here you can send invitations for your share link %(share_id)s to one or more comma-separated recipients. </p> ''' % share_dict }) sharelinks = [] share_item = build_sharelinkitem_object(configuration, share_dict) saved_id = share_item['share_id'] js_name = 'delete%s' % hexlify(saved_id) helper = html_post_helper(js_name, '%s.py' % target_op, { 'share_id': saved_id, 'action': 'delete', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) # Hide link to self del share_item['editsharelink'] share_item['delsharelink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really remove %s?' % saved_id), 'class': 'removelink iconspace', 'title': 'Remove share link %s' % saved_id, 'text': '' } sharelinks.append(share_item) output_objects.append({ 'object_type': 'sharelinks', 'sharelinks': sharelinks }) submit_button = '''<span> <input type=submit value="Send invitation(s)" /> </span>''' sharelink_html = invite_share_link_form(configuration, client_id, share_dict, 'html', submit_button, csrf_token) output_objects.append({ 'object_type': 'html_form', 'text': sharelink_html }) output_objects.append({ 'object_type': 'link', 'destination': 'sharelink.py', 'text': 'Return to share link overview' }) return (output_objects, returnvalues.OK) elif action in post_actions: share_dict = share_map.get(share_id, {}) if not share_dict and action != 'create': logger.warning('%s tried to %s missing or not owned link %s!' % (client_id, action, share_id)) output_objects.append({ 'object_type': 'error_text', 'text': '%s requires existing share link' % action }) return (output_objects, returnvalues.CLIENT_ERROR) share_path = share_dict.get('path', path) # Please note that base_dir must end in slash to avoid access to other # user dirs when own name is a prefix of another user name base_dir = os.path.abspath( os.path.join(configuration.user_home, client_dir)) + os.sep rel_share_path = share_path.lstrip(os.sep) # IMPORTANT: path must be expanded to abs for proper chrooting abs_path = os.path.abspath(os.path.join(base_dir, rel_share_path)) relative_path = abs_path.replace(base_dir, '') real_path = os.path.realpath(abs_path) single_file = os.path.isfile(real_path) vgrid_name = in_vgrid_share(configuration, abs_path) if action == 'delete': header_entry['text'] = 'Delete Share Link' (save_status, _) = delete_share_link(share_id, client_id, configuration, share_map) if save_status and vgrid_name: logger.debug("del vgrid sharelink pointer %s" % share_id) (del_status, del_msg) = vgrid_remove_sharelinks(configuration, vgrid_name, [share_id], 'share_id') if not del_status: logger.error("del vgrid sharelink pointer %s failed: %s" % (share_id, del_msg)) return (False, share_map) desc = "delete" elif action == "update": header_entry['text'] = 'Update Share Link' # Try to point replies to client_id email client_email = extract_field(client_id, 'email') if invite_list: invites = share_dict.get('invites', []) + invite_list invites_uniq = list(set([i for i in invites if i])) invites_uniq.sort() share_dict['invites'] = invites_uniq auto_msg = invite_share_link_message(configuration, client_id, share_dict, 'html') msg = '\n'.join(invite_msg) # Now send request to all targets in turn threads = [] for target in invite_list: job_dict = { 'NOTIFY': [target.strip()], 'JOB_ID': 'NOJOBID', 'USER_CERT': client_id, 'EMAIL_SENDER': client_email } logger.debug('invite %s to %s' % (target, share_id)) threads.append( notify_user_thread( job_dict, [auto_msg, msg], 'INVITESHARE', logger, '', configuration, )) # Try finishing delivery but do not block forever on one message notify_done = [False for _ in threads] for _ in range(3): for i in range(len(invite_list)): if not notify_done[i]: logger.debug('check done %s' % invite_list[i]) notify = threads[i] notify.join(3) notify_done[i] = not notify.isAlive() notify_sent, notify_failed = [], [] for i in range(len(invite_list)): if notify_done[i]: notify_sent.append(invite_list[i]) else: notify_failed.append(invite_list[i]) logger.debug('notify sent %s, failed %s' % (notify_sent, notify_failed)) if notify_failed: output_objects.append({ 'object_type': 'html_form', 'text': ''' <p>Failed to send invitation to %s</p>''' % ', '.join(notify_failed) }) if notify_sent: output_objects.append({ 'object_type': 'html_form', 'text': '''<p>Invitation sent to %s</p> <textarea class="fillwidth padspace" rows="%d" readonly="readonly"> %s %s </textarea> ''' % (', '.join(notify_sent), (auto_msg + msg).count('\n') + 3, auto_msg, msg) }) if expire: share_dict['expire'] = expire (save_status, _) = update_share_link(share_dict, client_id, configuration, share_map) desc = "update" elif action == "create": header_entry['text'] = 'Create Share Link' if not read_access and not write_access: output_objects.append({ 'object_type': 'error_text', 'text': 'No access set - please select read, write or both' }) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: check path here as relative_path is empty for path='/' if not path: output_objects.append({ 'object_type': 'error_text', 'text': 'No path provided!' }) return (output_objects, returnvalues.CLIENT_ERROR) # We refuse sharing of entire home for security reasons elif not valid_user_path( configuration, abs_path, base_dir, allow_equal=False): logger.warning('%s tried to %s restricted path %s ! (%s)' % (client_id, action, abs_path, path)) output_objects.append({ 'object_type': 'error_text', 'text': '''Illegal path "%s": you can only share your own data, and not your entire home direcory.''' % path }) return (output_objects, returnvalues.CLIENT_ERROR) elif not os.path.exists(abs_path): output_objects.append({ 'object_type': 'error_text', 'text': 'Provided path "%s" does not exist!' % path }) return (output_objects, returnvalues.CLIENT_ERROR) # Refuse sharing of (mainly auth) dot dirs in root of user home elif real_path.startswith(os.path.join(base_dir, '.')): output_objects.append({ 'object_type': 'error_text', 'text': 'Provided path "%s" cannot be shared for security reasons' % path }) return (output_objects, returnvalues.CLIENT_ERROR) elif single_file and write_access: output_objects.append({ 'object_type': 'error_text', 'text': '''Individual files cannot be shared with write access - please share a directory with the file in it or only share with read access. ''' }) return (output_objects, returnvalues.CLIENT_ERROR) # We check if abs_path is in vgrid share, but do not worry about # private_base or public_base since they are only available to # owners, who can always share anyway. if vgrid_name is not None and \ not vgrid_is_owner(vgrid_name, client_id, configuration): # share is inside vgrid share so we must check that user is # permitted to create sharelinks there. (load_status, settings_dict) = vgrid_settings(vgrid_name, configuration, recursive=True, as_dict=True) if not load_status: # Probably owners just never saved settings, use defaults settings_dict = {'vgrid_name': vgrid_name} allowed = settings_dict.get('create_sharelink', keyword_owners) if allowed != keyword_members: output_objects.append({ 'object_type': 'error_text', 'text': '''The settings for the %(vgrid_name)s %(vgrid_label)s do not permit you to re-share %(vgrid_label)s shared folders. Please contact the %(vgrid_name)s owners if you think you should be allowed to do that. ''' % { 'vgrid_name': vgrid_name, 'vgrid_label': configuration.site_vgrid_label } }) return (output_objects, returnvalues.CLIENT_ERROR) access_list = [] if read_access: access_list.append('read') if write_access: access_list.append('write') share_mode = '-'.join((access_list + ['only'])[:2]) # TODO: more validity checks here if share_dict: desc = "update" else: desc = "create" # IMPORTANT: always use expanded path share_dict.update({ 'path': relative_path, 'access': access_list, 'expire': expire, 'invites': invite_list, 'single_file': single_file }) attempts = 1 generate_share_id = False if not share_id: attempts = 3 generate_share_id = True for i in range(attempts): if generate_share_id: share_id = generate_sharelink_id(configuration, share_mode) share_dict['share_id'] = share_id (save_status, save_msg) = create_share_link(share_dict, client_id, configuration, share_map) if save_status: logger.info('created sharelink: %s' % share_dict) break else: # ID Collision? logger.warning('could not create sharelink: %s' % save_msg) if save_status and vgrid_name: logger.debug("add vgrid sharelink pointer %s" % share_id) (add_status, add_msg) = vgrid_add_sharelinks(configuration, vgrid_name, [share_dict]) if not add_status: logger.error( "save vgrid sharelink pointer %s failed: %s " % (share_id, add_msg)) return (False, share_map) else: output_objects.append({ 'object_type': 'error_text', 'text': 'No such action %s' % action }) return (output_objects, returnvalues.CLIENT_ERROR) if not save_status: output_objects.append({ 'object_type': 'error_text', 'text': 'Error in %s share link %s: ' % (desc, share_id) + 'save updated share links failed!' }) return (output_objects, returnvalues.CLIENT_ERROR) output_objects.append({ 'object_type': 'text', 'text': '%sd share link %s on %s .' % (desc.title(), share_id, relative_path) }) if action in ['create', 'update']: sharelinks = [] share_item = build_sharelinkitem_object(configuration, share_dict) saved_id = share_item['share_id'] js_name = 'delete%s' % hexlify(saved_id) helper = html_post_helper(js_name, '%s.py' % target_op, { 'share_id': saved_id, 'action': 'delete', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) share_item['delsharelink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really remove %s?' % saved_id), 'class': 'removelink iconspace', 'title': 'Remove share link %s' % saved_id, 'text': '' } sharelinks.append(share_item) output_objects.append({ 'object_type': 'sharelinks', 'sharelinks': sharelinks }) if action == 'create': # NOTE: Leave editsharelink here for use in fileman overlay #del share_item['editsharelink'] output_objects.append({ 'object_type': 'html_form', 'text': '<br />' }) submit_button = '''<span> <input type=submit value="Send invitation(s)" /> </span>''' invite_html = invite_share_link_form(configuration, client_id, share_dict, 'html', submit_button, csrf_token) output_objects.append({ 'object_type': 'html_form', 'text': invite_html }) else: output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid share link action: %s' % action }) return (output_objects, returnvalues.CLIENT_ERROR) output_objects.append({'object_type': 'html_form', 'text': '<br />'}) output_objects.append({ 'object_type': 'link', 'destination': 'sharelink.py', 'text': 'Return to share link overview' }) return (output_objects, returnvalues.OK)
def main(client_id, user_arguments_dict, environ=None): """Main function used by front end""" if environ is None: environ = os.environ (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id) client_dir = client_id_dir(client_id) status = returnvalues.OK defaults = signature()[1] (validate_status, accepted) = validate_input_and_cert( user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) flags = ''.join(accepted['flags']) src_list = accepted['src'] dst = accepted['dst'][-1] if not safe_handler(configuration, 'post', op_name, client_id, get_csrf_limit(configuration), accepted): output_objects.append({ 'object_type': 'error_text', 'text': '''Only accepting CSRF-filtered POST requests to prevent unintended updates''' }) return (output_objects, returnvalues.CLIENT_ERROR) # Please note that base_dir must end in slash to avoid access to other # user dirs when own name is a prefix of another user name base_dir = os.path.abspath( os.path.join(configuration.user_home, client_dir)) + os.sep status = returnvalues.OK abs_dest = base_dir + dst dst_list = glob.glob(abs_dest) if not dst_list: # New destination? if not glob.glob(os.path.dirname(abs_dest)): output_objects.append({ 'object_type': 'error_text', 'text': 'Illegal dst path provided!' }) return (output_objects, returnvalues.CLIENT_ERROR) else: dst_list = [abs_dest] # Use last match in case of multiple matches dest = dst_list[-1] if len(dst_list) > 1: output_objects.append({ 'object_type': 'warning', 'text': 'dst (%s) matches multiple targets - using last: %s' % (dst, dest) }) # IMPORTANT: path must be expanded to abs for proper chrooting abs_dest = os.path.abspath(dest) # Don't use abs_path in output as it may expose underlying # fs layout. relative_dest = abs_dest.replace(base_dir, '') if not valid_user_path(configuration, abs_dest, base_dir, True): logger.warning('%s tried to %s to restricted path %s ! (%s)' % (client_id, op_name, abs_dest, dst)) output_objects.append({ 'object_type': 'error_text', 'text': "Invalid path! (%s expands to an illegal path)" % dst }) return (output_objects, returnvalues.CLIENT_ERROR) if not check_write_access(abs_dest, parent_dir=True): logger.warning('%s called without write access: %s' % (op_name, abs_dest)) output_objects.append({ 'object_type': 'error_text', 'text': 'cannot move to "%s": inside a read-only location!' % relative_dest }) return (output_objects, returnvalues.CLIENT_ERROR) for pattern in src_list: unfiltered_match = glob.glob(base_dir + pattern) match = [] for server_path in unfiltered_match: # IMPORTANT: path must be expanded to abs for proper chrooting abs_path = os.path.abspath(server_path) if not valid_user_path(configuration, abs_path, base_dir, True): logger.warning('%s tried to %s restricted path %s ! (%s)' % (client_id, op_name, abs_path, pattern)) continue match.append(abs_path) # Now actually treat list of allowed matchings and notify if no # (allowed) match if not match: output_objects.append({ 'object_type': 'error_text', 'text': '%s: no such file or directory! %s' % (op_name, pattern) }) status = returnvalues.CLIENT_ERROR for abs_path in match: relative_path = abs_path.replace(base_dir, '') if verbose(flags): output_objects.append({ 'object_type': 'file', 'name': relative_path }) # Generally refuse handling symlinks including root vgrid shares if os.path.islink(abs_path): output_objects.append({ 'object_type': 'warning', 'text': """You're not allowed to move entire special folders like %s shared folders!""" % configuration.site_vgrid_label }) status = returnvalues.CLIENT_ERROR continue # Additionally refuse operations on inherited subvgrid share roots elif in_vgrid_share(configuration, abs_path) == relative_path: output_objects.append({ 'object_type': 'warning', 'text': """You're not allowed to move entire %s shared folders!""" % configuration.site_vgrid_label }) status = returnvalues.CLIENT_ERROR continue elif os.path.realpath(abs_path) == os.path.realpath(base_dir): logger.error("%s: refusing move home dir: %s" % (op_name, abs_path)) output_objects.append({ 'object_type': 'warning', 'text': "You're not allowed to move your entire home directory!" }) status = returnvalues.CLIENT_ERROR continue if not check_write_access(abs_path): logger.warning('%s called without write access: %s' % (op_name, abs_path)) output_objects.append({ 'object_type': 'error_text', 'text': 'cannot move "%s": inside a read-only location!' % pattern }) status = returnvalues.CLIENT_ERROR continue # If destination is a directory the src should be moved in there # Move with existing directory as target replaces the directory! abs_target = abs_dest if os.path.isdir(abs_target): if os.path.samefile(abs_target, abs_path): output_objects.append({ 'object_type': 'warning', 'text': "Cannot move '%s' to a subdirectory of itself!" % relative_path }) status = returnvalues.CLIENT_ERROR continue abs_target = os.path.join(abs_target, os.path.basename(abs_path)) try: gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'], 'moved', [relative_path, relative_dest]) shutil.move(abs_path, abs_target) logger.info('%s %s %s done' % (op_name, abs_path, abs_target)) except Exception, exc: if not isinstance(exc, GDPIOLogError): gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'], 'moved', [relative_path, relative_dest], failed=True, details=exc) output_objects.append({ 'object_type': 'error_text', 'text': "%s: '%s': %s" % (op_name, relative_path, exc) }) logger.error("%s: failed on '%s': %s" % (op_name, relative_path, exc)) status = returnvalues.SYSTEM_ERROR continue
def handle_dir( configuration, listing, dirname, dirname_with_dir, actual_dir, flags='', ): """handle a dir""" # Recursion can get here when called without explicit invisible files if invisible_path(dirname_with_dir): return special = '' extra_class = '' real_dir = os.path.realpath(actual_dir) abs_dir = os.path.abspath(actual_dir) # If we followed a symlink it is not a plain dir if real_dir != abs_dir: access_type = configuration.site_vgrid_label parent_dir = os.path.basename(os.path.dirname(actual_dir)) # configuration.logger.debug("checking link %s type (%s)" % \ # (dirname_with_dir, real_dir)) # Separate vgrid special dirs from plain ones if in_vgrid_share(configuration, actual_dir) == dirname_with_dir: dir_type = 'shared files' extra_class = 'vgridshared' elif in_vgrid_readonly(configuration, actual_dir) == dirname_with_dir: access_type = 'read-only' dir_type = 'shared files' extra_class = 'vgridshared readonly' elif in_vgrid_pub_web(configuration, actual_dir) == \ dirname_with_dir[len('public_base/'):]: dir_type = 'public web page' extra_class = 'vgridpublicweb' elif in_vgrid_priv_web(configuration, actual_dir) == \ dirname_with_dir[len('private_base/'):]: dir_type = 'private web page' extra_class = 'vgridprivateweb' # NOTE: in_vgrid_X returns None on miss, so don't use replace directly elif (in_vgrid_store_res(configuration, actual_dir) or '').replace( os.sep, '_') == os.path.basename(dirname_with_dir): dir_type = 'storage resource files' if os.path.islink(actual_dir): extra_class = 'vgridstoreres' elif real_dir.startswith(configuration.seafile_mount): access_type = 'read-only' dir_type = 'Seafile library access' if os.path.islink(actual_dir): extra_class = 'seafilereadonly' elif real_dir.find(trash_destdir) != -1: access_type = 'recently deleted data' dir_type = 'sub' else: dir_type = 'sub' # TODO: improve this greedy matching here? # configuration.logger.debug("check real_dir %s vs %s" % (real_dir, # trash_destdir)) if real_dir.endswith(trash_destdir): dir_type = '' extra_class = 'trashbin' special = ' - %s %s directory' % (access_type, dir_type) dir_obj = { 'object_type': 'direntry', 'type': 'directory', 'name': dirname, 'rel_path': dirname_with_dir, 'rel_path_enc': quote(dirname_with_dir), 'rel_dir_enc': quote(dirname_with_dir), # NOTE: dirname_with_dir is kept for backwards compliance 'dirname_with_dir': dirname_with_dir, 'flags': flags, 'special': special, 'extra_class': extra_class, } if long_list(flags): dir_obj['actual_dir'] = long_format(actual_dir) if file_info(flags): dir_obj['file_info'] = fileinfo_stat(actual_dir) listing.append(dir_obj)