def refresh_share_creds(configuration, protocol, username, share_modes=['read-write']): """Reload sharelink credentials for username (SHARE_ID) if they changed on disk. That is, add user entries in configuration.daemon_conf['shares'] for any corresponding active sharelinks. Removes all sharelink login entries if the sharelink is no longer active, too. The protocol argument specifies which auth files to use. Returns a tuple with the updated daemon_conf and the list of changed share IDs. NOTE: we limit share_modes to read-write sharelinks for now since we don't have guards in place to support read-only or write-only mode in daemons. NOTE: we further limit to directory sharelinks for chroot'ing. """ # Must end in sep base_dir = configuration.user_home.rstrip(os.sep) + os.sep changed_shares = [] conf = configuration.daemon_conf last_update = conf['time_stamp'] logger = conf.get("logger", logging.getLogger()) creds_lock = conf.get('creds_lock', None) if not protocol in ('sftp', 'davs', 'ftps', ): logger.error("invalid protocol: %s" % protocol) return (conf, changed_shares) if [kind for kind in share_modes if kind != 'read-write']: logger.error("invalid share_modes: %s" % share_modes) return (conf, changed_shares) if not possible_sharelink_id(configuration, username): # logger.debug("ruled out %s as a possible sharelink ID" % username) return (conf, changed_shares) try: (mode, _) = extract_mode_id(configuration, username) except ValueError, err: logger.error('refresh share creds called with invalid username %s: %s' % (username, err)) mode = 'INVALID-SHARELINK'
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, op_header=False) logger.info('Extracting input in %s' % op_name) status = returnvalues.OK defaults = signature()[1] logger.info('Extracted input in %s: %s' % (op_name, user_arguments_dict.keys())) # All non-file fields must be validated validate_args = dict([(key, user_arguments_dict.get(key, val)) for (key, val) in user_arguments_dict.items() if not key in manual_validation]) # IMPORTANT: we must explicitly inlude CSRF token validate_args[csrf_field] = user_arguments_dict.get(csrf_field, ['']) (validate_status, accepted) = validate_input( validate_args, defaults, output_objects, allow_rejects=False, ) if not validate_status: logger.error('%s validation failed: %s (%s)' % (op_name, validate_status, accepted)) return (accepted, returnvalues.CLIENT_ERROR) logger.info('validated input in %s: %s' % (op_name, validate_args.keys())) action = accepted['action'][-1] current_dir = os.path.normpath(accepted['current_dir'][-1].lstrip(os.sep)) flags = ''.join(accepted['flags']) share_id = accepted['share_id'][-1] output_format = accepted['output_format'][-1] if action != "status": 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) reject_write = False uploaded = [] header_item = {'object_type': 'header', 'text': ''} # Always include a files reply even if empty output_objects.append(header_item) output_objects.append({'object_type': 'uploadfiles', 'files': uploaded}) # Either authenticated user client_id set or sharelink ID if client_id: user_id = client_id target_dir = client_id_dir(client_id) base_dir = configuration.user_home redirect_name = configuration.site_user_redirect redirect_path = redirect_name id_args = '' page_title = 'Upload to User Directory: %s' % action userstyle = True widgets = True elif share_id: try: (share_mode, _) = extract_mode_id(configuration, share_id) except ValueError, err: logger.error('%s called with invalid share_id %s: %s' % (op_name, share_id, err)) output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid sharelink ID: %s' % share_id }) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: load and check sharelink pickle (currently requires client_id) user_id = 'anonymous user through share ID %s' % share_id # NOTE: we must return uploaded reply so we delay read-only failure if share_mode == 'read-only': logger.error('%s called without write access: %s' % (op_name, accepted)) reject_write = True target_dir = os.path.join(share_mode, share_id) base_dir = configuration.sharelink_home redirect_name = 'share_redirect' redirect_path = os.path.join(redirect_name, share_id) id_args = 'share_id=%s;' % share_id page_title = 'Upload to Shared Directory: %s' % action userstyle = False widgets = False
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, op_menu=client_id) client_dir = client_id_dir(client_id) status = returnvalues.OK defaults = signature()[1] (validate_status, accepted) = validate_input( user_arguments_dict, defaults, output_objects, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) flags = ''.join(accepted['flags']) patterns = accepted['path'] current_dir = accepted['current_dir'] share_id = accepted['share_id'][-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) # Either authenticated user client_id set or sharelink ID if client_id: user_id = client_id target_dir = client_id_dir(client_id) base_dir = configuration.user_home id_query = '' page_title = 'Remove User Directory' userstyle = True widgets = True elif share_id: try: (share_mode, _) = extract_mode_id(configuration, share_id) except ValueError, err: logger.error('%s called with invalid share_id %s: %s' % \ (op_name, share_id, err)) output_objects.append({'object_type': 'error_text', 'text' : 'Invalid sharelink ID: %s' % share_id}) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: load and check sharelink pickle (currently requires client_id) user_id = 'anonymous user through share ID %s' % share_id if share_mode == 'read-only': logger.error('%s called without write access: %s' % \ (op_name, accepted)) output_objects.append({'object_type': 'error_text', 'text' : 'No write access!'}) return (output_objects, returnvalues.CLIENT_ERROR) target_dir = os.path.join(share_mode, share_id) base_dir = configuration.sharelink_home id_query = '?share_id=%s' % share_id page_title = 'Remove Shared Directory' userstyle = False widgets = False
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) 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] iosessionid = accepted['iosessionid'][-1] share_id = accepted['share_id'][-1] freeze_id = accepted['freeze_id'][-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 # Special handling if used from a job (no client_id but iosessionid) if not client_id and iosessionid: base_dir = os.path.realpath(configuration.webserver_home + os.sep + iosessionid) + os.sep # Use selected base as source and destination dir by default src_base = dst_base = base_dir # Sharelink import if share_id is given - change to sharelink as src base if share_id: try: (share_mode, _) = extract_mode_id(configuration, share_id) except ValueError, err: logger.error('%s called with invalid share_id %s: %s' % (op_name, share_id, err)) output_objects.append( {'object_type': 'error_text', 'text': 'Invalid sharelink ID: %s' % share_id}) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: load and check sharelink pickle (currently requires client_id) if share_mode == 'write-only': logger.error('%s called import from write-only sharelink: %s' % (op_name, accepted)) output_objects.append( {'object_type': 'error_text', 'text': 'Sharelink %s is write-only!' % share_id}) return (output_objects, returnvalues.CLIENT_ERROR) target_dir = os.path.join(share_mode, share_id) src_base = os.path.abspath(os.path.join(configuration.sharelink_home, target_dir)) + os.sep if os.path.isfile(os.path.realpath(src_base)): logger.error('%s called import on single file sharelink: %s' % (op_name, share_id)) output_objects.append( {'object_type': 'error_text', 'text': """Import is only supported for directory sharelinks!"""}) return (output_objects, returnvalues.CLIENT_ERROR) elif not os.path.isdir(src_base): logger.error('%s called import with non-existant sharelink: %s' % (client_id, share_id)) output_objects.append( {'object_type': 'error_text', 'text': 'No such sharelink: %s' % share_id}) return (output_objects, returnvalues.CLIENT_ERROR)
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, op_menu=client_id) defaults = signature()[1] (validate_status, accepted) = validate_input( user_arguments_dict, defaults, output_objects, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) flags = ''.join(accepted['flags']) pattern_list = accepted['path'] current_dir = accepted['current_dir'][-1].lstrip('/') share_id = accepted['share_id'][-1] show_dest = accepted['with_dest'][0].lower() == 'true' status = returnvalues.OK # NOTE: in contrast to 'ls' we never include write operations here read_mode, write_mode = True, False visibility_mods = ''' .%(main_class)s .enable_write { display: none; } .%(main_class)s .disable_read { display: none; } .%(main_class)s .if_full { display: none; } ''' # Either authenticated user client_id set or sharelink ID if client_id: user_id = client_id target_dir = client_id_dir(client_id) base_dir = configuration.user_home redirect_name = configuration.site_user_redirect redirect_path = redirect_name id_args = '' root_link_name = 'USER HOME' main_class = "user_expand" page_title = 'User Files - Path Expansion' userstyle = True widgets = True elif share_id: try: (share_mode, _) = extract_mode_id(configuration, share_id) except ValueError, err: logger.error('%s called with invalid share_id %s: %s' % (op_name, share_id, err)) output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid sharelink ID: %s' % share_id }) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: load and check sharelink pickle (currently requires client_id) # then include shared by %(owner)s on page header user_id = 'anonymous user through share ID %s' % share_id target_dir = os.path.join(share_mode, share_id) base_dir = configuration.sharelink_home redirect_name = 'share_redirect' redirect_path = os.path.join(redirect_name, share_id) id_args = 'share_id=%s;' % share_id root_link_name = '%s' % share_id main_class = "sharelink_expand" page_title = 'Shared Files - Path Expansion' userstyle = False widgets = False
logger.debug("found sid dir: %s" % sid_name) # Save full prefix of link path full_prefix = os.path.join(root, sid_name) # Make sure absolute/normalized but unexpanded path is inside base. # Only prevents path itself outside base - not illegal linking # outside base, which is checked later. path = os.path.abspath(path) if not path.startswith(full_prefix): logger.error("got path from %s outside sid base: %s" % (client_ip, path)) print INVALID_MARKER continue if is_sharelink: # Share links use Alias to map directly into sharelink_home # and with first char mapping into access mode sub-dir there. (access_dir, _) = extract_mode_id(configuration, sid_name) real_root = os.path.join(configuration.sharelink_home, access_dir) + os.sep else: # Session links are directly in webserver_home and they map # either into mig_system_files for empty jobs or into specific # user_home for real job input/output. real_root = configuration.webserver_home.rstrip(os.sep) + \ os.sep # NOTE: we cannot completely trust linked path to be safe, # so we first check full prefix on normalized path above to avoid # user escaping link base with e.g. SHAREID/../bla . Next we # carefully expand only the SID link part and update base safely. # We expand with readlink to only follow initial link and extract
def refresh_shares(configuration, protocol, share_modes=['read-write']): """Refresh shares keys based on the sharelink state. Add user entries for all active sharelinks. Removes all the user entries for sharelinks no longer active. Returns a tuple with the daemon_conf and the list of changed sharelink IDs. NOTE: we limit share_modes to read-write sharelinks for now since we don't have guards in place to support read-only or write-only mode in daemons. NOTE: we further limit to directory sharelinks for chroot'ing. NOTE: Deprecated due to severe system load, use refresh_share_creds instead """ # Must end in sep base_dir = configuration.user_home.rstrip(os.sep) + os.sep changed_shares = [] conf = configuration.daemon_conf logger = conf.get("logger", logging.getLogger()) creds_lock = conf.get('creds_lock', None) if creds_lock: creds_lock.acquire() old_usernames = [i.username for i in conf['shares']] if creds_lock: creds_lock.release() cur_usernames = [] if not protocol in ('sftp', 'davs', 'ftps', ): logger.error("invalid protocol: %s" % protocol) return (conf, changed_shares) if [kind for kind in share_modes if kind != 'read-write']: logger.error("invalid share_modes: %s" % share_modes) return (conf, changed_shares) for link_name in os.listdir(configuration.sharelink_home): link_path = os.path.join(configuration.sharelink_home, link_name) try: link_dest = os.readlink(link_path) if not link_dest.startswith(base_dir): raise ValueError("Invalid base for share %s" % link_name) except Exception, err: logger.error("invalid share %s: %s" % (link_name, err)) continue (mode, _) = extract_mode_id(configuration, link_name) if not mode in share_modes: logger.info("invalid share mode %s for %s" % (mode, link_name)) continue share_dict = None if os.path.islink(link_path) and os.path.isdir(link_dest): share_id = link_name # NOTE: share link points inside user home of owner so extract here share_root = link_dest.replace(base_dir, '').lstrip(os.sep) # NOTE: just use share_id as password/digest for now share_pw_hash = generate_password_hash(configuration, share_id) share_pw_digest = generate_password_digest( configuration, dav_domain, share_id, share_id, configuration.site_digest_salt) # TODO: load pickle from user_settings of owner (from link_dest)? share_dict = {'share_id': share_id, 'share_root': share_root, 'share_pw_hash': share_pw_hash, 'share_pw_digest': share_pw_digest} # We only allow access to active shares if share_dict is not None and isinstance(share_dict, dict) and \ share_dict.has_key('share_id') and \ share_dict.has_key('share_root') and \ share_dict.has_key('share_pw_hash') and \ share_dict.has_key('share_pw_digest'): user_alias = share_id user_dir = share_dict['share_root'] user_password = share_dict['share_pw_hash'] user_digest = share_dict['share_pw_digest'] logger.info("Adding login for share %s" % user_alias) add_share_object(configuration, user_alias, user_dir, password=user_password, digest=user_digest) cur_usernames.append(user_alias) changed_shares.append(user_alias)