def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id) output_objects.append({'object_type': 'text', 'text' : '--------- Trying to Clean front end ----------' }) 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) unique_resource_name = accepted['unique_resource_name'][-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) if not is_owner(client_id, unique_resource_name, configuration.resource_home, logger): output_objects.append({'object_type': 'error_text', 'text' : 'Failure: You must be an owner of ' + unique_resource_name + ' to clean the front end!'}) return (output_objects, returnvalues.CLIENT_ERROR) exit_status = returnvalues.OK (status, msg) = stop_resource_frontend(unique_resource_name, configuration, logger) if not status: output_objects.append({'object_type': 'error_text', 'text' : 'Problems stopping front end during clean: %s' % msg}) return (output_objects, returnvalues.CLIENT_ERROR) (status2, msg2) = clean_resource_frontend(unique_resource_name, configuration.resource_home, logger) if not status2: output_objects.append({'object_type': 'error_text', 'text' : 'Problems cleaning front end during clean: %s' % msg2}) exit_status = returnvalues.SYSTEM_ERROR if status and status2: output_objects.append({'object_type': 'text', 'text' : 'Clean front end success: Stop output: %s Clean output %s' % (msg, msg2)}) return (output_objects, exit_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) if not configuration.site_enable_jobs: output_objects.append({ 'object_type': 'error_text', 'text': '''Job execution is not enabled on this system''' }) return (output_objects, returnvalues.SYSTEM_ERROR) status = returnvalues.OK title_entry = find_entry(output_objects, 'title') title_entry['text'] = 'Job Manager' user_settings = title_entry.get('user_settings', {}) title_entry['style'] = css_tmpl(configuration, user_settings) csrf_map = {} method = 'post' limit = get_csrf_limit(configuration) for target_op in csrf_backends: csrf_map[target_op] = make_csrf_token(configuration, method, target_op, client_id, limit) (add_import, add_init, add_ready) = js_tmpl_parts(csrf_map) title_entry['script']['advanced'] += add_import title_entry['script']['init'] += add_init title_entry['script']['ready'] += add_ready output_objects.append({'object_type': 'header', 'text': 'Job Manager'}) output_objects.append({ 'object_type': 'table_pager', 'entry_name': 'jobs', 'default_entries': default_pager_entries, 'form_append': pager_append() }) output_objects.append({'object_type': 'html_form', 'text': html_post()}) 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) 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, # NOTE: path can use wildcards, dst and current_dir cannot typecheck_overrides={'path': valid_path_pattern}, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) flags = ''.join(accepted['flags']) algo_list = accepted['hash_algo'] max_chunks = int(accepted['max_chunks'][-1]) pattern_list = accepted['path'] dst = accepted['dst'][-1] 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] if dst: dst = os.path.join(current_dir, dst) # 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 if verbose(flags): for flag in flags: output_objects.append({ 'object_type': 'text', 'text': '%s using flag: %s' % (op_name, flag) }) # 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): 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) if verbose(flags): output_objects.append({ 'object_type': 'text', 'text': "working in %s" % current_dir }) if 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) # 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('chksum 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): 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 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 checksum to "%s": inside a read-only location!' % relative_dest }) return (output_objects, returnvalues.CLIENT_ERROR) all_lines = [] 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: # 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 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': 'file_not_found', 'name': pattern }) status = returnvalues.FILE_NOT_FOUND for abs_path in match: relative_path = abs_path.replace(base_dir, '') output_lines = [] for hash_algo in algo_list: try: chksum_helper = _algo_map.get(hash_algo, _algo_map["md5"]) checksum = chksum_helper(abs_path, max_chunks=max_chunks) line = "%s %s\n" % (checksum, relative_path) logger.info("%s %s of %s: %s" % (op_name, hash_algo, abs_path, checksum)) output_lines.append(line) except Exception as 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 entry = {'object_type': 'file_output', 'lines': output_lines} output_objects.append(entry) all_lines += output_lines if dst and not write_file(''.join(all_lines), abs_dest, logger): output_objects.append({ 'object_type': 'error_text', 'text': "failed to write checksums to %s" % relative_dest }) logger.error("writing checksums to %s for %s failed" % (abs_dest, client_id)) status = returnvalues.SYSTEM_ERROR 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) defaults = signature()[1] title_entry = find_entry(output_objects, 'title') label = "%s" % configuration.site_vgrid_label title_entry['text'] = "Remove %s Trigger" % label output_objects.append({'object_type': 'header', 'text' : 'Remove %s Trigger' % label}) (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) vgrid_name = accepted['vgrid_name'][-1] rule_id = accepted['rule_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) logger.info("rmvgridtrigger %s %s" % (vgrid_name, rule_id)) # Validity of user and vgrid names is checked in this init function so # no need to worry about illegal directory traversal through variables (ret_val, msg, ret_variables) = \ init_vgrid_script_add_rem(vgrid_name, client_id, rule_id, 'trigger', configuration) if not ret_val: output_objects.append({'object_type': 'error_text', 'text': msg}) return (output_objects, returnvalues.CLIENT_ERROR) elif msg: # In case of warnings, msg is non-empty while ret_val remains True output_objects.append({'object_type': 'warning', 'text': msg}) # if we get here user is either vgrid owner or has rule ownership # can't remove if not a participant if not vgrid_is_trigger(vgrid_name, rule_id, configuration, recursive=False): output_objects.append({'object_type': 'error_text', 'text': '%s is not a trigger in %s %s.' % \ (rule_id, vgrid_name, label)}) return (output_objects, returnvalues.CLIENT_ERROR) # remove (rm_status, rm_msg) = vgrid_remove_triggers(configuration, vgrid_name, [rule_id]) if not rm_status: logger.error('%s failed to remove trigger: %s' % (client_id, rm_msg)) output_objects.append({'object_type': 'error_text', 'text': rm_msg}) output_objects.append({'object_type': 'error_text', 'text': '''%(rule_id)s might be listed as a trigger of this %(vgrid_label)s because it is a trigger of a parent %(vgrid_label)s. Removal must be performed from the most significant %(vgrid_label)s possible.''' % {'rule_id': rule_id, 'vgrid_label': label}}) return (output_objects, returnvalues.SYSTEM_ERROR) logger.info('%s removed trigger: %s' % (client_id, rule_id)) output_objects.append({'object_type': 'text', 'text': 'Trigger %s successfully removed from %s %s!' % (rule_id, vgrid_name, label)}) output_objects.append({'object_type': 'link', 'destination': 'vgridworkflows.py?vgrid_name=%s' % vgrid_name, 'text': 'Back to workflows for %s' % vgrid_name}) return (output_objects, returnvalues.OK)
def main(client_id, user_arguments_dict): """Main function used by front end""" (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) patterns = accepted['job_id'] action = accepted['action'][-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) if not configuration.site_enable_jobs: output_objects.append({ 'object_type': 'error_text', 'text': '''Job execution is not enabled on this system''' }) return (output_objects, returnvalues.SYSTEM_ERROR) if not action in valid_actions.keys(): output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid job action "%s" (only %s supported)' % (action, ', '.join(valid_actions.keys())) }) return (output_objects, returnvalues.CLIENT_ERROR) new_state = valid_actions[action] # 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.mrsl_files_dir, client_dir)) + os.sep status = returnvalues.OK filelist = [] for pattern in patterns: pattern = pattern.strip() # Backward compatibility - all_jobs keyword should match all jobs if pattern == all_jobs: pattern = '*' # 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 + '.mRSL') 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): # out of bounds - save user warning for later to allow # partial match: # ../*/* is technically allowed to match own files. logger.error( '%s tried to use %s %s outside own home! (pattern %s)' % (client_id, op_name, abs_path, pattern)) continue # Insert valid job files in filelist for later treatment 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: You do not have any matching job IDs!' % pattern }) status = returnvalues.CLIENT_ERROR else: filelist += match # job state change is hard on the server, limit if len(filelist) > 500: output_objects.append({ 'object_type': 'error_text', 'text': 'Too many matching jobs (%s)!' % len(filelist) }) return (output_objects, returnvalues.CLIENT_ERROR) changedstatusjobs = [] for filepath in filelist: # Extract job_id from filepath (replace doesn't modify filepath) mrsl_file = filepath.replace(base_dir, '') job_id = mrsl_file.replace('.mRSL', '') changedstatusjob = { 'object_type': 'changedstatusjob', 'job_id': job_id } job_dict = unpickle(filepath, logger) if not job_dict: changedstatusjob['message'] = '''The file containing the information for job id %s could not be opened! You can only %s your own jobs!''' % (job_id, action) changedstatusjobs.append(changedstatusjob) status = returnvalues.CLIENT_ERROR continue changedstatusjob['oldstatus'] = job_dict['STATUS'] # Is the job status compatible with action? possible_cancel_states = [ 'PARSE', 'QUEUED', 'RETRY', 'EXECUTING', 'FROZEN' ] if action == 'cancel' and \ not job_dict['STATUS'] in possible_cancel_states: changedstatusjob['message'] = \ 'You can only cancel jobs with status: %s.'\ % ' or '.join(possible_cancel_states) status = returnvalues.CLIENT_ERROR changedstatusjobs.append(changedstatusjob) continue possible_freeze_states = ['QUEUED', 'RETRY'] if action == 'freeze' and \ not job_dict['STATUS'] in possible_freeze_states: changedstatusjob['message'] = \ 'You can only freeze jobs with status: %s.'\ % ' or '.join(possible_freeze_states) status = returnvalues.CLIENT_ERROR changedstatusjobs.append(changedstatusjob) continue possible_thaw_states = ['FROZEN'] if action == 'thaw' and \ not job_dict['STATUS'] in possible_thaw_states: changedstatusjob['message'] = \ 'You can only thaw jobs with status: %s.'\ % ' or '.join(possible_thaw_states) status = returnvalues.CLIENT_ERROR changedstatusjobs.append(changedstatusjob) continue # job action is handled by changing the STATUS field, notifying the # job queue and making sure the server never submits jobs with status # FROZEN or CANCELED. # file is repickled to ensure newest information is used, job_dict # might be old if another script has modified the file. if not unpickle_and_change_status(filepath, new_state, logger): output_objects.append({ 'object_type': 'error_text', 'text': 'Job status could not be changed to %s!' % new_state }) status = returnvalues.SYSTEM_ERROR # Avoid key error and make sure grid_script gets expected number of # arguments if 'UNIQUE_RESOURCE_NAME' not in job_dict: job_dict['UNIQUE_RESOURCE_NAME'] = \ 'UNIQUE_RESOURCE_NAME_NOT_FOUND' if 'EXE' not in job_dict: job_dict['EXE'] = 'EXE_NAME_NOT_FOUND' # notify queue if not send_message_to_grid_script( 'JOBACTION ' + job_id + ' ' + job_dict['STATUS'] + ' ' + new_state + ' ' + job_dict['UNIQUE_RESOURCE_NAME'] + ' ' + job_dict['EXE'] + '\n', logger, configuration): output_objects.append({ 'object_type': 'error_text', 'text': '''Error sending message to grid_script, job may still be in the job queue.''' }) status = returnvalues.SYSTEM_ERROR continue changedstatusjob['newstatus'] = new_state changedstatusjobs.append(changedstatusjob) output_objects.append({ 'object_type': 'changedstatusjobs', 'changedstatusjobs': changedstatusjobs }) 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) 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) if not configuration.site_enable_jobs: output_objects.append({ 'object_type': 'error_text', 'text': '''Job execution is not enabled on this system''' }) return (output_objects, returnvalues.SYSTEM_ERROR) title_entry = find_entry(output_objects, 'title') title_entry['text'] = 'Manage jobs' output_objects.append({'object_type': 'header', 'text': 'Manage Jobs'}) output_objects.append({ 'object_type': 'sectionheader', 'text': 'View status of all submitted jobs' }) output_objects.append({ 'object_type': 'html_form', 'text': ''' <form method="get" action="jobstatus.py"> Sort by modification time: <input type="radio" name="flags" value="sv" />yes <input type="radio" name="flags" checked="checked" value="vi" />no<br /> <input type="hidden" name="job_id" value="*" /> <input type="hidden" name="output_format" value="html" /> <input type="submit" value="Show All" /> </form> ''' }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'View status of individual jobs' }) output_objects.append({ 'object_type': 'html_form', 'text': ''' Filter job IDs (* and ? wildcards are supported)<br /> <form method="get" action="jobstatus.py"> Job ID: <input type="text" name="job_id" value="*" size="30" /><br /> Show only <input type="text" name="max_jobs" size="6" value=5 /> first matching jobs<br /> Sort by modification time: <input type="radio" name="flags" checked="checked" value="vsi" />yes <input type="radio" name="flags" value="vi" />no<br /> <input type="hidden" name="output_format" value="html" /> <input type="submit" value="Show" /> </form> ''' }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Resubmit job' }) form_method = 'post' csrf_limit = get_csrf_limit(configuration) fill_helpers = { 'short_title': configuration.short_title, 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit } target_op = 'resubmit' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) fill_helpers.update({'target_op': target_op, 'csrf_token': csrf_token}) output_objects.append({ 'object_type': 'html_form', 'text': ''' <form method="%(form_method)s" action="%(target_op)s.py"> <input type="hidden" name="%(csrf_field)s" value="%(csrf_token)s" /> Job ID: <input type="text" name="job_id" size="30" /><br /> <input type="hidden" name="output_format" value="html" /> <input type="submit" value="Submit" /> </form> ''' % fill_helpers }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Freeze pending job' }) target_op = 'jobaction' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) fill_helpers.update({'target_op': target_op, 'csrf_token': csrf_token}) output_objects.append({ 'object_type': 'html_form', 'text': ''' <form method="%(form_method)s" action="%(target_op)s.py"> <input type="hidden" name="%(csrf_field)s" value="%(csrf_token)s" /> Job ID: <input type="text" name="job_id" size="30" /><br /> <input type="hidden" name="action" value="freeze" /> <input type="hidden" name="output_format" value="html" /> <input type="submit" value="Freeze job" /> </form> ''' % fill_helpers }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Thaw frozen job' }) output_objects.append({ 'object_type': 'html_form', 'text': ''' <form method="%(form_method)s" action="%(target_op)s.py"> <input type="hidden" name="%(csrf_field)s" value="%(csrf_token)s" /> Job ID: <input type="text" name="job_id" size="30" /><br /> <input type="hidden" name="action" value="thaw" /> <input type="hidden" name="output_format" value="html" /> <input type="submit" value="Thaw job" /> </form> ''' % fill_helpers }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Cancel pending or executing job' }) output_objects.append({ 'object_type': 'html_form', 'text': ''' <form method="%(form_method)s" action="%(target_op)s.py"> <input type="hidden" name="%(csrf_field)s" value="%(csrf_token)s" /> Job ID: <input type="text" name="job_id" size="30" /><br /> <input type="hidden" name="action" value="cancel" /> <input type="hidden" name="output_format" value="html" /> <input type="submit" value="Cancel job" /> </form> ''' % fill_helpers }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Request live I/O' }) output_objects.append({ 'object_type': 'html_form', 'text': ''' <form method="get" action="liveio.py"> Job ID: <input type="text" name="job_id" size="30" /><br /> <input type="hidden" name="output_format" value="html" /> <input type="submit" value="Request" /> </form> <br /> ''' }) 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) 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, # NOTE: path can use wildcards, dst and current_dir cannot typecheck_overrides={'path': valid_path_pattern}, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) flags = ''.join(accepted['flags']) pattern_list = accepted['path'] dst = accepted['dst'][-1] 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] if dst: dst = os.path.join(current_dir, dst) # 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 if verbose(flags): for flag in flags: output_objects.append({ 'object_type': 'text', 'text': '%s using flag: %s' % (op_name, flag) }) # 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): 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) if verbose(flags): output_objects.append({ 'object_type': 'text', 'text': "working in %s" % current_dir }) if 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) # 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('chksum 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): 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 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 checksum to "%s": inside a read-only location!' % relative_dest }) return (output_objects, returnvalues.CLIENT_ERROR) all_lines = [] 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: # 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 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': 'file_not_found', 'name': pattern }) status = returnvalues.FILE_NOT_FOUND # NOTE: we produce output matching an invocation of: # du -aL --apparent-size --block-size=1 PATH [PATH ...] filedus = [] summarize_output = summarize(flags) for abs_path in match: if invisible_path(abs_path): continue relative_path = abs_path.replace(base_dir, '') # cache accumulated sub dir sizes - du sums into parent dir size dir_sizes = {} try: # Assume a directory to walk for (root, dirs, files) in walk(abs_path, topdown=False, followlinks=True): if invisible_path(root): continue dir_bytes = 0 for name in files: real_file = os.path.join(root, name) if invisible_path(real_file): continue relative_file = real_file.replace(base_dir, '') size = os.path.getsize(real_file) dir_bytes += size if not summarize_output: filedus.append({ 'object_type': 'filedu', 'name': relative_file, 'bytes': size }) for name in dirs: real_dir = os.path.join(root, name) if invisible_path(real_dir): continue dir_bytes += dir_sizes[real_dir] relative_root = root.replace(base_dir, '') dir_bytes += os.path.getsize(root) dir_sizes[root] = dir_bytes if root == abs_path or not summarize_output: filedus.append({ 'object_type': 'filedu', 'name': relative_root, 'bytes': dir_bytes }) if os.path.isfile(abs_path): # Fall back to plain file where walk is empty size = os.path.getsize(abs_path) filedus.append({ 'object_type': 'filedu', 'name': relative_path, 'bytes': size }) except Exception as 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 if dst: all_lines += [ '%(bytes)d\t\t%(name)s\n' % entry for entry in filedus ] else: output_objects.append({ 'object_type': 'filedus', 'filedus': filedus }) if dst and not write_file(''.join(all_lines), abs_dest, logger): output_objects.append({ 'object_type': 'error_text', 'text': "failed to write disk use to %s" % relative_dest }) logger.error("writing disk use to %s for %s failed" % (abs_dest, client_id)) status = returnvalues.SYSTEM_ERROR 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, # NOTE: path cannot use wildcards here typecheck_overrides={}, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) path = accepted['path'][-1] chosen_newline = accepted['newline'][-1] submitjob = accepted['submitjob'][-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) if not configuration.site_enable_jobs and submitjob: output_objects.append({ 'object_type': 'error_text', 'text': '''Job execution is not enabled on this system''' }) return (output_objects, returnvalues.SYSTEM_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 # HTML spec dictates newlines in forms to be MS style (\r\n) # rather than un*x style (\n): change if requested. form_newline = '\r\n' allowed_newline = {'unix': '\n', 'mac': '\r', 'windows': '\r\n'} output_objects.append({ 'object_type': 'header', 'text': 'Saving changes to edited file' }) if not chosen_newline in allowed_newline.keys(): output_objects.append({ 'object_type': 'error_text', 'text': 'Unsupported newline style supplied: %s (must be one of %s)' % (chosen_newline, ', '.join(allowed_newline.keys())) }) return (output_objects, returnvalues.CLIENT_ERROR) saved_newline = allowed_newline[chosen_newline] # Check directory traversal attempts before actual handling to avoid # leaking information about file system layout while allowing consistent # error messages abs_path = '' unfiltered_match = glob.glob(base_dir + path) 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, path)) output_objects.append({ 'object_type': 'error_text', 'text': "Invalid path! (%s expands to an illegal path)" % path }) return (output_objects, returnvalues.CLIENT_ERROR) if abs_path == '': # IMPORTANT: path must be expanded to abs for proper chrooting abs_path = os.path.abspath(os.path.join(base_dir, path.lstrip(os.sep))) 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, path)) output_objects.append({ 'object_type': 'error_text', 'text': "Invalid path! (%s expands to an illegal path)" % path }) return (output_objects, returnvalues.CLIENT_ERROR) if not check_write_access(abs_path, parent_dir=True): logger.warning('%s called without write access: %s' % (op_name, abs_path)) output_objects.append({ 'object_type': 'error_text', 'text': 'cannot edit "%s": inside a read-only location!' % path }) status = returnvalues.CLIENT_ERROR return (output_objects, returnvalues.CLIENT_ERROR) (owner, time_left) = acquire_edit_lock(abs_path, client_id) if owner != client_id: output_objects.append({ 'object_type': 'error_text', 'text': "You don't have the lock for %s!" % path }) return (output_objects, returnvalues.CLIENT_ERROR) try: fh = open(abs_path, 'w+') fh.write(user_arguments_dict['editarea'][0].replace( form_newline, saved_newline)) fh.close() # everything ok output_objects.append({ 'object_type': 'text', 'text': 'Saved changes to %s.' % path }) logger.info('saved changes to %s' % path) release_edit_lock(abs_path, client_id) except Exception as exc: # Don't give away information about actual fs layout output_objects.append({ 'object_type': 'error_text', 'text': '%s could not be written! (%s)' % (path, str(exc).replace(base_dir, '')) }) return (output_objects, returnvalues.SYSTEM_ERROR) if submitjob: output_objects.append({ 'object_type': 'text', 'text': 'Submitting saved file to parser' }) submitstatus = {'object_type': 'submitstatus', 'name': path} (new_job_status, msg, job_id) = new_job(abs_path, client_id, configuration, False, True) if not new_job_status: submitstatus['status'] = False submitstatus['message'] = msg else: submitstatus['status'] = True submitstatus['job_id'] = job_id output_objects.append({ 'object_type': 'submitstatuslist', 'submitstatuslist': [submitstatus] }) output_objects.append({ 'object_type': 'link', 'destination': 'javascript:history.back()', 'class': 'backlink iconspace', 'title': 'Go back to previous page', 'text': 'Back to previous page' }) return (output_objects, returnvalues.OK)
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) 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) unique_res_names = accepted['unique_resource_name'] if not configuration.site_enable_resources: output_objects.append({ 'object_type': 'error_text', 'text': '''Resources are not enabled on this system''' }) return (output_objects, returnvalues.SYSTEM_ERROR) # prepare for confirm dialog, tablesort and toggling the views (css/js) title_entry = find_entry(output_objects, 'title') title_entry['text'] = "Resource Administration" # jquery support for tablesorter and confirmation on request and leave # requests table initially sorted by 4, 3 (date first and with alphabetical # client ID) table_specs = [{ 'table_id': 'accessrequeststable', 'pager_id': 'accessrequests_pager', 'sort_order': '[[4,0],[3,0]]' }] (add_import, add_init, add_ready) = man_base_js(configuration, table_specs, {'width': 600}) add_init += ''' var toggleHidden = function(classname) { // classname supposed to have a leading dot $(classname).toggleClass("hidden"); }; /* helper for dynamic form input fields */ function onOwnerInputChange() { makeSpareFields("#dynownerspares", "cert_id"); } ''' add_ready += ''' /* init add owners form with dynamic input fields */ onOwnerInputChange(); $("#dynownerspares").on("blur", "input[name=cert_id]", function(event) { //console.debug("in add owner blur handler"); onOwnerInputChange(); } ); ''' 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) }) form_method = 'post' csrf_limit = get_csrf_limit(configuration) fill_helpers = { 'short_title': configuration.short_title, 'vgrid_label': configuration.site_vgrid_label, 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit } (re_stat, re_list) = list_runtime_environments(configuration) if not re_stat: logger.warning('Failed to load list of runtime environments') output_objects.append({ 'object_type': 'error_text', 'text': 'Error getting list of runtime environments' }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({'object_type': 'header', 'text': 'Manage Resource'}) output_objects.append({ 'object_type': 'sectionheader', 'text': '%(short_title)s Resources Owned' % fill_helpers }) quick_links = [{ 'object_type': 'text', 'text': 'Quick links to all your resources and individual management' }] quick_links.append({ 'object_type': 'html_form', 'text': '<div class="hidden quicklinks">' }) quick_links.append({ 'object_type': 'link', 'destination': "javascript:toggleHidden('.quicklinks');", 'class': 'removeitemlink iconspace', 'title': 'Toggle view', 'text': 'Hide quick links' }) quick_links.append({'object_type': 'text', 'text': ''}) quick_res = {} quick_links_index = len(output_objects) output_objects.append({'object_type': 'sectionheader', 'text': ''}) owned = 0 res_map = get_resource_map(configuration) for unique_resource_name in res_map.keys(): if sandbox_resource(unique_resource_name): continue owner_list = res_map[unique_resource_name][OWNERS] resource_config = res_map[unique_resource_name][CONF] visible_res_name = res_map[unique_resource_name][RESID] if client_id in owner_list: quick_res[unique_resource_name] = { 'object_type': 'multilinkline', 'links': [{ 'object_type': 'link', 'destination': '?unique_resource_name=%s' % unique_resource_name, 'class': 'adminlink iconspace', 'title': 'Manage %s' % unique_resource_name, 'text': 'Manage %s' % unique_resource_name, }, { 'object_type': 'link', 'destination': 'viewres.py?unique_resource_name=%s' % visible_res_name, 'class': 'infolink iconspace', 'title': 'View %s' % unique_resource_name, 'text': 'View %s' % unique_resource_name, }] } if unique_resource_name in unique_res_names: raw_conf_file = os.path.join(configuration.resource_home, unique_resource_name, 'config.MiG') try: filehandle = open(raw_conf_file, 'r') raw_conf = filehandle.readlines() filehandle.close() except: raw_conf = [''] res_html = display_resource(client_id, unique_resource_name, raw_conf, resource_config, owner_list, re_list, configuration, fill_helpers) output_objects.append({ 'object_type': 'html_form', 'text': res_html }) # Pending requests target_op = "addresowner" csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) helper = html_post_helper( target_op, "%s.py" % target_op, { 'unique_resource_name': unique_resource_name, 'cert_id': '__DYNAMIC__', 'request_name': '__DYNAMIC__', csrf_field: csrf_token }) output_objects.append({ 'object_type': 'html_form', 'text': helper }) target_op = "rejectresreq" csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) helper = html_post_helper( target_op, "%s.py" % target_op, { 'unique_resource_name': unique_resource_name, 'request_name': '__DYNAMIC__', csrf_field: csrf_token }) output_objects.append({ 'object_type': 'html_form', 'text': helper }) request_dir = os.path.join(configuration.resource_home, unique_resource_name) request_list = [] for req_name in list_access_requests(configuration, request_dir): req = load_access_request(configuration, request_dir, req_name) if not req: continue if req.get('request_type', None) != "resourceowner": logger.error( "unexpected request_type %(request_type)s" % req) continue request_item = build_accessrequestitem_object( configuration, req) # Convert filename with exotic chars into url-friendly pure hex version shared_args = { "unique_resource_name": unique_resource_name, "request_name": hexlify(req["request_name"]) } accept_args, reject_args = {}, {} accept_args.update(shared_args) reject_args.update(shared_args) if req['request_type'] == "resourceowner": accept_args["cert_id"] = req["entity"] request_item['acceptrequestlink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s', %s, %s);" % ("addresowner", "Accept %(target)s %(request_type)s request from %(entity)s" % req, 'undefined', "{%s}" % ', '.join([ "'%s': '%s'" % pair for pair in accept_args.items() ])), 'class': 'addlink iconspace', 'title': 'Accept %(target)s %(request_type)s request from %(entity)s' % req, 'text': '' } request_item['rejectrequestlink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s', %s, %s);" % ("rejectresreq", "Reject %(target)s %(request_type)s request from %(entity)s" % req, 'undefined', "%s" % reject_args), 'class': 'removelink iconspace', 'title': 'Reject %(target)s %(request_type)s request from %(entity)s' % req, 'text': '' } request_list.append(request_item) output_objects.append({ 'object_type': 'sectionheader', 'text': "Pending Requests" }) output_objects.append({ 'object_type': 'table_pager', 'id_prefix': 'accessrequests_', 'entry_name': 'access requests', 'default_entries': default_pager_entries }) output_objects.append({ 'object_type': 'accessrequests', 'accessrequests': request_list }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Retire resource' }) output_objects.append({ 'object_type': 'text', 'text': ''' Use the link below to permanently remove the resource from the grid after stopping all units and the front end. ''' }) target_op = "delres" csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) js_name = 'delres%s' % hexlify(unique_resource_name) helper = html_post_helper( js_name, '%s.py' % target_op, { 'unique_resource_name': unique_resource_name, csrf_field: csrf_token }) output_objects.append({ 'object_type': 'html_form', 'text': helper }) output_objects.append({ 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really delete %s? (fails if it is busy)' % unique_resource_name), 'class': 'removelink iconspace', 'title': 'Delete %s' % unique_resource_name, 'text': 'Delete %s' % unique_resource_name }) owned += 1 if owned == 0: output_objects.append({ 'object_type': 'text', 'text': 'You are not listed as owner of any resources!' }) else: sorted_links = quick_res.items() sorted_links.sort() for (res_id, link_obj) in sorted_links: quick_links.append(link_obj) # add new line quick_links.append({'object_type': 'text', 'text': ''}) quick_links.append({ 'object_type': 'html_form', 'text': '</div><div class="quicklinks">' }) quick_links.append({ 'object_type': 'link', 'destination': "javascript:toggleHidden('.quicklinks');", 'class': 'additemlink iconspace', 'title': 'Toggle view', 'text': 'Show quick links' }) quick_links.append({'object_type': 'html_form', 'text': '</div>'}) output_objects = output_objects[:quick_links_index]\ + quick_links + output_objects[quick_links_index:] return (output_objects, returnvalues.OK)
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] transfer_id = accepted['transfer_id'][-1] protocol = accepted['protocol'][-1] fqdn = accepted['fqdn'][-1] port = accepted['port'][-1] src_list = accepted['transfer_src'] dst = accepted['transfer_dst'][-1] username = accepted['username'][-1] password = accepted['transfer_pw'][-1] key_id = accepted['key_id'][-1] # Skip empty exclude entries as they break backend calls exclude_list = [i for i in accepted['exclude'] if i] notify = accepted['notify'][-1] compress = accepted['compress'][-1] flags = accepted['flags'] anon_checked, pw_checked, key_checked = '', '', '' if username: if key_id: key_checked = 'checked' init_login = "******" else: pw_checked = 'checked' init_login = "******" else: anon_checked = 'checked' init_login = "******" use_compress = False if compress.lower() in ("true", "1", "yes", "on"): use_compress = True title_entry = find_entry(output_objects, 'title') title_entry['text'] = 'Background Data Transfers' title_entry['container_class'] = 'fillwidth', # jquery support for tablesorter and confirmation on delete/redo: # datatransfer and key tables initially sorted by 0 (id) */ datatransfer_spec = { 'table_id': 'datatransferstable', 'pager_id': 'datatransfers_pager', 'sort_order': '[[0,0]]' } transferkey_spec = { 'table_id': 'transferkeystable', 'pager_id': 'transferkeys_pager', 'sort_order': '[[0,0]]' } (add_import, add_init, add_ready) = man_base_js(configuration, [datatransfer_spec, transferkey_spec]) add_init += ''' var fields = 0; var max_fields = 20; var src_input = "<label for=\'transfer_src\'>Source path(s)</label>"; src_input += "<input id=\'src_FIELD\' type=text size=60 name=transfer_src value=\'PATH\' title=\'relative source path: local for exports and remote for imports\' />"; src_input += "<input id=\'src_file_FIELD\' type=radio onclick=\'setSrcDir(FIELD, false);\' checked />Source file"; src_input += "<input id=\'src_dir_FIELD\' type=radio onclick=\'setSrcDir(FIELD, true);\' />Source directory (recursive)"; src_input += "<br />"; var exclude_input = "<label for=\'exclude\'>Exclude path(s)</label>"; exclude_input += "<input type=text size=60 name=exclude value=\'PATH\' title=\'relative path or regular expression to exclude\' />"; exclude_input += "<br />"; function addSource(path, is_dir) { if (path === undefined) { path = ""; } if (is_dir === undefined) { is_dir = false; } if (fields < max_fields) { $("#srcfields").append(src_input.replace(/FIELD/g, fields).replace(/PATH/g, path)); setSrcDir(fields, is_dir); fields += 1; } else { alert("Maximum " + max_fields + " source fields allowed!"); } } function addExclude(path) { if (path === undefined) { path = ""; } $("#excludefields").append(exclude_input.replace(/PATH/g, path)); } function setDir(target, field_no, is_dir) { var id_prefix = "#"+target+"_"; var input_id = id_prefix+field_no; var file_id = id_prefix+"file_"+field_no; var dir_id = id_prefix+"dir_"+field_no; var value = $(input_id).val(); $(file_id).prop("checked", ""); $(dir_id).prop("checked", ""); if (is_dir) { $(dir_id).prop("checked", "checked"); if(value.substr(-1) != "/") { value += "/"; } } else { $(file_id).prop("checked", "checked"); if(value.substr(-1) == "/") { value = value.substr(0, value.length - 1); } } $(input_id).val(value); return false; } function setSrcDir(field_no, is_dir) { return setDir("src", field_no, is_dir); } function setDstDir(field_no, is_dir) { return setDir("dst", field_no, is_dir); } function refreshSrcDir(field_no) { var dir_id = "#src_dir_"+field_no; var is_dir = $(dir_id).prop("checked"); return setSrcDir(field_no, is_dir); } function refreshDstDir(field_no) { var dir_id = "#dst_dir_"+field_no; var is_dir = $(dir_id).prop("checked"); return setDstDir(field_no, is_dir); } function setDefaultPort() { port_map = {"http": 80, "https": 443, "sftp": 22, "scp": 22, "ftp": 21, "ftps": 21, "webdav": 80, "webdavs": 443, "rsyncssh": 22, "rsyncd": 873}; var protocol = $("#protocol_select").val(); var port = port_map[protocol]; if (port != undefined) { $("#port_input").val(port); } else { alert("no default port provided for "+protocol); } } function beforeSubmit() { for(var i=0; i < fields; i++) { refreshSrcDir(i); } refreshDstDir(0); // Proceed with submit return true; } function doSubmit() { $("#submit-request-transfer").click(); } function enableLogin(method) { $("#anonymous_choice").prop("checked", ""); $("#userpassword_choice").prop("checked", ""); $("#userkey_choice").prop("checked", ""); $("#username").prop("disabled", false); $("#password").prop("disabled", true); $("#key").prop("disabled", true); $("#login_fields").show(); $("#password_entry").hide(); $("#key_entry").hide(); if (method == "password") { $("#userpassword_choice").prop("checked", "checked"); $("#password").prop("disabled", false); $("#password_entry").show(); } else if (method == "key") { $("#userkey_choice").prop("checked", "checked"); $("#key").prop("disabled", false); $("#key_entry").show(); } else { $("#anonymous_choice").prop("checked", "checked"); $("#username").prop("disabled", true); $("#login_fields").hide(); } } ''' # Mangle ready handling to begin with dynamic init and end with tab init pre_ready = ''' enableLogin("%s"); ''' % init_login for src in src_list or ['']: pre_ready += ''' addSource("%s", %s); ''' % (src, ("%s" % src.endswith('/')).lower()) for exclude in exclude_list or ['']: pre_ready += ''' addExclude("%s"); ''' % exclude add_ready = ''' %s %s /* NOTE: requires managers CSS fix for proper tab bar height */ $(".datatransfer-tabs").tabs(); $("#logarea").scrollTop($("#logarea")[0].scrollHeight); ''' % (pre_ready, add_ready) 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) }) output_objects.append({ 'object_type': 'header', 'text': 'Manage background data transfers' }) if not configuration.site_enable_transfers: output_objects.append({ 'object_type': 'text', 'text': '''Backgroung data transfers 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('datatransfer %s from %s' % (action, client_id)) 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, transfer_map) = load_data_transfers(configuration, client_id) if not load_status: transfer_map = {} restrict_list = [] for from_fqdn in configuration.site_transfers_from: restrict_list += [from_fqdn, socket.gethostbyname(from_fqdn)] restrict_str = 'from="%s",no-pty,' % ','.join(restrict_list) restrict_str += 'no-port-forwarding,no-agent-forwarding,no-X11-forwarding' restrict_template = ''' As usual it is a good security measure to prepend a <em>from</em> restriction when you know the key will only be used from a single location.<br/> In this case the keys will only ever be used from %s and will not need much else, so the public key can be inserted in your authorized_keys file as: <br/> <p> <textarea class="publickey" rows="5" readonly="readonly">%s %%s</textarea> </p> ''' % (configuration.short_title, restrict_str) form_method = 'post' csrf_limit = get_csrf_limit(configuration) target_op = 'datatransfer' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) if action in get_actions: datatransfers = [] for (saved_id, transfer_dict) in transfer_map.items(): transfer_item = build_transferitem_object(configuration, transfer_dict) transfer_item['status'] = transfer_item.get('status', 'NEW') data_url = '' # NOTE: we need to urlencode any exotic chars in paths here if transfer_item['action'] == 'import': enc_path = quote(("%(dst)s" % transfer_item)) data_url = "fileman.py?path=%s" % enc_path elif transfer_item['action'] == 'export': enc_paths = [quote(i) for i in transfer_item['src']] data_url = "fileman.py?path=" + ';path='.join(enc_paths) if data_url: transfer_item['viewdatalink'] = { 'object_type': 'link', 'destination': data_url, 'class': 'viewlink iconspace', 'title': 'View local component of %s' % saved_id, 'text': '' } transfer_item['viewoutputlink'] = { 'object_type': 'link', 'destination': "fileman.py?path=transfer_output/%s/" % saved_id, 'class': 'infolink iconspace', 'title': 'View status files for %s' % saved_id, 'text': '' } # Edit is just a call to self with fillimport set args = [('action', 'fill%(action)s' % transfer_dict), ('key_id', '%(key)s' % transfer_dict), ('transfer_dst', '%(dst)s' % transfer_dict)] for src in transfer_dict['src']: args.append(('transfer_src', src)) for exclude in transfer_dict.get('exclude', []): args.append(('exclude', exclude)) for field in edit_fields: val = transfer_dict.get(field, '') args.append((field, val)) transfer_args = urlencode(args, True) transfer_item['edittransferlink'] = { 'object_type': 'link', 'destination': "%s.py?%s" % (target_op, transfer_args), 'class': 'editlink iconspace', 'title': 'Edit or duplicate transfer %s' % saved_id, 'text': '' } js_name = 'delete%s' % hexlify(saved_id) helper = html_post_helper( js_name, '%s.py' % target_op, { 'transfer_id': saved_id, 'action': 'deltransfer', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) transfer_item['deltransferlink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really remove %s?' % saved_id), 'class': 'removelink iconspace', 'title': 'Remove %s' % saved_id, 'text': '' } js_name = 'redo%s' % hexlify(saved_id) helper = html_post_helper( js_name, '%s.py' % target_op, { 'transfer_id': saved_id, 'action': 'redotransfer', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) transfer_item['redotransferlink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really reschedule %s?' % saved_id), 'class': 'refreshlink iconspace', 'title': 'Reschedule %s' % saved_id, 'text': '' } datatransfers.append(transfer_item) #logger.debug("found datatransfers: %s" % datatransfers) log_path = os.path.join(configuration.user_home, client_id_dir(client_id), "transfer_output", configuration.site_transfer_log) show_lines = 40 log_lines = read_tail(log_path, show_lines, logger) available_keys = load_user_keys(configuration, client_id) if available_keys: key_note = '' else: key_note = '''No keys available - you can add a key for use in transfers below.''' if action.endswith('import'): transfer_action = 'import' elif action.endswith('export'): transfer_action = 'export' else: transfer_action = 'unknown' import_checked, export_checked = 'checked', '' toggle_quiet, scroll_to_create = '', '' if action in ['fillimport', 'fillexport']: if quiet(flags): toggle_quiet = ''' <script> $("#wrap-tabs").hide(); $("#quiet-mode-content").show(); </script> ''' scroll_to_create = ''' <script> $("html, body").animate({ scrollTop: $("#createtransfer").offset().top }, 2000); </script> ''' if action == 'fillimport': import_checked = 'checked' elif action == 'fillexport': export_checked = 'checked' import_checked = '' fill_helpers = { 'import_checked': import_checked, 'export_checked': export_checked, 'anon_checked': anon_checked, 'pw_checked': pw_checked, 'key_checked': key_checked, 'transfer_id': transfer_id, 'protocol': protocol, 'fqdn': fqdn, 'port': port, 'username': username, 'password': password, 'key_id': key_id, 'transfer_src_string': ', '.join(src_list), 'transfer_src': src_list, 'transfer_dst': dst, 'exclude': exclude_list, 'compress': use_compress, 'notify': notify, 'toggle_quiet': toggle_quiet, 'scroll_to_create': scroll_to_create, 'transfer_action': transfer_action, 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit, 'target_op': target_op, 'csrf_token': csrf_token } # Make page with manage transfers tab and manage keys tab output_objects.append({ 'object_type': 'html_form', 'text': ''' <div id="quiet-mode-content" class="hidden"> <p> Accept data %(transfer_action)s of %(transfer_src_string)s from %(protocol)s://%(fqdn)s:%(port)s/ into %(transfer_dst)s ? </p> <p> <input type=button onClick="doSubmit();" value="Accept %(transfer_action)s" /> </p> </div> <div id="wrap-tabs" class="datatransfer-tabs"> <ul> <li><a href="#transfer-tab">Manage Data Transfers</a></li> <li><a href="#keys-tab">Manage Transfer Keys</a></li> </ul> ''' % fill_helpers }) # Display external transfers, log and form to add new ones output_objects.append({ 'object_type': 'html_form', 'text': ''' <div id="transfer-tab"> ''' }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'External Data Transfers' }) output_objects.append({ 'object_type': 'table_pager', 'id_prefix': 'datatransfers_', 'entry_name': 'transfers', 'default_entries': default_pager_entries }) output_objects.append({ 'object_type': 'datatransfers', 'datatransfers': datatransfers }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Latest Transfer Results' }) output_objects.append({ 'object_type': 'html_form', 'text': ''' <textarea id="logarea" class="fillwidth" rows=5 readonly="readonly">%s</textarea> ''' % (''.join(log_lines)) }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Create External Data Transfer' }) transfer_html = ''' <table class="addexttransfer"> <tr><td> Fill in the import/export data transfer details below to request a new background data transfer task.<br/> Source must be a path without wildcard characters and it must be specifically pointed out if the src is a directory. In that case recursive transfer will automatically be used and otherwise the src is considered a single file, so it will fail if that is not the case.<br/> Destination is a single location directory to transfer the data to. It is considered in relation to your user home for <em>import</em> requests. Source is similarly considered in relation to your user home in <em>export</em> requests.<br/> Destination is a always handled as a directory path to transfer source files into.<br/> <form method="%(form_method)s" action="%(target_op)s.py" onSubmit="return beforeSubmit();"> <input type="hidden" name="%(csrf_field)s" value="%(csrf_token)s" /> <fieldset id="transferbox"> <table id="createtransfer" class="addexttransfer"> <tr><td> <label for="action">Action</label> <input type=radio name=action %(import_checked)s value="import" />import data <input type=radio name=action %(export_checked)s value="export" />export data </td></tr> <tr><td> <label for="transfer_id">Optional Transfer ID / Name </label> <input type=text size=60 name=transfer_id value="%(transfer_id)s" pattern="[a-zA-Z0-9._-]*" title="Optional ID string containing only ASCII letters and digits possibly with separators like hyphen, underscore and period" /> </td></tr> <tr><td> <label for="protocol">Protocol</label> <select id="protocol_select" class="protocol-select themed-select html-select" name="protocol" onblur="setDefaultPort();"> ''' # select requested protocol for (key, val) in valid_proto: if protocol == key: selected = 'selected' else: selected = '' transfer_html += '<option %s value="%s">%s</option>' % \ (selected, key, val) transfer_html += ''' </select> </td></tr> <tr><td> <label for="fqdn">Host and port</label> <input type=text size=37 name=fqdn value="%(fqdn)s" required pattern="[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+" title="A fully qualified domain name or Internet IP address for the remote location"/> <input id="port_input" type=number step=1 min=1 max=65535 name=port value="%(port)s" required /> </td></tr> <tr><td> <label for="">Login method</label> <input id="anonymous_choice" type=radio %(anon_checked)s onclick="enableLogin(\'anonymous\');" /> anonymous access <input id="userpassword_choice" type=radio %(pw_checked)s onclick="enableLogin(\'password\');" /> login with password <input id="userkey_choice" type=radio %(key_checked)s onclick="enableLogin(\'key\');" /> login with key </td></tr> <tr id="login_fields" style="display: none;"><td> <label for="username">Username</label> <input id="username" type=text size=60 name=username value="%(username)s" pattern="[a-zA-Z0-9._-]*" title="Optional username used to login on the remote site, if required" /> <br/> <span id="password_entry"> <label for="transfer_pw">Password</label> <input id="password" type=password size=60 name=transfer_pw value="" /> </span> <span id="key_entry"> <label for="key_id">Key</label> <select id="key" class="key-select themed-select html-select" name=key_id /> ''' # select requested key for key_dict in available_keys: if key_dict['key_id'] == key_id: selected = 'selected' else: selected = '' transfer_html += '<option %s value="%s">%s</option>' % \ (selected, key_dict['key_id'], key_dict['key_id']) selected = '' transfer_html += ''' </select> %s ''' % key_note transfer_html += ''' </span> </td></tr> <tr><td> <div id="srcfields"> <!-- NOTE: automatically filled by addSource function --> </div> <input id="addsrcbutton" type="button" onclick="addSource(); return false;" value="Add another source field" /> </td></tr> <tr><td> <label for="transfer_dst">Destination path</label> <input id=\'dst_0\' type=text size=60 name=transfer_dst value="%(transfer_dst)s" required title="relative destination path: local for imports and remote for exports" /> <input id=\'dst_dir_0\' type=radio checked />Destination directory <input id=\'dst_file_0\' type=radio disabled />Destination file<br /> </td></tr> <tr><td> <div id="excludefields"> <!-- NOTE: automatically filled by addExclude function --> </div> <input id="addexcludebutton" type="button" onclick="addExclude(); return false;" value="Add another exclude field" /> </td></tr> <tr><td> <label for="compress">Enable compression (leave unset except for <em>slow</em> sites)</label> <input type=checkbox name=compress> </td></tr> <tr><td> <label for="notify">Optional notify on completion (e.g. email address)</label> <input type=text size=60 name=notify value=\'%(notify)s\'> </td></tr> <tr><td> <span> <input id="submit-request-transfer" type=submit value="Request transfer" /> <input type=reset value="Clear" /> </span> </td></tr> </table> </fieldset> </form> </td> </tr> </table> %(toggle_quiet)s %(scroll_to_create)s ''' output_objects.append({ 'object_type': 'html_form', 'text': transfer_html % fill_helpers }) output_objects.append({ 'object_type': 'html_form', 'text': ''' </div> ''' }) # Display key management output_objects.append({ 'object_type': 'html_form', 'text': ''' <div id="keys-tab"> ''' }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Manage Data Transfer Keys' }) key_html = ''' <form method="%(form_method)s" action="%(target_op)s.py"> <input type="hidden" name="%(csrf_field)s" value="%(csrf_token)s" /> <table class="managetransferkeys"> <tr><td> ''' transferkeys = [] for key_dict in available_keys: key_item = build_keyitem_object(configuration, key_dict) saved_id = key_item['key_id'] js_name = 'delete%s' % hexlify(saved_id) helper = html_post_helper(js_name, '%s.py' % target_op, { 'key_id': saved_id, 'action': 'delkey', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) key_item['delkeylink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really remove %s?' % saved_id), 'class': 'removelink iconspace', 'title': 'Remove %s' % saved_id, 'text': '' } transferkeys.append(key_item) output_objects.append({ 'object_type': 'table_pager', 'id_prefix': 'transferkeys_', 'entry_name': 'keys', 'default_entries': default_pager_entries }) output_objects.append({ 'object_type': 'transferkeys', 'transferkeys': transferkeys }) key_html += ''' Please copy the public key to your ~/.ssh/authorized_keys or ~/.ssh/authorized_keys2 file on systems where you want to login with the corresponding key.<br/> %s </td></tr> <tr><td> Select a name below to create a new key for use in future transfers. The key is generated and stored in a private storage area on %s, so that only the transfer service can access and use it for your transfers. </td></tr> <tr><td> <input type=hidden name=action value="generatekey" /> Key name:<br/> <input type=text size=60 name=key_id value="" required pattern="[a-zA-Z0-9._-]+" title="internal name for the key when used in transfers. I.e. letters and digits separated only by underscores, periods and hyphens" /> <br/> <input type=submit value="Generate key" /> </td></tr> </table> </form> ''' % (restrict_template % 'ssh-rsa AAAAB3NzaC...', configuration.short_title) output_objects.append({ 'object_type': 'html_form', 'text': key_html % fill_helpers }) output_objects.append({ 'object_type': 'html_form', 'text': ''' </div> ''' }) output_objects.append({ 'object_type': 'html_form', 'text': ''' </div> ''' }) return (output_objects, returnvalues.OK) elif action in transfer_actions: # NOTE: all path validation is done at run-time in grid_transfers transfer_dict = transfer_map.get(transfer_id, {}) if action == 'deltransfer': if transfer_dict is None: output_objects.append({ 'object_type': 'error_text', 'text': 'existing transfer_id is required for delete' }) return (output_objects, returnvalues.CLIENT_ERROR) (save_status, _) = delete_data_transfer(configuration, client_id, transfer_id, transfer_map) desc = "delete" elif action == 'redotransfer': if transfer_dict is None: output_objects.append({ 'object_type': 'error_text', 'text': 'existing transfer_id is required for reschedule' }) return (output_objects, returnvalues.CLIENT_ERROR) transfer_dict['status'] = 'NEW' (save_status, _) = update_data_transfer(configuration, client_id, transfer_dict, transfer_map) desc = "reschedule" else: if not fqdn: output_objects.append({ 'object_type': 'error_text', 'text': 'No host address provided!' }) return (output_objects, returnvalues.CLIENT_ERROR) if not [src for src in src_list if src] or not dst: output_objects.append({ 'object_type': 'error_text', 'text': 'transfer_src and transfer_dst parameters ' 'required for all data transfers!' }) return (output_objects, returnvalues.CLIENT_ERROR) if protocol == "rsyncssh" and not key_id: output_objects.append({ 'object_type': 'error_text', 'text': 'RSYNC over SSH is only supported with key!' }) return (output_objects, returnvalues.CLIENT_ERROR) if not password and not key_id and protocol in warn_anon: output_objects.append({ 'object_type': 'warning', 'text': ''' %s transfers usually require explicit authentication with your credentials. Proceeding as requested with anonymous login, but the transfer is likely to fail.''' % valid_proto_map[protocol] }) if key_id and protocol in warn_key: output_objects.append({ 'object_type': 'warning', 'text': ''' %s transfers usually only support authentication with username and password rather than key. Proceeding as requested, but the transfer is likely to fail if it really requires login.''' % valid_proto_map[protocol] }) # Make pseudo-unique ID based on msec time since epoch if not given if not transfer_id: transfer_id = "transfer-%d" % (time.time() * 1000) if transfer_dict: desc = "update" else: desc = "create" if password: # We don't want to store password in plain text on disk password_digest = make_digest('datatransfer', client_id, password, configuration.site_digest_salt) else: password_digest = '' transfer_dict.update({ 'transfer_id': transfer_id, 'action': action, 'protocol': protocol, 'fqdn': fqdn, 'port': port, 'username': username, 'password_digest': password_digest, 'key': key_id, 'src': src_list, 'dst': dst, 'exclude': exclude_list, 'compress': use_compress, 'notify': notify, 'status': 'NEW' }) (save_status, _) = create_data_transfer(configuration, client_id, transfer_dict, transfer_map) if not save_status: output_objects.append({ 'object_type': 'error_text', 'text': 'Error in %s data transfer %s: ' % (desc, transfer_id) + 'save updated transfers failed!' }) return (output_objects, returnvalues.CLIENT_ERROR) output_objects.append({ 'object_type': 'text', 'text': '%sd transfer request %s.' % (desc.title(), transfer_id) }) if action != 'deltransfer': output_objects.append({ 'object_type': 'link', 'destination': "fileman.py?path=transfer_output/%s/" % transfer_id, 'title': 'Transfer status and output', 'text': 'Transfer status and output folder' }) output_objects.append({ 'object_type': 'text', 'text': ''' Please note that the status files only appear after the transfer starts, so it may be empty now. ''' }) logger.debug('datatransfer %s from %s done: %s' % (action, client_id, transfer_dict)) elif action in key_actions: if action == 'generatekey': (gen_status, pub) = generate_user_key(configuration, client_id, key_id) if gen_status: output_objects.append({ 'object_type': 'html_form', 'text': ''' Generated new key with name %s and associated public key:<br/> <textarea class="publickey" rows="5" readonly="readonly">%s</textarea> <p> Please copy it to your ~/.ssh/authorized_keys or ~/.ssh/authorized_keys2 file on the host(s) where you want to use this key for background transfer login. <br/> %s </p> ''' % (key_id, pub, restrict_template % pub) }) else: output_objects.append({ 'object_type': 'error_text', 'text': ''' Key generation for name %s failed with error: %s''' % (key_id, pub) }) return (output_objects, returnvalues.CLIENT_ERROR) elif action == 'delkey': pubkey = '[unknown]' available_keys = load_user_keys(configuration, client_id) for key_dict in available_keys: if key_dict['key_id'] == key_id: pubkey = key_dict.get('public_key', pubkey) (del_status, msg) = delete_user_key(configuration, client_id, key_id) if del_status: output_objects.append({ 'object_type': 'html_form', 'text': ''' <p> Deleted the key "%s" and the associated public key:<br/> </p> <textarea class="publickey" rows="5" readonly="readonly">%s</textarea> <p> You will no longer be able to use it in your data transfers and can safely remove the public key from your ~/.ssh/authorized_keys* files on any hosts where you may have previously added it. </p> ''' % (key_id, pubkey) }) else: output_objects.append({ 'object_type': 'error_text', 'text': ''' Key removal for name %s failed with error: %s''' % (key_id, msg) }) return (output_objects, returnvalues.CLIENT_ERROR) else: output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid data transfer action: %s' % action }) return (output_objects, returnvalues.CLIENT_ERROR) output_objects.append({ 'object_type': 'link', 'destination': 'datatransfer.py', 'text': 'Return to data transfers 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, 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, # NOTE: path cannot use wildcards here typecheck_overrides={}, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) flags = ''.join(accepted['flags']) patterns = accepted['path'] current_dir = accepted['current_dir'][-1] 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 = 'Create User Directory' userstyle = True widgets = True elif share_id: try: (share_mode, _) = extract_mode_id(configuration, share_id) except ValueError as 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 = 'Create Shared Directory' userstyle = False widgets = False else: logger.error('%s called without proper auth: %s' % (op_name, accepted)) output_objects.append({ 'object_type': 'error_text', 'text': 'Authentication is missing!' }) return (output_objects, returnvalues.SYSTEM_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(base_dir, target_dir)) + os.sep title_entry = find_entry(output_objects, 'title') title_entry['text'] = page_title title_entry['skipwidgets'] = not widgets title_entry['skipuserstyle'] = not userstyle output_objects.append({'object_type': 'header', 'text': page_title}) # Input validation assures target_dir can't escape base_dir if not os.path.isdir(base_dir): output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid client/sharelink id!' }) return (output_objects, returnvalues.CLIENT_ERROR) if verbose(flags): for flag in flags: output_objects.append({ 'object_type': 'text', 'text': '%s using flag: %s' % (op_name, flag) }) for pattern in patterns: # Check directory traversal attempts before actual handling to avoid # leaking information about file system layout while allowing # consistent error messages # NB: Globbing disabled on purpose here unfiltered_match = [base_dir + os.sep + current_dir + os.sep + 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): # out of bounds - save user warning for later to allow # partial match: # ../*/* is technically allowed to match own files. logger.warn('%s tried to %s %s restricted path! (%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: cannot create directory '%s': Permission denied" % (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 }) if not parents(flags) and os.path.exists(abs_path): output_objects.append({ 'object_type': 'error_text', 'text': '%s: path exist!' % pattern }) status = returnvalues.CLIENT_ERROR continue if not check_write_access(abs_path, parent_dir=True): logger.warning('%s called without write access: %s' % (op_name, abs_path)) output_objects.append({ 'object_type': 'error_text', 'text': 'cannot create "%s": inside a read-only location!' % pattern }) status = returnvalues.CLIENT_ERROR continue try: gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'], 'created', [relative_path]) if parents(flags): if not os.path.isdir(abs_path): os.makedirs(abs_path) else: os.mkdir(abs_path) logger.info('%s %s done' % (op_name, abs_path)) except Exception as exc: if not isinstance(exc, GDPIOLogError): gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'], 'created', [relative_path], failed=True, details=exc) output_objects.append({ 'object_type': 'error_text', 'text': "%s: '%s' failed!" % (op_name, relative_path) }) logger.error("%s: failed on '%s': %s" % (op_name, relative_path, exc)) status = returnvalues.SYSTEM_ERROR continue output_objects.append({ 'object_type': 'text', 'text': "created directory %s" % (relative_path) }) if id_query: open_query = "%s;current_dir=%s" % (id_query, relative_path) else: open_query = "?current_dir=%s" % relative_path output_objects.append({ 'object_type': 'link', 'destination': 'ls.py%s' % open_query, 'text': 'Open %s' % relative_path }) output_objects.append({'object_type': 'text', 'text': ''}) output_objects.append({ 'object_type': 'link', 'destination': 'ls.py%s' % id_query, 'text': 'Return to files overview' }) 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, op_menu=False) defaults = signature()[1] logger.debug('in extoidaction: %s' % user_arguments_dict) (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) # Unfortunately OpenID does not use POST # 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) title_entry = find_entry(output_objects, 'title') title_entry[ 'text'] = '%s OpenID account sign up' % configuration.short_title title_entry['skipmenu'] = True output_objects.append({ 'object_type': 'header', 'text': '%s OpenID account sign up' % configuration.short_title }) admin_email = configuration.admin_email smtp_server = configuration.smtp_server user_pending = os.path.abspath(configuration.user_pending) # force name to capitalized form (henrik karlsen -> Henrik Karlsen) id_url = os.environ['REMOTE_USER'].strip() openid_prefix = configuration.user_ext_oid_provider.rstrip('/') + '/' raw_login = id_url.replace(openid_prefix, '') full_name = accepted['openid.sreg.full_name'][-1].strip().title() country = accepted['openid.sreg.country'][-1].strip().upper() state = accepted['state'][-1].strip().title() organization = accepted['openid.sreg.organization'][-1].strip() organizational_unit = accepted['openid.sreg.organizational_unit'][ -1].strip() locality = accepted['openid.sreg.locality'][-1].strip() # lower case email address email = accepted['openid.sreg.email'][-1].strip().lower() password = accepted['password'][-1] #verifypassword = accepted['verifypassword'][-1] # keep comment to a single line comment = accepted['comment'][-1].replace('\n', ' ') # single quotes break command line format - remove comment = comment.replace("'", ' ') accept_terms = (accepted['accept_terms'][-1].strip().lower() in ('1', 'o', 'y', 't', 'on', 'yes', 'true')) 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) if not accept_terms: output_objects.append({ 'object_type': 'error_text', 'text': 'You must accept the terms of use in sign up!' }) output_objects.append({ 'object_type': 'link', 'destination': 'javascript:history.back();', 'class': 'genericbutton', 'text': "Try again" }) return (output_objects, returnvalues.CLIENT_ERROR) user_dict = { 'full_name': full_name, 'organization': organization, 'organizational_unit': organizational_unit, 'locality': locality, 'state': state, 'country': country, 'email': email, 'password': password, 'comment': comment, 'expire': default_account_expire(configuration, 'oid'), 'openid_names': [raw_login], 'auth': ['extoid'], } fill_distinguished_name(user_dict) user_id = user_dict['distinguished_name'] if configuration.user_openid_providers and configuration.user_openid_alias: user_dict['openid_names'].append( user_dict[configuration.user_openid_alias]) req_path = None try: (os_fd, req_path) = tempfile.mkstemp(dir=user_pending) os.write(os_fd, dumps(user_dict)) os.close(os_fd) except Exception as err: logger.error('Failed to write OpenID account request to %s: %s' % (req_path, err)) output_objects.append({ 'object_type': 'error_text', 'text': '''Request could not be sent to site administrators. Please contact them manually on %s if this error persists.''' % admin_email }) return (output_objects, returnvalues.SYSTEM_ERROR) logger.info('Wrote OpenID account request to %s' % req_path) tmp_id = req_path.replace(user_pending, '') user_dict['tmp_id'] = tmp_id # TODO: remove cert generation or generate pw for it mig_user = os.environ.get('USER', 'mig') helper_commands = user_manage_commands(configuration, mig_user, req_path, user_id, user_dict, 'oid') user_dict.update(helper_commands) user_dict['site'] = configuration.short_title user_dict['vgrid_label'] = configuration.site_vgrid_label user_dict['vgridman_links'] = generate_https_urls( configuration, '%(auto_base)s/%(auto_bin)s/vgridman.py', {}) email_header = '%s OpenID request for %s' % \ (configuration.short_title, full_name) email_msg = """ Received an OpenID account sign up with user data * Full Name: %(full_name)s * Organization: %(organization)s * State: %(state)s * Country: %(country)s * Email: %(email)s * Comment: %(comment)s * Expire: %(expire)s Command to create user on %(site)s server: %(command_user_create)s Optional command to create matching certificate: %(command_cert_create)s Finally add the user %(distinguished_name)s to any relevant %(vgrid_label)ss using one of the management links: %(vgridman_links)s --- If user must be denied access or deleted at some point --- Command to reject user account request on %(site)s server: %(command_user_reject)s Remove the user %(distinguished_name)s from any relevant %(vgrid_label)ss using one of the management links: %(vgridman_links)s Optional command to revoke any user certificates: %(command_cert_revoke)s You need to copy the resulting signed certificate revocation list (crl.pem) to the web server(s) for the revocation to take effect. Command to suspend user on %(site)s server: %(command_user_suspend)s Command to delete user again on %(site)s server: %(command_user_delete)s --- """ % user_dict logger.info('Sending email: to: %s, header: %s, msg: %s, smtp_server: %s' % (admin_email, email_header, email_msg, smtp_server)) if not send_email(admin_email, email_header, email_msg, logger, configuration): output_objects.append({ 'object_type': 'error_text', 'text': '''An error occured trying to send the email requesting your new user account. Please email the site administrators (%s) manually and include the session ID: %s''' % (admin_email, tmp_id) }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': """Request sent to site administrators: Your user account will be created as soon as possible, so please be patient. Once handled an email will be sent to the account you have specified ('%s') with further information. In case of inquiries about this request, please email the site administrators (%s) and include the session ID: %s""" % (email, configuration.admin_email, tmp_id) }) return (output_objects, returnvalues.OK)
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) defaults = signature()[1] output_objects.append({ 'object_type': 'header', 'text': 'Create runtime environment' }) (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) re_template = accepted['re_template'][-1].upper().strip() software_entries = int(accepted['software_entries'][-1]) environment_entries = int(accepted['environment_entries'][-1]) testprocedure_entry = int(accepted['testprocedure_entry'][-1]) template = {} if re_template: if not is_runtime_environment(re_template, configuration): output_objects.append({ 'object_type': 'error_text', 'text': "re_template ('%s') is not a valid existing runtime env!" % re_template }) return (output_objects, returnvalues.CLIENT_ERROR) (template, msg) = get_re_dict(re_template, configuration) if not template: output_objects.append({ 'object_type': 'error_text', 'text': 'Could not read re_template %s. %s' % (re_template, msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) # Override template fields if user loaded a template and modified the # required entries and chose update. # Use default of 1 sw, 1 env and 0 test or template setting otherwise if software_entries < 0: software_entries = len(template.get('SOFTWARE', [None])) if environment_entries < 0: environment_entries = len(template.get('ENVIRONMENTVARIABLE', [None])) if testprocedure_entry < 0: testprocedure_entry = len(template.get('TESTPROCEDURE', [])) if 'SOFTWARE' in template: new_sw = template['SOFTWARE'][:software_entries] template['SOFTWARE'] = new_sw if 'ENVIRONMENTVARIABLE' in template: new_env = template['ENVIRONMENTVARIABLE'][:environment_entries] template['ENVIRONMENTVARIABLE'] = new_env if 'TESTPROCEDURE' in template: new_test = template['TESTPROCEDURE'][:testprocedure_entry] template['TESTPROCEDURE'] = new_test # Avoid DoS, limit number of software_entries if software_entries > max_software_entries: output_objects.append( {'object_type': 'error_text', 'text' : 'Maximum number of software_entries %s exceeded (%s)' % \ (max_software_entries, software_entries)}) return (output_objects, returnvalues.CLIENT_ERROR) # Avoid DoS, limit number of environment_entries if environment_entries > max_environment_entries: output_objects.append( {'object_type': 'error_text', 'text' : 'Maximum number of environment_entries %s exceeded (%s)' % \ (max_environment_entries, environment_entries)}) return (output_objects, returnvalues.CLIENT_ERROR) rekeywords_dict = get_keywords_dict() (list_status, ret) = list_runtime_environments(configuration) if not list_status: output_objects.append({'object_type': 'error_text', 'text': ret}) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': 'Use existing Runtime Environment as template' }) html_form = \ """<form method='get' action='adminre.py'> <select name='re_template'> <option value=''>None</option> """ for existing_re in ret: html_form += " <option value='%s'>%s</option>\n" % \ (existing_re, existing_re) html_form += """ </select> <input type='submit' value='Get' /> </form>""" output_objects.append({'object_type': 'html_form', 'text': html_form}) output_objects.append({ 'object_type': 'text', 'text': '''Note that a runtime environment can not be changed after creation and it can only be removed if not in use by any resources, so please be careful when filling in the details''' }) output_objects.append({ 'object_type': 'text', 'text': '''Changing the number of software and environment entries removes all data in the form, so please enter the correct values before entering any information.''' }) html_form = \ """<form method='get' action='adminre.py'> <table> """ html_form += """ <tr> <td>Number of needed software entries</td> <td><input type='number' name='software_entries' min=0 max=99 minlength=1 maxlength=2 value='%s' required pattern='[0-9]{1,2}' title='number of software entries needed in runtime environment' /></td> </tr>""" % software_entries html_form += """ <tr> <td>Number of environment entries</td> <td> <input type='number' name='environment_entries' min=0 max=99 minlength=1 maxlength=2 value='%s' required pattern='[0-9]{1,2}' title='number of environment variables provided by runtime environment' /> </td> </tr>""" % environment_entries output_objects.append({'object_type': 'html_form', 'text': html_form}) if testprocedure_entry == 0: select_string = """<option value='0' selected>No</option> <option value=1>Yes</option>""" elif testprocedure_entry == 1: select_string = """<option value='0'>No</option> <option value='1' selected>Yes</option>""" else: output_objects.append({ 'object_type': 'error_text', 'text': 'testprocedure_entry should be 0 or 1, you specified %s' % testprocedure_entry }) return (output_objects, returnvalues.CLIENT_ERROR) html_form = """ <tr> <td>Runtime environment has a testprocedure</td> <td><select name='testprocedure_entry'>%s</select></td> </tr> <tr> <td colspan=2> <input type='hidden' name='re_template' value='%s' /> <input type='submit' value='Update fields' /> </td> </tr> </table> </form><br /> """ % (select_string, re_template) form_method = 'post' csrf_limit = get_csrf_limit(configuration) fill_helpers = { 'short_title': configuration.short_title, 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit } target_op = 'createre' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) fill_helpers.update({'target_op': target_op, 'csrf_token': csrf_token}) html_form += """ <form method='%(form_method)s' action='%(target_op)s.py'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <b>Runtime Environment Name</b><br /> <small>(eg. BASH-2.X-1, must be unique):</small><br /> <input class='p80width' type='text' name='re_name' required pattern='[a-zA-Z0-9_.-]+' title='unique name of ASCII letters and digits separated only by underscores, periods and hyphens' /> <br /> <br /><b>Description:</b><br /> <textarea class='p80width' rows='4' name='redescription'> """ if template: html_form += template['DESCRIPTION'].replace('<br />', '\n') html_form += '</textarea><br />' soft_list = [] if software_entries > 0: html_form += '<br /><b>Needed Software:</b><br />' if template: if 'SOFTWARE' in template: soft_list = template['SOFTWARE'] for soft in soft_list: html_form += """ <textarea class='p80width' rows='6' name='software'>""" for keyname in soft.keys(): if keyname != '': html_form += '%s=%s\n' % (keyname, soft[keyname]) html_form += '</textarea><br />' # loop and create textareas for any missing software entries software = rekeywords_dict['SOFTWARE'] sublevel_required = [] sublevel_optional = [] if 'Sublevel' in software and software['Sublevel']: sublevel_required = software['Sublevel_required'] sublevel_optional = software['Sublevel_optional'] for _ in range(len(soft_list), software_entries): html_form += """ <textarea class='p80width' rows='6' name='software'>""" for sub_req in sublevel_required: html_form += '%s= # required\n' % sub_req for sub_opt in sublevel_optional: html_form += '%s= # optional\n' % sub_opt html_form += '</textarea><br />' if template and testprocedure_entry == 1: if 'TESTPROCEDURE' in template: html_form += """ <br /><b>Testprocedure</b> (in mRSL format):<br /> <textarea class='p80width' rows='15' name='testprocedure'>""" base64string = '' for stringpart in template['TESTPROCEDURE']: base64string += stringpart decodedstring = base64.decodestring(base64string) html_form += decodedstring html_form += '</textarea>' output_objects.append({ 'object_type': 'html_form', 'text': html_form }) html_form = """ <br /><b>Expected .stdout file if testprocedure is executed</b><br /> <textarea class='p80width' rows='10' name='verifystdout'>""" if 'VERIFYSTDOUT' in template: for line in template['VERIFYSTDOUT']: html_form += line html_form += '</textarea>' html_form += """ <br /><b>Expected .stderr file if testprocedure is executed</b><br /> <textarea cols='50' rows='10' name='verifystderr'>""" if 'VERIFYSTDERR' in template: for line in template['VERIFYSTDERR']: html_form += line html_form += '</textarea>' html_form += """ <br /><b>Expected .status file if testprocedure is executed</b><br /> <textarea cols='50' rows='10' name='verifystatus'>""" if 'VERIFYSTATUS' in template: for line in template['VERIFYSTATUS']: html_form += line html_form += '</textarea>' elif testprocedure_entry == 1: html_form += """ <br /><b>Testprocedure</b> (in mRSL format):<br /> <textarea class='p80width' rows='15' name='testprocedure'>""" html_form += \ """::EXECUTE:: ls </textarea> <br /><b>Expected .stdout file if testprocedure is executed</b><br /> <textarea class='p80width' rows='10' name='verifystdout'></textarea> <br /><b>Expected .stderr file if testprocedure is executed</b><br /> <textarea class='p80width' rows='10' name='verifystderr'></textarea> <br /><b>Expected .status file if testprocedure is executed</b><br /> <textarea class='p80width' rows='10' name='verifystatus'></textarea> """ environmentvariable = rekeywords_dict['ENVIRONMENTVARIABLE'] sublevel_required = [] sublevel_optional = [] if 'Sublevel' in environmentvariable\ and environmentvariable['Sublevel']: sublevel_required = environmentvariable['Sublevel_required'] sublevel_optional = environmentvariable['Sublevel_optional'] env_list = [] if environment_entries > 0: html_form += '<br /><b>Environments:</b><br />' if template: if 'ENVIRONMENTVARIABLE' in template: env_list = template['ENVIRONMENTVARIABLE'] for env in env_list: html_form += """ <textarea class='p80width' rows='4' name='environment'>""" for keyname in env.keys(): if keyname != '': html_form += '%s=%s\n' % (keyname, env[keyname]) html_form += '</textarea><br />' # loop and create textareas for any missing environment entries for _ in range(len(env_list), environment_entries): html_form += """ <textarea class='p80width' rows='4' name='environment'>""" for sub_req in sublevel_required: html_form += '%s= # required\n' % sub_req for sub_opt in sublevel_optional: html_form += '%s= # optional\n' % sub_opt html_form += '</textarea><br />' html_form += """<br /><br /><input type='submit' value='Create' /> </form> """ output_objects.append({ 'object_type': 'html_form', 'text': html_form % fill_helpers }) return (output_objects, returnvalues.OK)
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) defaults = signature()[1] title_entry = find_entry(output_objects, 'title') label = "%s" % configuration.site_vgrid_label title_entry['text'] = '%s send request' % configuration.short_title output_objects.append({ 'object_type': 'header', 'text': '%s send request' % configuration.short_title }) (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) target_id = client_id vgrid_name = accepted['vgrid_name'][-1].strip() visible_user_names = accepted['cert_id'] visible_res_names = accepted['unique_resource_name'] request_type = accepted['request_type'][-1].strip().lower() request_text = accepted['request_text'][-1].strip() protocols = [proto.strip() for proto in accepted['protocol']] use_any = False if any_protocol in protocols: use_any = True protocols = configuration.notify_protocols protocols = [proto.lower() for proto in protocols] 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) valid_request_types = [ 'resourceowner', 'resourceaccept', 'resourcereject', 'vgridowner', 'vgridmember', 'vgridresource', 'vgridaccept', 'vgridreject', 'plain' ] if not request_type in valid_request_types: output_objects.append({ 'object_type': 'error_text', 'text': '%s is not a valid request_type (valid types: %s)!' % (request_type.lower(), valid_request_types) }) return (output_objects, returnvalues.CLIENT_ERROR) if not protocols: output_objects.append({ 'object_type': 'error_text', 'text': 'No protocol specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) user_map = get_user_map(configuration) reply_to = user_map[client_id][USERID] # Try to point replies to client_id email client_email = extract_field(reply_to, 'email') if request_type == "plain": if not visible_user_names: output_objects.append({ 'object_type': 'error_text', 'text': 'No user ID specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) user_id = visible_user_names[-1].strip() anon_map = anon_to_real_user_map(configuration) if user_id in anon_map: user_id = anon_map[user_id] if user_id not in user_map: output_objects.append({ 'object_type': 'error_text', 'text': 'No such user: %s' % user_id }) return (output_objects, returnvalues.CLIENT_ERROR) target_name = user_id user_dict = user_map[user_id] vgrid_access = user_vgrid_access(configuration, client_id) vgrids_allow_email = user_dict[CONF].get('VGRIDS_ALLOW_EMAIL', []) vgrids_allow_im = user_dict[CONF].get('VGRIDS_ALLOW_IM', []) if any_vgrid in vgrids_allow_email: email_vgrids = vgrid_access else: email_vgrids = set(vgrids_allow_email).intersection(vgrid_access) if any_vgrid in vgrids_allow_im: im_vgrids = vgrid_access else: im_vgrids = set(vgrids_allow_im).intersection(vgrid_access) if use_any: # Do not try disabled protocols if ANY was requested if not email_vgrids: protocols = [ proto for proto in protocols if proto not in email_keyword_list ] if not im_vgrids: protocols = [ proto for proto in protocols if proto in email_keyword_list ] if not email_vgrids and [ proto for proto in protocols if proto in email_keyword_list ]: output_objects.append({ 'object_type': 'error_text', 'text': 'You are not allowed to send emails to %s!' % user_id }) return (output_objects, returnvalues.CLIENT_ERROR) if not im_vgrids and [ proto for proto in protocols if proto not in email_keyword_list ]: output_objects.append({ 'object_type': 'error_text', 'text': 'You are not allowed to send instant messages to %s!' % user_id }) return (output_objects, returnvalues.CLIENT_ERROR) for proto in protocols: if not user_dict[CONF].get(proto.upper(), False): if use_any: # Remove missing protocols if ANY protocol was requested protocols = [i for i in protocols if i != proto] else: output_objects.append({ 'object_type': 'error_text', 'text': 'User %s does not accept %s messages!' % (user_id, proto) }) return (output_objects, returnvalues.CLIENT_ERROR) if not protocols: output_objects.append({ 'object_type': 'error_text', 'text': 'User %s does not accept requested protocol(s) messages!' % user_id }) return (output_objects, returnvalues.CLIENT_ERROR) target_list = [user_id] elif request_type in ["vgridaccept", "vgridreject"]: # Always allow accept messages but only between owners/members if not visible_user_names and not visible_res_names: output_objects.append({ 'object_type': 'error_text', 'text': 'No user or resource ID specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) if not vgrid_name: output_objects.append({ 'object_type': 'error_text', 'text': 'No vgrid_name specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) if vgrid_name.upper() == default_vgrid.upper(): output_objects.append({ 'object_type': 'error_text', 'text': 'No requests for %s are allowed!' % default_vgrid }) return (output_objects, returnvalues.CLIENT_ERROR) if not vgrid_is_owner(vgrid_name, client_id, configuration): output_objects.append({ 'object_type': 'error_text', 'text': 'You are not an owner of %s or a parent %s!' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: we support exactly one vgrid but multiple users/resources here if visible_user_names: logger.info("setting user recipients: %s" % visible_user_names) target_list = [user_id.strip() for user_id in visible_user_names] elif visible_res_names: # vgrid resource accept - lookup and notify resource owners logger.info("setting res owner recipients: %s" % visible_res_names) target_list = [] for unique_resource_name in visible_res_names: logger.info("loading res owners for %s" % unique_resource_name) (load_status, res_owners) = resource_owners(configuration, unique_resource_name) if not load_status: output_objects.append({ 'object_type': 'error_text', 'text': 'Could not lookup owners of %s!' % unique_resource_name }) continue logger.info("adding res owners to recipients: %s" % res_owners) target_list += [user_id for user_id in res_owners] target_id = '%s %s owners' % (vgrid_name, label) target_name = vgrid_name elif request_type in ["resourceaccept", "resourcereject"]: # Always allow accept messages between actual resource owners if not visible_user_names: output_objects.append({ 'object_type': 'error_text', 'text': 'No user ID specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) if not visible_res_names: output_objects.append({ 'object_type': 'error_text', 'text': 'No resource ID specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: we support exactly one resource but multiple users here unique_resource_name = visible_res_names[-1].strip() target_name = unique_resource_name res_map = get_resource_map(configuration) if unique_resource_name not in res_map: output_objects.append({ 'object_type': 'error_text', 'text': 'No such resource: %s' % unique_resource_name }) return (output_objects, returnvalues.CLIENT_ERROR) owners_list = res_map[unique_resource_name][OWNERS] if not client_id in owners_list: output_objects.append({ 'object_type': 'error_text', 'text': 'You are not an owner of %s!' % unique_resource_name }) output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid resource %s message!' % request_type }) return (output_objects, returnvalues.CLIENT_ERROR) target_id = '%s resource owners' % unique_resource_name target_name = unique_resource_name target_list = [user_id.strip() for user_id in visible_user_names] elif request_type == "resourceowner": if not visible_res_names: output_objects.append({ 'object_type': 'error_text', 'text': 'No resource ID specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: we support exactly one resource but multiple users here unique_resource_name = visible_res_names[-1].strip() anon_map = anon_to_real_res_map(configuration.resource_home) if unique_resource_name in anon_map: unique_resource_name = anon_map[unique_resource_name] target_name = unique_resource_name res_map = get_resource_map(configuration) if unique_resource_name not in res_map: output_objects.append({ 'object_type': 'error_text', 'text': 'No such resource: %s' % unique_resource_name }) return (output_objects, returnvalues.CLIENT_ERROR) target_list = res_map[unique_resource_name][OWNERS] if client_id in target_list: output_objects.append({ 'object_type': 'error_text', 'text': 'You are already an owner of %s!' % unique_resource_name }) return (output_objects, returnvalues.CLIENT_ERROR) request_dir = os.path.join(configuration.resource_home, unique_resource_name) access_request = { 'request_type': request_type, 'entity': client_id, 'target': unique_resource_name, 'request_text': request_text } if not save_access_request(configuration, request_dir, access_request): output_objects.append({ 'object_type': 'error_text', 'text': 'Could not save request - owners may still manually add you' }) return (output_objects, returnvalues.SYSTEM_ERROR) elif request_type in ["vgridmember", "vgridowner", "vgridresource"]: if not vgrid_name: output_objects.append({ 'object_type': 'error_text', 'text': 'No vgrid_name specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) # default vgrid is read-only if vgrid_name.upper() == default_vgrid.upper(): output_objects.append({ 'object_type': 'error_text', 'text': 'No requests for %s are not allowed!' % default_vgrid }) return (output_objects, returnvalues.CLIENT_ERROR) # stop owner or member request if already an owner # and prevent repeated resource access requests if request_type == 'vgridresource': # NOTE: we support exactly one resource here unique_resource_name = visible_res_names[-1].strip() target_id = entity = unique_resource_name if vgrid_is_resource(vgrid_name, unique_resource_name, configuration): output_objects.append({ 'object_type': 'error_text', 'text': 'You already have access to %s or a parent %s.' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) else: target_id = entity = client_id if vgrid_is_owner(vgrid_name, client_id, configuration): output_objects.append({ 'object_type': 'error_text', 'text': 'You are already an owner of %s or a parent %s!' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) # only ownership requests are allowed for existing members if request_type == 'vgridmember': if vgrid_is_member(vgrid_name, client_id, configuration): output_objects.append({ 'object_type': 'error_text', 'text': 'You are already a member of %s or a parent %s.' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) # Find all VGrid owners configured to receive notifications target_name = vgrid_name (settings_status, settings_dict) = vgrid_settings(vgrid_name, configuration, recursive=True, as_dict=True) if not settings_status: settings_dict = {} request_recipients = settings_dict.get('request_recipients', default_vgrid_settings_limit) # We load and use direct owners first if any - otherwise inherited owners_list = [] for inherited in (False, True): (owners_status, owners_list) = vgrid_owners(vgrid_name, configuration, recursive=inherited) if not owners_status: output_objects.append({ 'object_type': 'error_text', 'text': 'Failed to lookup owners for %s %s - sure it exists?' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) elif owners_list: break if not owners_list: output_objects.append({ 'object_type': 'error_text', 'text': 'Failed to lookup owners for %s %s - sure it exists?' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) # Now we have direct or inherited owners to notify target_list = owners_list[:request_recipients] request_dir = os.path.join(configuration.vgrid_home, vgrid_name) access_request = { 'request_type': request_type, 'entity': entity, 'target': vgrid_name, 'request_text': request_text } if not save_access_request(configuration, request_dir, access_request): output_objects.append({ 'object_type': 'error_text', 'text': 'Could not save request - owners may still manually add you' }) return (output_objects, returnvalues.SYSTEM_ERROR) else: output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid request type: %s' % request_type }) return (output_objects, returnvalues.CLIENT_ERROR) # Now send request to all targets in turn # TODO: inform requestor if no owners have mail/IM set in their settings logger.debug("sending notification to recipients: %s" % target_list) for target in target_list: if not target: logger.warning("skipping empty notify target: %s" % target_list) continue # USER_CERT entry is destination notify = [] for proto in protocols: notify.append('%s: SETTINGS' % proto) job_dict = { 'NOTIFY': notify, 'JOB_ID': 'NOJOBID', 'USER_CERT': target, 'EMAIL_SENDER': client_email } notifier = notify_user_thread( job_dict, [target_id, target_name, request_type, request_text, reply_to], 'SENDREQUEST', logger, '', configuration, ) # Try finishing delivery but do not block forever on one message notifier.join(30) output_objects.append({ 'object_type': 'text', 'text': 'Sent %s message to %d people' % (request_type, len(target_list)) }) im_notify_protocols = [ i for i in configuration.notify_protocols if i != 'email' ] if im_notify_protocols: enabled_notify = 'IM / email' else: enabled_notify = 'email' output_objects.append({ 'object_type': 'text', 'text': """Please make sure you have %s notifications configured on your Setings page if you expect a reply to this message""" % enabled_notify }) 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, 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, # NOTE: path can use wildcards typecheck_overrides={'path': valid_path_pattern}, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) flags = ''.join(accepted['flags']) pattern_list = accepted['path'] iosessionid = accepted['iosessionid'][-1] 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 File' if force(flags): rm_helper = delete_path else: rm_helper = remove_path userstyle = True widgets = True elif share_id: try: (share_mode, _) = extract_mode_id(configuration, share_id) except ValueError as 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 File' rm_helper = delete_path userstyle = False widgets = False elif iosessionid.strip() and iosessionid.isalnum(): user_id = iosessionid base_dir = configuration.webserver_home target_dir = iosessionid page_title = 'Remove Session File' rm_helper = delete_path userstyle = False widgets = False else: logger.error('%s called without proper auth: %s' % (op_name, accepted)) output_objects.append({ 'object_type': 'error_text', 'text': 'Authentication is missing!' }) return (output_objects, returnvalues.SYSTEM_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(base_dir, target_dir)) + os.sep title_entry = find_entry(output_objects, 'title') title_entry['text'] = page_title title_entry['skipwidgets'] = not widgets title_entry['skipuserstyle'] = not userstyle output_objects.append({'object_type': 'header', 'text': page_title}) logger.debug("%s: with paths: %s" % (op_name, pattern_list)) # Input validation assures target_dir can't escape base_dir if not os.path.isdir(base_dir): output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid client/sharelink/session id!' }) logger.warning('%s used %s with invalid base dir: %s' % (user_id, op_name, base_dir)) return (output_objects, returnvalues.CLIENT_ERROR) if verbose(flags): for flag in flags: output_objects.append({ 'object_type': 'text', 'text': '%s using flag: %s' % (op_name, flag) }) 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: # 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 match.append(abs_path) # Now actually treat list of allowed matchings and notify if no # (allowed) match if not match: logger.warning("%s: no matching paths: %s" % (op_name, pattern_list)) output_objects.append({ 'object_type': 'file_not_found', 'name': pattern }) status = returnvalues.FILE_NOT_FOUND for abs_path in match: real_path = os.path.realpath(abs_path) relative_path = abs_path.replace(base_dir, '') if verbose(flags): output_objects.append({ 'object_type': 'file', 'name': relative_path }) # Make it harder to accidentially delete too much - e.g. do not # delete VGrid files without explicit selection of subdir contents if abs_path == os.path.abspath(base_dir): logger.error("%s: refusing rm home dir: %s" % (op_name, abs_path)) output_objects.append({ 'object_type': 'warning', 'text': "You're not allowed to delete your entire home directory!" }) status = returnvalues.CLIENT_ERROR continue # Generally refuse handling symlinks including root vgrid shares elif os.path.islink(abs_path): logger.error("%s: refusing rm link: %s" % (op_name, abs_path)) output_objects.append({ 'object_type': 'warning', 'text': """ You're not allowed to delete entire special folders like %s shares and %s """ % (configuration.site_vgrid_label, trash_linkname) }) 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 remove entire %s shared folders!""" % configuration.site_vgrid_label }) status = returnvalues.CLIENT_ERROR continue elif os.path.isdir(abs_path) and not recursive(flags): logger.error("%s: non-recursive call on dir '%s'" % (op_name, abs_path)) output_objects.append({ 'object_type': 'error_text', 'text': "cannot remove '%s': is a direcory" % relative_path }) status = returnvalues.CLIENT_ERROR continue trash_base = get_trash_location(configuration, abs_path) if not trash_base and not force(flags): logger.error("%s: no trash for dir '%s'" % (op_name, abs_path)) output_objects.append({ 'object_type': 'error_text', 'text': "No trash enabled for '%s' - read-only?" % relative_path }) status = returnvalues.CLIENT_ERROR continue try: if rm_helper == remove_path and \ os.path.commonprefix([real_path, trash_base]) \ == trash_base: logger.warning("%s: already in trash: '%s'" % (op_name, real_path)) output_objects.append({ 'object_type': 'error_text', 'text': """ '%s' is already in trash - no action: use force flag to permanently delete""" % relative_path }) status = returnvalues.CLIENT_ERROR continue except Exception as err: logger.error("%s: check trash failed: %s" % (op_name, err)) 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 remove "%s": inside a read-only location!' % pattern }) status = returnvalues.CLIENT_ERROR continue # TODO: limit delete in vgrid share trash to vgrid owners / conf? # ... malicious members can still e.g. truncate all files. # we could consider removing write bit on move to trash. # TODO: user setting to switch on/off trash? # TODO: add direct delete checkbox in fileman move to trash dialog? # TODO: add empty trash option for Trash? # TODO: user settings to define read-only and auto-expire in trash? # TODO: add trash support for sftp/ftps/webdavs? gdp_iolog_action = 'deleted' gdp_iolog_paths = [relative_path] if rm_helper == remove_path: gdp_iolog_action = 'moved' trash_base_path = \ get_trash_location(configuration, abs_path, True) trash_relative_path = \ trash_base_path.replace(configuration.user_home, '') trash_relative_path = \ trash_relative_path.replace( configuration.vgrid_files_home, '') gdp_iolog_paths.append(trash_relative_path) try: gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'], gdp_iolog_action, gdp_iolog_paths) gdp_iolog_status = True except GDPIOLogError as exc: gdp_iolog_status = False rm_err = [str(exc)] rm_status = False if gdp_iolog_status: (rm_status, rm_err) = rm_helper(configuration, abs_path) if not rm_status or not gdp_iolog_status: if gdp_iolog_status: gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'], gdp_iolog_action, gdp_iolog_paths, failed=True, details=rm_err) logger.error("%s: failed on '%s': %s" % (op_name, abs_path, ', '.join(rm_err))) output_objects.append({ 'object_type': 'error_text', 'text': "remove '%s' failed: %s" % (relative_path, '. '.join(rm_err)) }) status = returnvalues.SYSTEM_ERROR continue logger.info("%s: successfully (re)moved %s" % (op_name, abs_path)) output_objects.append({ 'object_type': 'text', 'text': "removed %s" % (relative_path) }) output_objects.append({ 'object_type': 'link', 'destination': 'ls.py%s' % id_query, 'text': 'Return to files overview' }) 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) defaults = signature()[1] status = returnvalues.OK output_objects.append({ 'object_type': 'header', 'text': 'Add Resource Owner(s)' }) (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) unique_resource_name = accepted['unique_resource_name'][-1].strip() cert_id_list = accepted['cert_id'] request_name = unhexlify(accepted['request_name'][-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) if not is_owner(client_id, unique_resource_name, configuration.resource_home, logger): output_objects.append({ 'object_type': 'error_text', 'text': 'You must be an owner of %s to add a new owner!' % unique_resource_name }) return (output_objects, returnvalues.CLIENT_ERROR) # is_owner incorporates unique_resource_name verification - no need to # specifically check for illegal directory traversal cert_id_added = [] for cert_id in cert_id_list: cert_id = cert_id.strip() if not cert_id: continue if not is_user(cert_id, configuration.mig_server_home): output_objects.append({ 'object_type': 'error_text', 'text': '%s is not a valid %s user!' % (cert_id, configuration.short_title) }) status = returnvalues.CLIENT_ERROR continue # don't add if already an owner if resource_is_owner(unique_resource_name, cert_id, configuration): output_objects.append({ 'object_type': 'error_text', 'text': '%s is already an owner of %s.' % (cert_id, unique_resource_name) }) status = returnvalues.CLIENT_ERROR continue # Add owner (add_status, add_msg) = resource_add_owners(configuration, unique_resource_name, [cert_id]) if not add_status: output_objects.append({ 'object_type': 'error_text', 'text': 'Could not add new owner, reason: %s' % add_msg }) status = returnvalues.SYSTEM_ERROR continue cert_id_added.append(cert_id) if request_name: request_dir = os.path.join(configuration.resource_home, unique_resource_name) if not delete_access_request(configuration, request_dir, request_name): logger.error("failed to delete owner request for %s in %s" % \ (unique_resource_name, request_name)) output_objects.append({ 'object_type': 'error_text', 'text': 'Failed to remove saved request for %s in %s!' % \ (unique_resource_name, request_name)}) if cert_id_added: output_objects.append({ 'object_type': 'html_form', 'text': 'New owner(s)<br/>%s<br/>successfully added to %s!' % ('<br />'.join(cert_id_added), unique_resource_name) }) cert_id_fields = '' for cert_id in cert_id_added: cert_id_fields += """<input type=hidden name=cert_id value='%s' /> """ % cert_id form_method = 'post' csrf_limit = get_csrf_limit(configuration) fill_helpers = { 'res_id': unique_resource_name, 'cert_id_fields': cert_id_fields, 'any_protocol': any_protocol, 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit } target_op = 'sendrequestaction' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) fill_helpers.update({'target_op': target_op, 'csrf_token': csrf_token}) output_objects.append({ 'object_type': 'html_form', 'text': """ <form method='%(form_method)s' action='%(target_op)s.py'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <input type=hidden name=request_type value='resourceaccept' /> <input type=hidden name=unique_resource_name value='%(res_id)s' /> %(cert_id_fields)s <input type=hidden name=protocol value='%(any_protocol)s' /> <table> <tr> <td class='title'>Custom message to user(s)</td> </tr> <tr> <td><textarea name=request_text cols=72 rows=10> We have granted you ownership access to our %(res_id)s resource. You can access the resource administration page from the Resources page. Regards, the %(res_id)s resource owners </textarea></td> </tr> <tr> <td><input type='submit' value='Inform user(s)' /></td> </tr> </table> </form> <br /> """ % fill_helpers }) output_objects.append({'object_type': 'link', 'destination': 'resadmin.py?unique_resource_name=%s' % \ unique_resource_name, 'class': 'adminlink iconspace', 'title': 'Administrate resource', 'text': 'Manage resource'}) return (output_objects, status)
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, op_menu=False) logger = configuration.logger logger.info('%s: args: %s' % (op_name, user_arguments_dict)) prefilter_map = {} output_objects.append({'object_type': 'header', 'text': 'Automatic %s sign up' % configuration.short_title}) (auth_type, auth_flavor) = detect_client_auth(configuration, environ) identity = extract_client_id(configuration, environ, lookup_dn=False) if client_id and auth_type == AUTH_CERTIFICATE: if auth_flavor == AUTH_MIG_CERT: base_url = configuration.migserver_https_mig_cert_url elif auth_flavor == AUTH_EXT_CERT: base_url = configuration.migserver_https_ext_cert_url else: logger.warning('no matching sign up auth flavor %s' % auth_flavor) output_objects.append({'object_type': 'error_text', 'text': '%s sign up not supported' % auth_flavor}) return (output_objects, returnvalues.SYSTEM_ERROR) elif identity and auth_type == AUTH_OPENID_V2: if auth_flavor == AUTH_MIG_OID: base_url = configuration.migserver_https_mig_oid_url elif auth_flavor == AUTH_EXT_OID: base_url = configuration.migserver_https_ext_oid_url else: logger.warning('no matching sign up auth flavor %s' % auth_flavor) output_objects.append({'object_type': 'error_text', 'text': '%s sign up not supported' % auth_flavor}) return (output_objects, returnvalues.SYSTEM_ERROR) for name in ('openid.sreg.cn', 'openid.sreg.fullname', 'openid.sreg.full_name'): prefilter_map[name] = filter_commonname elif identity and auth_type == AUTH_OPENID_CONNECT: if auth_flavor == AUTH_MIG_OIDC: base_url = configuration.migserver_https_mig_oidc_url elif auth_flavor == AUTH_EXT_OIDC: base_url = configuration.migserver_https_ext_oidc_url else: logger.warning('no matching sign up auth flavor %s' % auth_flavor) output_objects.append({'object_type': 'error_text', 'text': '%s sign up not supported' % auth_flavor}) return (output_objects, returnvalues.SYSTEM_ERROR) oidc_keys = signature(AUTH_OPENID_CONNECT)[1].keys() # NOTE: again we lowercase to avoid case sensitivity in validation for key in environ: low_key = key.replace('OIDC_CLAIM_', 'oidc.claim.').lower() if low_key in oidc_keys: user_arguments_dict[low_key] = [environ[key]] else: logger.error('autocreate without ID rejected for %s' % client_id) output_objects.append({'object_type': 'error_text', 'text': 'Missing user credentials'}) return (output_objects, returnvalues.CLIENT_ERROR) defaults = signature(auth_type)[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False, prefilter_map=prefilter_map) if not validate_status: logger.warning('%s from %s got invalid input: %s' % (op_name, client_id, accepted)) return (accepted, returnvalues.CLIENT_ERROR) logger.debug('Accepted arguments: %s' % accepted) # logger.debug('with environ: %s' % environ) admin_email = configuration.admin_email smtp_server = configuration.smtp_server (openid_names, oid_extras) = ([], {}) tmp_id = 'tmp%s' % time.time() logger.info('Received autocreate from %s with ID %s' % (client_id, tmp_id)) # Extract raw values if auth_type == AUTH_CERTIFICATE: uniq_id = accepted['cert_id'][-1].strip() raw_name = accepted['cert_name'][-1].strip() country = accepted['country'][-1].strip() state = accepted['state'][-1].strip() org = accepted['org'][-1].strip() org_unit = '' # NOTE: leave role and association alone here role = '' association = '' locality = '' timezone = '' email = accepted['email'][-1].strip() elif auth_type == AUTH_OPENID_V2: uniq_id = accepted['openid.sreg.nickname'][-1].strip() \ or accepted['openid.sreg.short_id'][-1].strip() raw_name = accepted['openid.sreg.fullname'][-1].strip() \ or accepted['openid.sreg.full_name'][-1].strip() country = accepted['openid.sreg.country'][-1].strip() state = accepted['openid.sreg.state'][-1].strip() org = accepted['openid.sreg.o'][-1].strip() \ or accepted['openid.sreg.organization'][-1].strip() org_unit = accepted['openid.sreg.ou'][-1].strip() \ or accepted['openid.sreg.organizational_unit'][-1].strip() # We may receive multiple roles and associations role = ','.join([i for i in accepted['openid.sreg.role'] if i]) association = ','.join([i for i in accepted['openid.sreg.association'] if i]) locality = accepted['openid.sreg.locality'][-1].strip() timezone = accepted['openid.sreg.timezone'][-1].strip() # We may encounter results without an email, fall back to uniq_id then email = accepted['openid.sreg.email'][-1].strip() or uniq_id elif auth_type == AUTH_OPENID_CONNECT: uniq_id = accepted['oidc.claim.upn'][-1].strip() \ or accepted['oidc.claim.sub'][-1].strip() raw_name = accepted['oidc.claim.fullname'][-1].strip() country = accepted['oidc.claim.country'][-1].strip() state = accepted['oidc.claim.state'][-1].strip() org = accepted['oidc.claim.o'][-1].strip() \ or accepted['oidc.claim.organization'][-1].strip() org_unit = accepted['oidc.claim.ou'][-1].strip() \ or accepted['oidc.claim.organizational_unit'][-1].strip() # We may receive multiple roles and associations role = ','.join([i for i in accepted['oidc.claim.role'] if i]) association = ','.join([i for i in accepted['oidc.claim.association'] if i]) locality = accepted['oidc.claim.locality'][-1].strip() timezone = accepted['oidc.claim.timezone'][-1].strip() # We may encounter results without an email, fall back to uniq_id then email = accepted['oidc.claim.email'][-1].strip() or uniq_id # TODO: switch to canonical_user fra mig.shared.base instead? # Fix case of values: # force name to capitalized form (henrik karlsen -> Henrik Karlsen) # please note that we get utf8 coded bytes here and title() treats such # chars as word termination. Temporarily force to unicode. try: full_name = force_utf8(force_unicode(raw_name).title()) except Exception: logger.warning('could not use unicode form to capitalize full name' ) full_name = raw_name.title() country = country.upper() state = state.upper() email = email.lower() accept_terms = (accepted['accept_terms'][-1].strip().lower() in ('1', 'o', 'y', 't', 'on', 'yes', 'true')) if auth_type in (AUTH_OPENID_V2, AUTH_OPENID_CONNECT): # KU OpenID sign up does not deliver accept_terms so we implicitly # let it imply acceptance for now accept_terms = True # Remap some oid attributes if on KIT format with faculty in # organization and institute in organizational_unit. We can add them # as different fields as long as we make sure the x509 fields are # preserved. # Additionally in the special case with unknown institute (ou=ukendt) # we force organization to KU to align with cert policies. # We do that to allow autocreate updating existing cert users. if org_unit not in ('', 'NA'): org_unit = org_unit.upper() oid_extras['faculty'] = org oid_extras['institute'] = org_unit org = org_unit.upper() org_unit = 'NA' if org == 'UKENDT': org = 'KU' logger.info('unknown affilition, set organization to %s' % org) # Stay on virtual host - extra useful while we test dual OpenID base_url = environ.get('REQUEST_URI', base_url).split('?')[0] backend = 'home.py' if configuration.site_enable_gdp: backend = 'gdpman.py' elif configuration.site_autolaunch_page: backend = os.path.basename(configuration.site_autolaunch_page) elif configuration.site_landing_page: backend = os.path.basename(configuration.site_landing_page) base_url = base_url.replace('autocreate.py', backend) raw_login = '' if auth_type == AUTH_OPENID_V2: # OpenID 2.0 provides user ID on URL format - only add plain ID for oid_provider in configuration.user_openid_providers: openid_prefix = oid_provider.rstrip('/') + '/' if identity.startswith(openid_prefix): raw_login = identity.replace(openid_prefix, '') break elif auth_type == AUTH_OPENID_CONNECT: raw_login = identity if raw_login and not raw_login in openid_names: openid_names.append(raw_login) if email and not email in openid_names: openid_names.append(email) # TODO: Add additional ext oid/oidc provider ID aliases here? # we should have the proxy file read... proxy_content = accepted['proxy_upload'][-1] # keep comment to a single line comment = accepted['comment'][-1].replace('\n', ' ') # single quotes break command line format - remove comment = comment.replace("'", ' ') # TODO: improve and enforce full authsig from extoid/extoidc provider authsig_list = accepted.get('authsig', []) # if len(authsig_list) != 1: # logger.warning('%s from %s got invalid authsig: %s' % # (op_name, client_id, authsig_list)) user_dict = { 'short_id': uniq_id, 'full_name': full_name, 'organization': org, 'organizational_unit': org_unit, 'locality': locality, 'state': state, 'country': country, 'email': email, 'role': role, 'association': association, 'timezone': timezone, 'password': '', 'comment': 'Signed up through autocreate with %s' % auth_type, 'openid_names': openid_names, } user_dict.update(oid_extras) # We must receive some ID from the provider otherwise we probably hit the # already logged in situation and must autologout first if not uniq_id and not email: if auth_type == AUTH_OPENID_V2 and identity and \ accepted.get('openid.sreg.required', ''): logger.warning('autocreate forcing autologut for %s' % client_id) output_objects.append({'object_type': 'html_form', 'text': '''<p class="spinner iconleftpad"> Auto log out first to avoid sign up problems ... </p>'''}) req_url = environ['SCRIPT_URI'] html = \ """ <a id='autologout' href='%s'></a> <script type='text/javascript'> document.getElementById('autologout').click(); </script>""" \ % openid_autologout_url(configuration, identity, client_id, req_url, user_arguments_dict) output_objects.append({'object_type': 'html_form', 'text': html}) else: logger.warning('%s autocreate without ID refused for %s' % (auth_type, client_id)) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: Unfortunately external OpenID 2.0 redirect does not enforce POST # Extract helper environments from Apache to verify request authenticity redirector = environ.get('HTTP_REFERER', '') extoid_prefix = configuration.user_ext_oid_provider.replace('id/', '') # TODO: extend redirector check to match the full signup request? # may not work with recent browser policy changes to limit referrer # details on cross site requests. # NOTE: redirector check breaks for FF default policy so disabled again! if auth_flavor == AUTH_EXT_OID and redirector and \ not redirector.startswith(extoid_prefix) and \ not redirector.startswith(configuration.migserver_https_sid_url) \ and not redirector.startswith(configuration.migserver_http_url) \ and not redirector.startswith(get_site_base_url(configuration)): logger.error('stray %s autocreate rejected for %r (ref: %r)' % (auth_flavor, client_id, redirector)) output_objects.append({'object_type': 'error_text', 'text': '''Only accepting authentic requests through %s OpenID 2.0''' % configuration.user_ext_oid_title}) return (output_objects, returnvalues.CLIENT_ERROR) elif auth_flavor != AUTH_EXT_OID and not safe_handler( configuration, 'post', op_name, client_id, get_csrf_limit(configuration), accepted): logger.error('unsafe %s autocreate rejected for %s' % (auth_flavor, client_id)) output_objects.append({'object_type': 'error_text', 'text': '''Only accepting CSRF-filtered POST requests to prevent unintended updates'''}) return (output_objects, returnvalues.CLIENT_ERROR) if auth_flavor == AUTH_EXT_CERT: ext_login_title = "%s certificate" % configuration.user_ext_cert_title personal_page_url = configuration.migserver_https_ext_cert_url # TODO: consider limiting expire to real cert expire if before default? user_dict['expire'] = default_account_expire(configuration, AUTH_CERTIFICATE) try: distinguished_name_to_user(uniq_id) user_dict['distinguished_name'] = uniq_id except: logger.error('%s autocreate with bad DN refused for %s' % (auth_flavor, client_id)) output_objects.append({'object_type': 'error_text', 'text': '''Illegal Distinguished name: Please note that the distinguished name must be a valid certificate DN with multiple "key=val" fields separated by "/". '''}) return (output_objects, returnvalues.CLIENT_ERROR) elif auth_flavor == AUTH_EXT_OID: ext_login_title = "%s login" % configuration.user_ext_oid_title personal_page_url = configuration.migserver_https_ext_oid_url user_dict['expire'] = default_account_expire(configuration, AUTH_OPENID_V2) fill_distinguished_name(user_dict) uniq_id = user_dict['distinguished_name'] elif auth_flavor == AUTH_EXT_OIDC: ext_login_title = "%s login" % configuration.user_ext_oid_title personal_page_url = configuration.migserver_https_ext_oidc_url user_dict['expire'] = default_account_expire(configuration, AUTH_OPENID_CONNECT) fill_distinguished_name(user_dict) uniq_id = user_dict['distinguished_name'] else: # Reject the migX sign up methods through this handler logger.error('%s autocreate not supported for %s - only ext auth' % (auth_flavor, client_id)) output_objects.append({'object_type': 'error_text', 'text': ''' Unsuported %s sign up method - you should sign up through the official sign up wrappers or go through the dedicated web form for %s.''' % (auth_type, auth_flavor)}) return (output_objects, returnvalues.CLIENT_ERROR) # IMPORTANT: do NOT let a user create with ID different from client_id if auth_type == AUTH_CERTIFICATE and client_id != uniq_id: logger.error('refusing autocreate invalid user for %s: %s' % (client_id, user_dict)) output_objects.append({'object_type': 'error_text', 'text': '''Only accepting create matching supplied ID!'''}) return (output_objects, returnvalues.CLIENT_ERROR) if not accept_terms: output_objects.append({'object_type': 'error_text', 'text': 'You must accept the terms of use in sign up!'}) output_objects.append( {'object_type': 'link', 'destination': 'javascript:history.back();', 'class': 'genericbutton', 'text': "Try again"}) return (output_objects, returnvalues.CLIENT_ERROR) # Save auth access method user_dict['auth'] = user_dict.get('auth', None) if not user_dict['auth']: user_dict['auth'] = [] elif isinstance(user_dict['auth'], basestring): user_dict['auth'] = [user_dict['auth']] user_dict['auth'].append(auth_flavor) fill_helper = {'short_title': configuration.short_title, 'base_url': base_url, 'admin_email': admin_email, 'ext_login_title': ext_login_title, 'front_page_url': get_site_base_url(configuration), 'personal_page_url': personal_page_url} fill_helper.update(user_dict) # If server allows automatic addition of users with a CA validated cert # we create the user immediately and skip mail if auth_type == AUTH_CERTIFICATE and configuration.auto_add_cert_user \ or auth_type == AUTH_OPENID_V2 and \ configuration.auto_add_oid_user \ or auth_type == AUTH_OPENID_CONNECT and \ configuration.auto_add_oid_user: fill_user(user_dict) logger.info('create user: %s' % user_dict) # Now all user fields are set and we can begin adding the user db_path = os.path.join(configuration.mig_server_home, user_db_filename) try: create_user(user_dict, configuration.config_file, db_path, ask_renew=False, default_renew=True) if configuration.site_enable_griddk \ and accepted['proxy_upload'] != ['']: # save the file, display expiration date proxy_out = handle_proxy(proxy_content, uniq_id, configuration) output_objects.extend(proxy_out) except Exception as err: logger.error('create failed for %s: %s' % (uniq_id, err)) output_objects.append({'object_type': 'error_text', 'text': ''' Could not create the user account for you: Please report this problem to the site administrators (%(admin_email)s).''' % fill_helper}) return (output_objects, returnvalues.SYSTEM_ERROR) logger.info('created user account for %s' % uniq_id) email_header = 'Welcome to %s' % configuration.short_title email_msg = """Hi and welcome to %(short_title)s! Your account sign up succeeded and you can now log in to your account using your %(ext_login_title)s from %(front_page_url)s There you'll also find further information about making the most of %(short_title)s, including a user guide and answers to Frequently Asked Questions, plus site status and support information. You're welcome to contact us with questions or comments using the contact details there and in the footer of your personal %(short_title)s pages. Please note that by signing up and using %(short_title)s you also formally accept the site Terms of Use, which you'll always find in the current form at %(front_page_url)s/terms.html All the best, The %(short_title)s Admins """ % fill_helper logger.info('Send email: to: %s, header: %s, msg: %s, smtp_server: %s' % (email, email_header, email_msg, smtp_server)) if not send_email(email, email_header, email_msg, logger, configuration): output_objects.append({ 'object_type': 'error_text', 'text': """An error occured trying to send your account welcome email. Please inform the site admins (%s) manually and include the session ID: %s""" % (admin_email, tmp_id)}) return (output_objects, returnvalues.SYSTEM_ERROR) logger.info('sent welcome email for %s to %s' % (uniq_id, email)) output_objects.append({'object_type': 'html_form', 'text': """ <p>Creating your %(short_title)s user account and sending welcome email ... </p> <p class='spinner iconleftpad'> redirecting to your <a href='%(personal_page_url)s'> personal pages </a> in a moment. </p> <script type='text/javascript'> setTimeout(function() {location.href='%(personal_page_url)s';}, 3000); </script> """ % fill_helper}) return (output_objects, returnvalues.OK) else: logger.warning('autocreate disabled and refused for %s' % client_id) output_objects.append({ 'object_type': 'error_text', 'text': """Automatic user creation disabled on this site. Please contact the site admins (%(admin_email)s) if you think it should be enabled. """ % fill_helper}) return (output_objects, returnvalues.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) status = returnvalues.OK defaults = signature()[1] title_entry = find_entry(output_objects, 'title') title_entry['text'] = 'Resource management' (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) show_sandboxes = (accepted['show_sandboxes'][-1] != 'false') operation = accepted['operation'][-1] caching = (accepted['caching'][-1].lower() in ('true', 'yes')) if not configuration.site_enable_resources: output_objects.append({'object_type': 'error_text', 'text': '''Resources are not enabled on this system'''}) return (output_objects, returnvalues.SYSTEM_ERROR) if not operation in allowed_operations: output_objects.append({'object_type': 'text', 'text': '''Operation must be one of %s.''' % ', '.join(allowed_operations)}) return (output_objects, returnvalues.OK) logger.info("%s %s begin for %s" % (op_name, operation, client_id)) pending_updates = False if operation in show_operations: # jquery support for tablesorter and confirmation on delete # table initially sorted by col. 1 (admin), then 0 (name) # NOTE: We distinguish between caching on page load and forced refresh refresh_call = 'ajax_resman(%s)' table_spec = {'table_id': 'resourcetable', 'sort_order': '[[1,0],[0,0]]', 'refresh_call': refresh_call % 'false'} (add_import, add_init, add_ready) = man_base_js(configuration, [table_spec]) if operation == "show": add_ready += '%s;' % refresh_call % 'true' 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)}) output_objects.append({'object_type': 'header', 'text': 'Available Resources'}) output_objects.append({'object_type': 'sectionheader', 'text': 'Resources available on this server'}) output_objects.append({'object_type': 'text', 'text': ''' All available resources are listed below with overall hardware specifications. Any resources that you own will have a administration icon that you can click to open resource management. ''' }) # Helper forms for requests and removes form_method = 'post' csrf_limit = get_csrf_limit(configuration) target_op = 'sendrequestaction' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) helper = html_post_helper('reqresowner', '%s.py' % target_op, {'unique_resource_name': '__DYNAMIC__', 'request_type': 'resourceowner', 'request_text': '', csrf_field: csrf_token}) output_objects.append({'object_type': 'html_form', 'text': helper}) target_op = 'rmresowner' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) helper = html_post_helper('rmresowner', '%s.py' % target_op, {'unique_resource_name': '__DYNAMIC__', 'cert_id': client_id, csrf_field: csrf_token}) output_objects.append({'object_type': 'html_form', 'text': helper}) output_objects.append({'object_type': 'table_pager', 'entry_name': 'resources', 'default_entries': default_pager_entries}) resources = [] if operation in list_operations: logger.info("get vgrid and resource map with caching %s" % caching) visible_res_confs = user_visible_res_confs(configuration, client_id, caching) res_map = get_resource_map(configuration, caching) anon_map = anon_to_real_res_map(configuration.resource_home) # NOTE: use simple pending check if caching to avoid lock during update if caching: pending_updates = pending_vgrids_update(configuration) or \ pending_resources_update(configuration) else: pending_updates = False if pending_updates: logger.debug("found pending cache updates: %s" % pending_updates) else: logger.debug("no pending cache updates") # Iterate through resources and show management for each one requested fields = ['PUBLICNAME', 'NODECOUNT', 'CPUCOUNT', 'MEMORY', 'DISK', 'ARCHITECTURE', 'SANDBOX', 'RUNTIMEENVIRONMENT'] # NOTE: only resources that user is allowed to access are listed. # Resource with neither exes nor stores are not shown to anyone # but the owners. Similarly resources are not shown if all # resource units solely participate in VGrids, which the user # can't access. for visible_res_name in visible_res_confs.keys(): unique_resource_name = visible_res_name if visible_res_name in anon_map.keys(): unique_resource_name = anon_map[visible_res_name] if not show_sandboxes and sandbox_resource(unique_resource_name): continue res_obj = {'object_type': 'resource', 'name': visible_res_name} # NOTE: res may not yet have been added to res_map here if not res_map.get(unique_resource_name, None): logger.info("skip %s not yet in resource map" % unique_resource_name) continue if client_id in res_map[unique_resource_name][OWNERS]: # Admin of resource when owner res_obj['resownerlink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s', %s, %s);" % ('rmresowner', 'Really leave %s owners?' % unique_resource_name, 'undefined', "{unique_resource_name: '%s'}" % unique_resource_name), 'class': 'removelink iconspace', 'title': 'Leave %s owners' % unique_resource_name, 'text': ''} res_obj['resdetailslink'] = { 'object_type': 'link', 'destination': 'resadmin.py?unique_resource_name=%s' % unique_resource_name, 'class': 'adminlink iconspace', 'title': 'Administrate %s' % unique_resource_name, 'text': ''} else: # link to become owner res_obj['resownerlink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s', '%s', %s);" % ('reqresowner', "Request ownership of " + visible_res_name + ":<br/>" + "\nPlease write a message to the owners (field below).", 'request_text', "{unique_resource_name: '%s'}" % visible_res_name), 'class': 'addlink iconspace', 'title': 'Request ownership of %s' % visible_res_name, 'text': ''} res_obj['resdetailslink'] = { 'object_type': 'link', 'destination': 'viewres.py?unique_resource_name=%s' % visible_res_name, 'class': 'infolink iconspace', 'title': 'View detailed %s specs' % visible_res_name, 'text': ''} # fields for everyone: public status for name in fields: res_obj[name] = res_map[unique_resource_name][CONF].get(name, '') # Use runtimeenvironment names instead of actual definitions res_obj['RUNTIMEENVIRONMENT'] = [i[0] for i in res_obj['RUNTIMEENVIRONMENT']] res_obj['RUNTIMEENVIRONMENT'].sort() resources.append(res_obj) if operation == "show": # insert dummy placeholder to build table res_obj = {'object_type': 'resource', 'name': ''} resources.append(res_obj) output_objects.append({'object_type': 'resource_list', 'pending_updates': pending_updates, 'resources': resources}) if operation in show_operations: if configuration.site_enable_sandboxes: if show_sandboxes: output_objects.append({'object_type': 'link', 'destination': '?show_sandboxes=false', 'class': 'removeitemlink iconspace', 'title': 'Hide sandbox resources', 'text': 'Exclude sandbox resources', }) else: output_objects.append({'object_type': 'link', 'destination': '?show_sandboxes=true', 'class': 'additemlink iconspace', 'title': 'Show sandbox resources', 'text': 'Include sandbox resources', }) output_objects.append( {'object_type': 'sectionheader', 'text': 'Resource Status'}) output_objects.append({'object_type': 'text', 'text': ''' Live resource status is available in the resource monitor page with all %s/resources you can access ''' % configuration.site_vgrid_label}) output_objects.append({ 'object_type': 'link', 'destination': 'showvgridmonitor.py?vgrid_name=ALL', 'class': 'monitorlink iconspace', 'title': 'Show monitor with all resources you can access', 'text': 'Global resource monitor', }) output_objects.append({'object_type': 'sectionheader', 'text': 'Additional Resources'}) output_objects.append({ 'object_type': 'text', 'text': 'You can sign up spare or dedicated resources to the grid below.' }) output_objects.append({'object_type': 'link', 'destination': 'resedit.py', 'class': 'addlink iconspace', 'title': 'Show sandbox resources', 'text': 'Create a new %s resource' % configuration.short_title, }) output_objects.append({'object_type': 'sectionheader', 'text': ''}) if configuration.site_enable_sandboxes: output_objects.append({ 'object_type': 'link', 'destination': 'ssslogin.py', 'class': 'adminlink iconspace', 'title': 'Administrate and monitor your sandbox resources', 'text': 'Administrate %s sandbox resources' % configuration.short_title}) output_objects.append({'object_type': 'sectionheader', 'text': ''}) output_objects.append({ 'object_type': 'link', 'destination': 'oneclick.py', 'class': 'sandboxlink iconspace', 'title': 'Run a One-click resource in your browser', 'text': 'Use this computer as One-click %s resource' % configuration.short_title}) logger.info("%s %s end for %s" % (op_name, operation, client_id)) 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) 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) patterns = accepted['job_id'] 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) if not configuration.site_enable_jobs: output_objects.append({ 'object_type': 'error_text', 'text': '''Job execution is not enabled on this system''' }) return (output_objects, returnvalues.SYSTEM_ERROR) if not patterns: output_objects.append({ 'object_type': 'error_text', 'text': 'No job_id specified!' }) return (output_objects, returnvalues.NO_SUCH_JOB_ID) # 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.mrsl_files_dir, client_dir)) + os.sep filelist = [] keywords_dict = mrslkeywords.get_keywords_dict(configuration) for pattern in patterns: pattern = pattern.strip() # Backward compatibility - all_jobs keyword should match all jobs if pattern == all_jobs: pattern = '*' # 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 + '.mRSL') 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): # 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 # Insert valid job files in filelist for later treatment 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: You do not have any matching job IDs!' % pattern }) status = returnvalues.CLIENT_ERROR else: filelist += match # resubmit is hard on the server if len(filelist) > 100: output_objects.append({ 'object_type': 'error_text', 'text': 'Too many matching jobs (%s)!' % len(filelist) }) return (output_objects, returnvalues.CLIENT_ERROR) resubmitobjs = [] status = returnvalues.OK for filepath in filelist: mrsl_file = filepath.replace(base_dir, '') job_id = mrsl_file.replace('.mRSL', '') # ("Resubmitting job with job_id: %s" % job_id) resubmitobj = {'object_type': 'resubmitobj', 'job_id': job_id} mrsl_dict = unpickle(filepath, logger) if not mrsl_dict: resubmitobj['message'] = "No such job: %s (%s)" % (job_id, mrsl_file) status = returnvalues.CLIENT_ERROR resubmitobjs.append(resubmitobj) continue resubmit_items = keywords_dict.keys() # loop selected keywords and create mRSL string resubmit_job_string = '' for dict_elem in resubmit_items: value = '' # Extract job value with fallback to default to support optional # fields job_value = mrsl_dict.get(dict_elem, keywords_dict[dict_elem]['Value']) if keywords_dict[dict_elem]['Type'].startswith( 'multiplekeyvalues'): for (elem_key, elem_val) in job_value: if elem_key: value += '%s=%s\n' % (str(elem_key).strip(), str(elem_val).strip()) elif keywords_dict[dict_elem]['Type'].startswith('multiple'): for elem in job_value: if elem: value += '%s\n' % str(elem).rstrip() else: if str(job_value): value += '%s\n' % str(job_value).rstrip() # Only insert keywords with an associated value if value: if value.rstrip() != '': resubmit_job_string += '''::%s:: %s ''' % (dict_elem, value.rstrip()) # save tempfile (filehandle, tempfilename) = \ tempfile.mkstemp(dir=configuration.mig_system_files, text=True) os.write(filehandle, resubmit_job_string) os.close(filehandle) # submit job the usual way (new_job_status, msg, new_job_id) = new_job(tempfilename, client_id, configuration, False, True) if not new_job_status: resubmitobj['status'] = False resubmitobj['message'] = msg status = returnvalues.SYSTEM_ERROR resubmitobjs.append(resubmitobj) continue # o.out("Resubmit failed: %s" % msg) # o.reply_and_exit(o.ERROR) resubmitobj['status'] = True resubmitobj['new_job_id'] = new_job_id resubmitobjs.append(resubmitobj) # o.out("Resubmit successful: %s" % msg) # o.out("%s" % msg) output_objects.append({ 'object_type': 'resubmitobjs', 'resubmitobjs': resubmitobjs }) 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) 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) output_objects.append({'object_type': 'header', 'text': 'Downloads'}) output_objects.append({ 'object_type': 'html_form', 'text': """ <div class="migcontent"> This page provides access to on-demand downloads of the %(site)s user scripts in all available formats.<br /> Simply pick your flavor of choice to generate the latest user scripts in your %(site)s home directory and as a zip file for easy download.<p> In order to use the scripts your need the interpreter of choice (bash or python at the moment) and the <a href='http://curl.haxx.se' class='urllink iconspace'>cURL</a> command line client.<br /> There's a tutorial with examples of all the commands available on the %(site)s page. The python version of the user scripts additionally includes a miglib python module, which may be used to incorporate %(site)s commands in your python applications. </div> """ % { 'site': configuration.short_title } }) output_objects.append({ 'object_type': 'sectionheader', 'text': '%s User Scripts' % configuration.short_title }) form_method = 'post' csrf_limit = get_csrf_limit(configuration) fill_helpers = { 'short_title': configuration.short_title, 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit } target_op = 'scripts' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) fill_helpers.update({'target_op': target_op, 'csrf_token': csrf_token}) output_objects.append({ 'object_type': 'html_form', 'text': """ <div class='migcontent'> Generate %(short_title)s user scripts to manage jobs and files:<br/> <div class='row button-grid'> <div class='col-lg-4 left'> <form method='%(form_method)s' action='%(target_op)s.py'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='lang' value='python' /> <input type='submit' value='python version' /> </form> </div> <div class='col-lg-4 middle'> <form method='%(form_method)s' action='%(target_op)s.py'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='lang' value='sh' /> <input type='submit' value='sh version' /> </form> </div> <div class='col-lg-4 right'> <form method='%(form_method)s' action='%(target_op)s.py'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <input type='hidden' name='output_format' value='html' /> <input type='submit' value='all versions' /> </form> </div> </div> </div> <br /> """ % fill_helpers }) output_objects.append({ 'object_type': 'sectionheader', 'text': '%s Resource Scripts' % configuration.short_title }) output_objects.append({ 'object_type': 'html_form', 'text': """ <div class='migcontent'> Generate %(short_title)s scripts to administrate resources and vgrids:<br/> <div class='row button-grid'> <div class='col-lg-4 left'> <form method='%(form_method)s' action='%(target_op)s.py'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='lang' value='python' /> <input type='hidden' name='flavor' value='resource' /> <input type='submit' value='python version' /> </form> </div> <div class='col-lg-4 middle'> <form method='%(form_method)s' action='%(target_op)s.py'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='lang' value='sh' /> <input type='hidden' name='flavor' value='resource' /> <input type='submit' value='sh version' /> </form> </div> <div class='col-lg-4 right'> <form method='%(form_method)s' action='%(target_op)s.py'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flavor' value='resource' /> <input type='submit' value='all versions' /> </form> </div> </div> </div> """ % fill_helpers }) return (output_objects, status)
def html_tmpl(configuration, client_id, title_entry, csrf_map={}, chroot=''): """HTML page base: some upload and menu entries depend on configuration""" active_menu = extract_menu(configuration, title_entry) user_settings = title_entry.get('user_settings', {}) legacy_ui = legacy_user_interface(configuration, user_settings) fill_helpers = {'short_title': configuration.short_title} html = ''' <!-- CONTENT --> <div class="container"> <div id="app-nav-container" class="row"> <h1>Welcome to %(short_title)s!</h1> <div class="home-page__header col-12"> <p class="sub-title">Tools from %(short_title)s helps you with storage, sharing and archiving of data. %(short_title)s delivers centralised storage space for personal and shared files.</p> </div> <div id="tips-container" class="col-12 invert-theme"> <div id="tips-content" class="tips-placeholder"> <!-- NOTE: filled by AJAX .. init for space --> </div> </div> ''' % fill_helpers html += render_apps(configuration, title_entry, active_menu) html += ''' <div class="col-lg-12 vertical-spacer"></div> </div> </div> ''' # Dynamic app selection form_method = 'post' csrf_limit = get_csrf_limit(configuration) target_op = 'settingsaction' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) settings_specs = get_keywords_dict() current_settings_dict = load_settings(client_id, configuration) if not current_settings_dict: # no current settings found current_settings_dict = {} fill_helpers['form_prefix'] = ''' <form class="save_settings save_apps" action="settingsaction.py" method="%s"> <input type="hidden" name="%s" value="%s"> ''' % (form_method, csrf_field, csrf_token) fill_helpers['form_suffix'] = ''' %s <input type="submit" value="Save"> </form> ''' % save_settings_html(configuration) apps_field = 'SITE_USER_MENU' # NOTE: build list of all default and user selectable apps in that order app_list = [app_id for app_id in configuration.site_default_menu] app_list += [ app_id for app_id in configuration.site_user_menu if not app_id in app_list ] mandatory_apps = [] for app_name in configuration.site_default_menu: mandatory_apps.append(menu_items[app_name]['title']) fill_helpers['mandatory_apps'] = ', '.join(mandatory_apps) html += ''' <!-- APP POP-UP --> <div id="add-app__window" class="app-container hidden"> <span class="add-app__close far fa-times-circle" onclick="closeAddApp()"></span> <div class="container"> <div class="row"> <div class="app-page__header col-12"> <h1>Your apps & app-setup</h1> <p class="sub-title-white">Here you can select which apps you want to use in your %(short_title)s system. Only %(mandatory_apps)s are mandatory.</p> </div> <div class="app-page__header col-12"> <h2>Select your apps</h2> %(form_prefix)s ''' % fill_helpers # The backend form requires all user settings even if we only want to edit # the user apps subset here - just fill as hidden values. # TODO: consider a save version with only relevant values. for (key, val) in current_settings_dict.items(): if key == apps_field: continue spec = settings_specs[key] if spec['Type'] == 'multiplestrings': html += ''' <textarea class="hidden" name="%s">%s</textarea> ''' % (key, '\n'.join(val)) elif spec['Type'] == 'string': html += ''' <input type="hidden" name="%s" value="%s"> ''' % (key, val) elif spec['Type'] == 'boolean': html += ''' <input type="hidden" name="%s" value="%s"> ''' % (key, val) html += ''' <div class="app-grid row"> ''' for app_id in app_list: app = menu_items[app_id] app_name = app['title'] if not legacy_ui and app.get('legacy_only', False): continue if app_id == 'vgrids': app_name = '%ss' % configuration.site_vgrid_label mandatory = (app_id in configuration.site_default_menu) app_btn_classes = 'app__btn col-12 ' if mandatory: app_btn_classes += "mandatory" else: app_btn_classes += "optional" app_icon_classes = app['class'] check_field = '' if not mandatory: checked, checkmark = "", "checkmark" if app_id in current_settings_dict.get(apps_field, []): checked = "checked='checked'" checkmark = "checkmark" check_field = ''' <input type="checkbox" name="SITE_USER_MENU" %s value="%s"> <span class="%s"></span> ''' % (checked, app_id, checkmark) html += ''' <div class="col-lg-2"> <div class="%s"> <label class="app-content"> %s <div class="background-app"> <span class="%s"></span><h3>%s</h3> </div> </label> </div> </div> ''' % (app_btn_classes, check_field, app_icon_classes, app_name) html += ''' </div> <br /> %(form_suffix)s </div> </div> </div> <div class="col-lg-12 vertical-spacer"></div> </div> ''' % fill_helpers return html
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=False) client_dir = client_id_dir(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) if not configuration.site_enable_openid or \ not 'migoid' in configuration.site_signup_methods: output_objects.append({ 'object_type': 'error_text', 'text': '''Local OpenID login is not enabled on this site''' }) return (output_objects, returnvalues.SYSTEM_ERROR) title_entry = find_entry(output_objects, 'title') title_entry['text'] = '%s OpenID account request' % \ configuration.short_title title_entry['skipmenu'] = True form_fields = [ 'full_name', 'organization', 'email', 'country', 'state', 'password', 'verifypassword', 'comment' ] title_entry['style']['advanced'] += account_css_helpers(configuration) add_import, add_init, add_ready = account_js_helpers( configuration, form_fields) title_entry['script']['advanced'] += add_import title_entry['script']['init'] += add_init title_entry['script']['ready'] += add_ready title_entry['script']['body'] = "class='staticpage'" header_entry = { 'object_type': 'header', 'text': '%s account request - with OpenID login' % configuration.short_title } output_objects.append(header_entry) output_objects.append({ 'object_type': 'html_form', 'text': ''' <div id="contextual_help"> </div> ''' }) # 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 user_fields = { 'full_name': '', 'organization': '', 'email': '', 'state': '', 'country': '', 'password': '', 'verifypassword': '', 'comment': '' } if not os.path.isdir(base_dir) and client_id: # Redirect to extcert page with certificate requirement but without # changing access method (CGI vs. WSGI). extcert_url = os.environ['REQUEST_URI'].replace('-sid', '-bin') extcert_url = os.path.join(os.path.dirname(extcert_url), 'extcert.py') extcert_link = { 'object_type': 'link', 'destination': extcert_url, 'text': 'Sign up with existing certificate (%s)' % client_id } output_objects.append({ 'object_type': 'warning', 'text': '''Apparently you already have a suitable %s certificate that you may sign up with:''' % configuration.short_title }) output_objects.append(extcert_link) output_objects.append({ 'object_type': 'warning', 'text': '''However, if you want a dedicated %s %s User OpenID you can still request one below:''' % (configuration.short_title, configuration.user_mig_oid_title) }) elif client_id: for entry in (title_entry, header_entry): entry['text'] = entry['text'].replace('request', 'request / renew') output_objects.append({ 'object_type': 'html_form', 'text': '''<p> Apparently you already have valid %s credentials, but if you want to add %s User OpenID access to the same account you can do so by posting the form below. Changing fields is <span class="warningtext"> not </span> supported, so all fields must remain unchanged for it to work. Otherwise it results in a request for a new account and OpenID without access to your old files, jobs and privileges. </p>''' % (configuration.short_title, configuration.user_mig_oid_title) }) user_fields.update(distinguished_name_to_user(client_id)) # Override with arg values if set for field in user_fields: if not field in accepted: continue override_val = accepted[field][-1].strip() if override_val: user_fields[field] = override_val user_fields = canonical_user(configuration, user_fields, user_fields.keys()) # Site policy dictates min length greater or equal than password_min_len policy_min_len, policy_min_classes = parse_password_policy(configuration) user_fields.update({ 'valid_name_chars': '%s (and common accents)' % html_escape(valid_name_chars), 'valid_password_chars': html_escape(valid_password_chars), 'password_min_len': max(policy_min_len, password_min_len), 'password_max_len': password_max_len, 'password_min_classes': max(policy_min_classes, 1), 'site': configuration.short_title }) form_method = 'post' csrf_limit = get_csrf_limit(configuration) fill_helpers = { 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit } target_op = 'reqoidaction' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) fill_helpers.update({'target_op': target_op, 'csrf_token': csrf_token}) fill_helpers.update({'site_signup_hint': configuration.site_signup_hint}) # Write-protect ID fields if requested for field in cert_field_map: fill_helpers['readonly_%s' % field] = '' ro_fields = [i for i in accepted['ro_fields'] if i in cert_field_map] if keyword_auto in accepted['ro_fields']: ro_fields += [i for i in cert_field_map if not i in ro_fields] for field in ro_fields: fill_helpers['readonly_%s' % field] = 'readonly' fill_helpers.update(user_fields) html = """ <p class='sub-title'>Please enter your information in at least the <span class='highlight_required'>mandatory</span> fields below and press the Send button to submit the account request to the %(site)s administrators.</p> <p class='personal leftpad highlight_message'> IMPORTANT: we need to identify and notify you about login info, so please use a working Email address clearly affiliated with your Organization! </p> %(site_signup_hint)s <hr /> """ html += account_request_template(configuration, default_values=fill_helpers) # TODO: remove this legacy version? html += """ <div style="height: 0; visibility: hidden; display: none;"> <!--OLD FORM--> <form method='%(form_method)s' action='%(target_op)s.py' onSubmit='return validate_form();'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <table> <!-- NOTE: javascript support for unicode pattern matching is lacking so we only restrict e.g. Full Name to words separated by space here. The full check takes place in the backend, but users are better of with sane early warnings than the cryptic backend errors. --> <tr><td class='mandatory label'>Full name</td><td><input id='full_name_field' type=text name=cert_name value='%(full_name)s' required pattern='[^ ]+([ ][^ ]+)+' title='Your full name, i.e. two or more names separated by space' /></td><td class=fill_space><br /></td></tr> <tr><td class='mandatory label'>Email address</td><td><input id='email_field' type=email name=email value='%(email)s' required title='A valid email address that you read' /> </td><td class=fill_space><br /></td></tr> <tr><td class='mandatory label'>Organization</td><td><input id='organization_field' type=text name=org value='%(organization)s' required pattern='[^ ]+([ ][^ ]+)*' title='Name of your organisation: one or more abbreviations or words separated by space' /></td><td class=fill_space><br /></td></tr> <tr><td class='mandatory label'>Two letter country-code</td><td><input id='country_field' type=text name=country minlength=2 maxlength=2 value='%(country)s' required pattern='[A-Z]{2}' title='The two capital letters used to abbreviate your country' /></td><td class=fill_space><br /></td></tr> <tr><td class='optional label'>State</td><td><input id='state_field' type=text name=state value='%(state)s' pattern='([A-Z]{2})?' maxlength=2 title='Leave empty or enter the capital 2-letter abbreviation of your state if you are a US resident' /> </td><td class=fill_space><br /></td></tr> <tr><td class='mandatory label'>Password</td><td><input id='password_field' type=password name=password minlength=%(password_min_len)d maxlength=%(password_max_len)d value='%(password)s' required pattern='.{%(password_min_len)d,%(password_max_len)d}' title='Password of your choice, see help box for limitations' /> </td><td class=fill_space><br /></td></tr> <tr><td class='mandatory label'>Verify password</td><td><input id='verifypassword_field' type=password name=verifypassword minlength=%(password_min_len)d maxlength=%(password_max_len)d value='%(verifypassword)s' required pattern='.{%(password_min_len)d,%(password_max_len)d}' title='Repeat your chosen password' /></td><td class=fill_space><br /></td></tr> <!-- NOTE: we technically allow saving the password on scrambled form hide it by default --> <tr class='hidden'><td class='optional label'>Password recovery</td><td class=''><input id='passwordrecovery_checkbox' type=checkbox name=passwordrecovery></td><td class=fill_space><br/></td></tr> <tr><td class='optional label'>Optional comment or reason why you should<br />be granted a %(site)s account:</td><td><textarea id='comment_field' rows=4 name=comment title='A free-form comment where you can explain what you need the account for' ></textarea></td><td class=fill_space><br /></td></tr> <tr><td class='label'><!-- empty area --></td><td> <input id='submit_button' type=submit value=Send /></td><td class=fill_space><br/></td></tr> </table> </form> <hr /> <div class='warn_message'>Please note that if you enable password recovery your password will be saved on encoded format but recoverable by the %(site)s administrators</div> </div> <br /> <br /> <!-- Hidden help text --> <div id='help_text'> <div id='1full_name_help'>Your full name, restricted to the characters in '%(valid_name_chars)s'</div> <div id='1organization_help'>Organization name or acronym matching email</div> <div id='1email_help'>Email address associated with your organization if at all possible</div> <div id='1country_help'>Country code of your organization and on the form DE/DK/GB/US/.. , <a href='https://en.wikipedia.org/wiki/ISO_3166-1'>help</a></div> <div id='1state_help'>Optional 2-letter ANSI state code of your organization, please just leave empty unless it is in the US or similar, <a href='https://en.wikipedia.org/wiki/List_of_U.S._state_abbreviations'>help</a></div> <div id='1password_help'>Password is restricted to the characters:<br/><tt>%(valid_password_chars)s</tt><br/>Certain other complexity requirements apply for adequate strength. For example it must be %(password_min_len)s to %(password_max_len)s characters long and contain at least %(password_min_classes)d different character classes.</div> <div id='1verifypassword_help'>Please repeat password</div> <!--<div id='1comment_help'>Optional, but a short informative comment may help us verify your account needs and thus speed up our response. Typically the name of a local collaboration partner or project may be helpful.</div>--> </div> """ output_objects.append({ 'object_type': 'html_form', 'text': html % fill_helpers }) return (output_objects, returnvalues.OK)
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) defaults = signature()[1] title_entry = find_entry(output_objects, 'title') label = "%s" % configuration.site_vgrid_label title_entry['text'] = "Reject %s Request" % label output_objects.append({ 'object_type': 'header', 'text': 'Reject %s Request' % label }) (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) vgrid_name = accepted['vgrid_name'][-1].strip() request_name = unhexlify(accepted['request_name'][-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) # Validity of user and vgrid names is checked in this init function so # no need to worry about illegal directory traversal through variables (ret_val, msg, ret_variables) = \ init_vgrid_script_add_rem(vgrid_name, client_id, request_name, 'request', configuration) if not ret_val: output_objects.append({'object_type': 'error_text', 'text': msg}) return (output_objects, returnvalues.CLIENT_ERROR) elif msg: # In case of warnings, msg is non-empty while ret_val remains True output_objects.append({'object_type': 'warning', 'text': msg}) if request_name: request_dir = os.path.join(configuration.vgrid_home, vgrid_name) req = load_access_request(configuration, request_dir, request_name) if not req or not delete_access_request(configuration, request_dir, request_name): logger.error("failed to delete owner request for %s in %s" % \ (vgrid_name, request_name)) output_objects.append({ 'object_type': 'error_text', 'text': 'Failed to remove saved vgrid request for %s in %s!'\ % (vgrid_name, request_name)}) return (output_objects, returnvalues.CLIENT_ERROR) output_objects.append({ 'object_type': 'text', 'text': ''' Deleted %(request_type)s access request to %(target)s for %(entity)s . ''' % req }) if req['request_type'] == 'vgridresource': id_field = "unique_resource_name" else: id_field = "cert_id" form_method = 'post' csrf_limit = get_csrf_limit(configuration) fill_helpers = { 'protocol': any_protocol, 'id_field': id_field, 'vgrid_label': label, 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit } fill_helpers.update(req) target_op = 'sendrequestaction' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) fill_helpers.update({'target_op': target_op, 'csrf_token': csrf_token}) output_objects.append({ 'object_type': 'html_form', 'text': """ <p> You can use the reply form below if you want to additionally send an explanation for rejecting the request. </p> <form method='%(form_method)s' action='%(target_op)s.py'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <input type=hidden name=request_type value='vgridreject' /> <input type=hidden name=vgrid_name value='%(target)s' /> <input type=hidden name=%(id_field)s value='%(entity)s' /> <input type=hidden name=protocol value='%(protocol)s' /> <table> <tr> <td class='title'>Optional reject message to requestor(s)</td> </tr><tr> <td><textarea name=request_text cols=72 rows=10> We have decided to reject your %(request_type)s request to our %(target)s %(vgrid_label)s. Regards, the %(target)s %(vgrid_label)s owners </textarea></td> </tr> <tr> <td><input type='submit' value='Inform requestor(s)' /></td> </tr> </table> </form> <br /> """ % fill_helpers }) output_objects.append({ 'object_type': 'link', 'destination': 'adminvgrid.py?vgrid_name=%s' % vgrid_name, 'text': 'Back to administration for %s' % vgrid_name }) return (output_objects, returnvalues.OK)
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id) output_objects.append({ 'object_type': 'text', 'text': '--------- Trying to RESTART exe ----------' }) 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) unique_resource_name = accepted['unique_resource_name'][-1] cputime = accepted['cputime'][-1] exe_name_list = accepted['exe_name'] all = accepted['all'][-1].lower() == 'true' parallel = accepted['parallel'][-1].lower() == 'true' if not configuration.site_enable_resources: output_objects.append({ 'object_type': 'error_text', 'text': '''Resources are not enabled on this system''' }) return (output_objects, returnvalues.SYSTEM_ERROR) 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) if not is_owner(client_id, unique_resource_name, configuration.resource_home, logger): output_objects.append({ 'object_type': 'error_text', 'text': 'Failure: You must be an owner of ' + unique_resource_name + ' to restart the exe!' }) return (output_objects, returnvalues.CLIENT_ERROR) exit_status = returnvalues.OK if all: exe_name_list = get_all_exe_names(unique_resource_name) # take action based on supplied list of exes if len(exe_name_list) == 0: output_objects.append({ 'object_type': 'text', 'text': "No exes specified and 'all' argument not set to true: Nothing to do!" }) workers = [] task_list = [] for exe_name in exe_name_list: task = Worker(target=stop_resource_exe, args=(unique_resource_name, exe_name, configuration.resource_home, logger)) workers.append((exe_name, [task])) task_list.append(task) throttle_max_concurrent(task_list) task.start() if not parallel: task.join() # Complete each stop thread before launching corresponding start threads for (exe_name, task_list) in workers: # We could optimize with non-blocking join here but keep it simple for now # as final result will need to wait for slowest member anyway task_list[0].join() task = Worker(target=start_resource_exe, args=(unique_resource_name, exe_name, configuration.resource_home, int(cputime), logger)) task_list.append(task) throttle_max_concurrent(task_list) task.start() if not parallel: task.join() for (exe_name, task_list) in workers: (status, msg) = task_list[0].finish() output_objects.append({ 'object_type': 'header', 'text': 'Restart exe output:' }) if not status: output_objects.append({ 'object_type': 'error_text', 'text': 'Problems stopping exe during restart: %s' % msg }) (status2, msg2) = task_list[1].finish() if not status2: output_objects.append({ 'object_type': 'error_text', 'text': 'Problems starting exe during restart: %s' % msg2 }) exit_status = returnvalues.SYSTEM_ERROR if status and status2: output_objects.append({ 'object_type': 'text', 'text': 'Restart exe success: Stop output: %s ; Start output: %s' % (msg, msg2) }) return (output_objects, exit_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, # NOTE: path cannot use wildcards here typecheck_overrides={}, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) status = returnvalues.OK chroot = '' if configuration.site_enable_gdp: chroot = get_project_from_client_id(configuration, client_id) all_paths = accepted['path'] entry_path = all_paths[-1] title_entry = find_entry(output_objects, 'title') user_settings = title_entry.get('user_settings', {}) title_entry['text'] = 'File Manager' title_entry['style'] = css_tmpl(configuration, user_settings) legacy_buttons = False if legacy_user_interface(configuration, user_settings): legacy_buttons = True logger.debug("enable legacy buttons") if configuration.site_enable_jobs and \ 'submitjob' in extract_menu(configuration, title_entry): enable_submit = 'true' else: enable_submit = 'false' csrf_map = {} method = 'post' limit = get_csrf_limit(configuration) for target_op in csrf_backends: csrf_map[target_op] = make_csrf_token(configuration, method, target_op, client_id, limit) (add_import, add_init, add_ready) = js_tmpl_parts(configuration, entry_path, enable_submit, str(configuration.site_enable_preview), legacy_buttons, csrf_map, chroot) title_entry['script']['advanced'] += add_import title_entry['script']['init'] += add_init title_entry['script']['ready'] += add_ready title_entry['container_class'] = 'fillwidth', output_objects.append({ 'object_type': 'header', 'class': 'fileman-title', 'text': 'File Manager' }) output_objects.append({ 'object_type': 'html_form', 'text': html_tmpl(configuration, client_id, title_entry, csrf_map, chroot) }) if len(all_paths) > 1: output_objects.append({ 'object_type': 'sectionheader', 'text': 'All requested paths:' }) for path in all_paths: output_objects.append({ 'object_type': 'link', 'text': path, 'destination': 'fileman.py?path=%s' % path }) output_objects.append({'object_type': 'text', 'text': ''}) 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, op_menu=False) defaults = signature()[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False) if not validate_status: logger.warning('%s invalid input: %s' % (op_name, accepted)) return (accepted, returnvalues.CLIENT_ERROR) if not configuration.site_enable_openid or \ not 'migoid' in configuration.site_signup_methods: output_objects.append({ 'object_type': 'error_text', 'text': '''Local OpenID login is not enabled on this site''' }) return (output_objects, returnvalues.SYSTEM_ERROR) title_entry = find_entry(output_objects, 'title') title_entry['text'] = '%s OpenID account request' % \ configuration.short_title title_entry['skipmenu'] = True output_objects.append({ 'object_type': 'header', 'text': '%s OpenID account request' % configuration.short_title }) admin_email = configuration.admin_email smtp_server = configuration.smtp_server user_pending = os.path.abspath(configuration.user_pending) # TODO: switch to canonical_user fra mig.shared.base instead? # force name to capitalized form (henrik karlsen -> Henrik Karlsen) # please note that we get utf8 coded bytes here and title() treats such # chars as word termination. Temporarily force to unicode. raw_name = accepted['cert_name'][-1].strip() try: cert_name = force_utf8(force_unicode(raw_name).title()) except Exception: cert_name = raw_name.title() country = accepted['country'][-1].strip().upper() state = accepted['state'][-1].strip().upper() org = accepted['org'][-1].strip() # lower case email address email = accepted['email'][-1].strip().lower() password = accepted['password'][-1] verifypassword = accepted['verifypassword'][-1] # The checkbox typically returns value 'on' if selected passwordrecovery = (accepted['passwordrecovery'][-1].strip().lower() in ('1', 'o', 'y', 't', 'on', 'yes', 'true')) # keep comment to a single line comment = accepted['comment'][-1].replace('\n', ' ') # single quotes break command line format - remove comment = comment.replace("'", ' ') accept_terms = (accepted['accept_terms'][-1].strip().lower() in ('1', 'o', 'y', 't', 'on', 'yes', 'true')) 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) if not accept_terms: output_objects.append({ 'object_type': 'error_text', 'text': 'You must accept the terms of use in sign up!' }) output_objects.append({ 'object_type': 'link', 'destination': 'javascript:history.back();', 'class': 'genericbutton', 'text': "Try again" }) return (output_objects, returnvalues.CLIENT_ERROR) if password != verifypassword: output_objects.append({ 'object_type': 'error_text', 'text': 'Password and verify password are not identical!' }) output_objects.append({ 'object_type': 'link', 'destination': 'javascript:history.back();', 'class': 'genericbutton', 'text': "Try again" }) return (output_objects, returnvalues.CLIENT_ERROR) try: assure_password_strength(configuration, password) except Exception as exc: logger.warning( "%s invalid password for '%s' (policy %s): %s" % (op_name, cert_name, configuration.site_password_policy, exc)) output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid password requested: %s.' % exc }) output_objects.append({ 'object_type': 'link', 'destination': 'javascript:history.back();', 'class': 'genericbutton', 'text': "Try again" }) return (output_objects, returnvalues.CLIENT_ERROR) if not existing_country_code(country, configuration): output_objects.append({ 'object_type': 'error_text', 'text': '''Illegal country code: Please read and follow the instructions shown in the help bubble when filling the country field on the request page! Specifically if you are from the U.K. you need to use GB as country code in line with the ISO-3166 standard. ''' }) output_objects.append({ 'object_type': 'link', 'destination': 'javascript:history.back();', 'class': 'genericbutton', 'text': "Try again" }) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: move this check to conf? if not forced_org_email_match(org, email, configuration): output_objects.append({ 'object_type': 'error_text', 'text': '''Illegal email and organization combination: Please read and follow the instructions in red on the request page! If you are a student with only a @*.ku.dk address please just use KU as organization. As long as you state that you want the account for course purposes in the comment field, you will be given access to the necessary resources anyway. ''' }) output_objects.append({ 'object_type': 'link', 'destination': 'javascript:history.back();', 'class': 'genericbutton', 'text': "Try again" }) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: we save password on scrambled form only if explicitly requested if passwordrecovery: logger.info('saving %s scrambled password to enable recovery' % email) scrambled_pw = scramble_password(configuration.site_password_salt, password) else: logger.info('only saving %s password hash' % email) scrambled_pw = '' user_dict = { 'full_name': cert_name, 'organization': org, 'state': state, 'country': country, 'email': email, 'comment': comment, 'password': scrambled_pw, 'password_hash': make_hash(password), 'expire': default_account_expire(configuration, 'oid'), 'openid_names': [], 'auth': ['migoid'], } fill_distinguished_name(user_dict) user_id = user_dict['distinguished_name'] user_dict['authorized'] = (user_id == client_id) if configuration.user_openid_providers and configuration.user_openid_alias: user_dict['openid_names'] += \ [user_dict[configuration.user_openid_alias]] logger.info('got account request from reqoid: %s' % user_dict) # For testing only if cert_name.upper().find('DO NOT SEND') != -1: output_objects.append({ 'object_type': 'text', 'text': "Test request ignored!" }) return (output_objects, returnvalues.OK) req_path = None try: (os_fd, req_path) = tempfile.mkstemp(dir=user_pending) os.write(os_fd, dumps(user_dict)) os.close(os_fd) except Exception as err: logger.error('Failed to write OpenID account request to %s: %s' % (req_path, err)) output_objects.append({ 'object_type': 'error_text', 'text': '''Request could not be sent to site administrators. Please contact them manually on %s if this error persists.''' % admin_email }) return (output_objects, returnvalues.SYSTEM_ERROR) logger.info('Wrote OpenID account request to %s' % req_path) tmp_id = os.path.basename(req_path) user_dict['tmp_id'] = tmp_id mig_user = os.environ.get('USER', 'mig') helper_commands = user_manage_commands(configuration, mig_user, req_path, user_id, user_dict, 'oid') user_dict.update(helper_commands) user_dict['site'] = configuration.short_title user_dict['vgrid_label'] = configuration.site_vgrid_label user_dict['vgridman_links'] = generate_https_urls( configuration, '%(auto_base)s/%(auto_bin)s/vgridman.py', {}) email_header = '%s OpenID request for %s' % \ (configuration.short_title, cert_name) email_msg = \ """ Received an OpenID request with account data * Full Name: %(full_name)s * Organization: %(organization)s * State: %(state)s * Country: %(country)s * Email: %(email)s * Comment: %(comment)s * Expire: %(expire)s Command to create user on %(site)s server: %(command_user_create)s Command to inform user and %(site)s admins: %(command_user_notify)s Optional command to create matching certificate: %(command_cert_create)s Finally add the user %(distinguished_name)s to any relevant %(vgrid_label)ss using one of the management links: %(vgridman_links)s --- If user must be denied access or deleted at some point --- Command to reject user account request on %(site)s server: %(command_user_reject)s Remove the user %(distinguished_name)s from any relevant %(vgrid_label)ss using one of the management links: %(vgridman_links)s Optional command to revoke any matching user certificate: %(command_cert_revoke)s You need to copy the resulting signed certificate revocation list (crl.pem) to the web server(s) for the revocation to take effect. Command to suspend user on %(site)s server: %(command_user_suspend)s Command to delete user again on %(site)s server: %(command_user_delete)s --- """ % user_dict logger.info('Sending email: to: %s, header: %s, msg: %s, smtp_server: %s' % (admin_email, email_header, email_msg, smtp_server)) if not send_email(admin_email, email_header, email_msg, logger, configuration): output_objects.append({ 'object_type': 'error_text', 'text': '''An error occured trying to send the email requesting the site administrators to create a new OpenID and account. Please email them (%s) manually and include the session ID: %s''' % (admin_email, tmp_id) }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': """Request sent to site administrators: Your OpenID account request will be verified and handled as soon as possible, so please be patient. Once handled an email will be sent to the account you have specified ('%s') with further information. In case of inquiries about this request, please email the site administrators (%s) and include the session ID: %s""" % (email, configuration.admin_email, tmp_id) }) return (output_objects, returnvalues.OK)
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) defaults = signature()[1] title_entry = find_entry(output_objects, 'title') label = "%s" % configuration.site_vgrid_label title_entry['text'] = "Administrate %s" % label # NOTE: Delay header entry here to include vgrid name (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) vgrid_name = accepted['vgrid_name'][-1] # prepare for confirm dialog, tablesort and toggling the views (css/js) # jquery support for tablesorter and confirmation on request and leave # requests table initially sorted by 0, 4, 3 (type first, then date and # with alphabetical client ID last) # sharelinks table initially sorted by 5, 4 reversed (active first and # in growing age) table_specs = [{ 'table_id': 'accessrequeststable', 'pager_id': 'accessrequests_pager', 'sort_order': '[[0,0],[4,0],[3,0]]' }, { 'table_id': 'sharelinkstable', 'pager_id': 'sharelinks_pager', 'sort_order': '[[5,1],[4,1]]' }] (add_import, add_init, add_ready) = man_base_js(configuration, table_specs, {'width': 600}) add_init += ''' var toggleHidden = function(classname) { // classname supposed to have a leading dot $(classname).toggleClass("hidden"); }; /* helpers for dynamic form input fields */ function onOwnerInputChange() { makeSpareFields("#dynownerspares", "cert_id"); } function onMemberInputChange() { makeSpareFields("#dynmemberspares", "cert_id"); } function onResourceInputChange() { makeSpareFields("#dynresourcespares", "unique_resource_name"); } ''' add_ready += ''' /* init add owners/member/resource forms with dynamic input fields */ onOwnerInputChange(); $("#dynownerspares").on("blur", "input[name=cert_id]", function(event) { //console.debug("in add owner blur handler"); onOwnerInputChange(); } ); onMemberInputChange(); $("#dynmemberspares").on("blur", "input[name=cert_id]", function(event) { //console.debug("in add member blur handler"); onMemberInputChange(); } );''' if configuration.site_enable_resources: add_ready += ''' onResourceInputChange(); $("#dynresourcespares").on("blur", "input[name=unique_resource_name]", function(event) { console.debug("in resource blur handler"); onResourceInputChange(); } ); ''' 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) }) form_method = 'post' csrf_limit = get_csrf_limit(configuration) fill_helpers = { 'short_title': configuration.short_title, 'vgrid_label': label, 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit } output_objects.append({ 'object_type': 'header', 'text': "Administrate '%s'" % vgrid_name }) if not vgrid_is_owner(vgrid_name, client_id, configuration): output_objects.append({ 'object_type': 'error_text', 'text': 'Only owners of %s can administrate it.' % vgrid_name }) target_op = "sendrequestaction" csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) js_name = 'reqvgridowner%s' % hexlify(vgrid_name) helper = html_post_helper( js_name, '%s.py' % target_op, { 'vgrid_name': vgrid_name, 'request_type': 'vgridowner', 'request_text': '', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) output_objects.append({ 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s', '%s');" % (js_name, "Request ownership of " + vgrid_name + ":<br/>" + "\nPlease write a message to the owners below.", 'request_text'), 'class': 'addadminlink iconspace', 'title': 'Request ownership of %s' % vgrid_name, 'text': 'Apply to become an owner' }) return (output_objects, returnvalues.SYSTEM_ERROR) for (item, scr) in zip(['owner', 'member', 'resource'], ['vgridowner', 'vgridmember', 'vgridres']): if item == 'resource' and not configuration.site_enable_resources: continue output_objects.append({ 'object_type': 'sectionheader', 'text': "%ss" % item.title() }) (init_status, oobjs) = vgrid_add_remove_table(client_id, vgrid_name, item, scr, configuration) if not init_status: output_objects.extend(oobjs) return (output_objects, returnvalues.SYSTEM_ERROR) else: output_objects.append({ 'object_type': 'html_form', 'text': '<div class="div-%s">' % item }) output_objects.append({ 'object_type': 'link', 'destination': "javascript:toggleHidden('.div-%s');" % item, 'class': 'removeitemlink iconspace', 'title': 'Toggle view', 'text': 'Hide %ss' % item.title() }) output_objects.extend(oobjs) output_objects.append({ 'object_type': 'html_form', 'text': '</div><div class="hidden div-%s">' % item }) output_objects.append({ 'object_type': 'link', 'destination': "javascript:toggleHidden('.div-%s');" % item, 'class': 'additemlink iconspace', 'title': 'Toggle view', 'text': 'Show %ss' % item.title() }) output_objects.append({ 'object_type': 'html_form', 'text': '</div>' }) # Pending requests target_op = "addvgridowner" csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) helper = html_post_helper( "acceptvgridownerreq", "%s.py" % target_op, { 'vgrid_name': vgrid_name, 'cert_id': '__DYNAMIC__', 'request_name': '__DYNAMIC__', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) target_op = "addvgridmember" csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) helper = html_post_helper( "acceptvgridmemberreq", "%s.py" % target_op, { 'vgrid_name': vgrid_name, 'cert_id': '__DYNAMIC__', 'request_name': '__DYNAMIC__', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) target_op = "addvgridres" csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) helper = html_post_helper( "acceptvgridresourcereq", "%s.py" % target_op, { 'vgrid_name': vgrid_name, 'unique_resource_name': '__DYNAMIC__', 'request_name': '__DYNAMIC__', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) target_op = "rejectvgridreq" csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) helper = html_post_helper( "rejectvgridreq", "%s.py" % target_op, { 'vgrid_name': vgrid_name, 'request_name': '__DYNAMIC__', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) request_dir = os.path.join(configuration.vgrid_home, vgrid_name) request_list = [] for req_name in list_access_requests(configuration, request_dir): req = load_access_request(configuration, request_dir, req_name) if not req: continue if not req.get('request_type', None) in ["vgridowner", "vgridmember", "vgridresource"]: logger.error("unexpected request_type %(request_type)s" % req) continue request_item = build_accessrequestitem_object(configuration, req) # Convert filename with exotic chars into url-friendly pure hex version shared_args = {"request_name": hexlify(req["request_name"])} accept_args, reject_args = {}, {} accept_args.update(shared_args) reject_args.update(shared_args) if req['request_type'] == "vgridresource": accept_args["unique_resource_name"] = req["entity"] else: accept_args["cert_id"] = req["entity"] request_item['acceptrequestlink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s', %s, %s);" % ("accept%(request_type)sreq" % req, "Accept %(target)s %(request_type)s request from %(entity)s" % req, 'undefined', "{%s}" % ', '.join(["'%s': '%s'" % pair for pair in accept_args.items()])), 'class': 'addlink iconspace', 'title': 'Accept %(target)s %(request_type)s request from %(entity)s' % req, 'text': '' } request_item['rejectrequestlink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s', %s, %s);" % ("rejectvgridreq", "Reject %(target)s %(request_type)s request from %(entity)s" % req, 'undefined', "%s" % reject_args), 'class': 'removelink iconspace', 'title': 'Reject %(target)s %(request_type)s request from %(entity)s' % req, 'text': '' } request_list.append(request_item) output_objects.append({ 'object_type': 'sectionheader', 'text': "Pending Requests" }) output_objects.append({ 'object_type': 'table_pager', 'id_prefix': 'accessrequests_', 'entry_name': 'access requests', 'default_entries': default_pager_entries }) output_objects.append({ 'object_type': 'accessrequests', 'accessrequests': request_list }) # VGrid Share links # Table columns to skip skip_list = [ 'editsharelink', 'delsharelink', 'invites', 'expire', 'single_file' ] # NOTE: Inheritance is a bit tricky for sharelinks because parent shares # only have relevance if they actually share a path that is a prefix of # vgrid_name. (share_status, share_list) = vgrid_sharelinks(vgrid_name, configuration) sharelinks = [] if share_status: for share_dict in share_list: rel_path = share_dict['path'].strip(os.sep) parent_vgrids = vgrid_list_parents(vgrid_name, configuration) include_share = False # Direct sharelinks (careful not to greedy match A/B with A/BCD) if rel_path == vgrid_name or \ rel_path.startswith(vgrid_name+os.sep): include_share = True # Parent vgrid sharelinks that in effect also give access here for parent in parent_vgrids: if rel_path == parent: include_share = True if include_share: share_item = build_sharelinkitem_object( configuration, share_dict) sharelinks.append(share_item) else: logger.warning("failed to load vgrid sharelinks for %s: %s" % (vgrid_name, share_list)) output_objects.append({ 'object_type': 'sectionheader', 'text': "Share Links" }) output_objects.append({ 'object_type': 'html_form', 'text': '<p>Current share links in %s shared folder</p>' % vgrid_name }) output_objects.append({ 'object_type': 'table_pager', 'id_prefix': 'sharelinks_', 'entry_name': 'share links', 'default_entries': default_pager_entries }) output_objects.append({ 'object_type': 'sharelinks', 'sharelinks': sharelinks, 'skip_list': skip_list }) # VGrid settings output_objects.append({'object_type': 'sectionheader', 'text': "Settings"}) (direct_status, direct_dict) = vgrid_settings(vgrid_name, configuration, recursive=False, as_dict=True) if not direct_status or not direct_dict: direct_dict = {} (settings_status, settings_dict) = vgrid_settings(vgrid_name, configuration, recursive=True, as_dict=True) if not settings_status or not settings_dict: settings_dict = {} form_method = 'post' csrf_limit = get_csrf_limit(configuration) # Always set these values settings_dict.update({ 'vgrid_name': vgrid_name, 'vgrid_label': label, 'owners': keyword_owners, 'members': keyword_members, 'all': keyword_all, 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit }) target_op = 'vgridsettings' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) settings_dict.update({'target_op': target_op, 'csrf_token': csrf_token}) settings_form = ''' <form method="%(form_method)s" action="%(target_op)s.py"> <fieldset> <legend>%(vgrid_label)s configuration</legend> <input type="hidden" name="%(csrf_field)s" value="%(csrf_token)s" /> <input type="hidden" name="vgrid_name" value="%(vgrid_name)s" /> ''' description = settings_dict.get('description', '') settings_form += ''' <h4>Public description</h4> <textarea class="fillwidth padspace" name="description" rows=10 >%s</textarea> ''' % description settings_form += '<br/>' settings_form += '''<p>All visibility options below can be set to owners, members or everyone and by default only owners can see participation. In effect setting visibility to <em>members</em> means that owners and members can see the corresponding participants. Similarly setting a visibility flag to <em>everyone</em> means that all %s users can see the participants.</p> ''' % configuration.short_title visibility_options = [("Owners are visible to", "visible_owners"), ("Members are visible to", "visible_members"), ("Resources are visible to", "visible_resources")] for (title, field) in visibility_options: settings_form += '<h4>%s</h4>' % title if direct_dict.get(field, False): choices = _valid_visible + _reset_choice else: choices = _valid_visible + _keep_choice for (key, val) in choices: checked = '' if settings_dict.get(field, keyword_owners) == val: checked = "checked" settings_form += ''' <input type="radio" name="%s" value="%s" %s/> %s ''' % (field, val, checked, key) settings_form += '<br/>' field = 'restrict_settings_adm' restrict_settings_adm = settings_dict.get(field, default_vgrid_settings_limit) if direct_dict.get(field, False): direct_note = _reset_note else: direct_note = _keep_note settings_form += ''' <h4>Restrict Settings</h4> Restrict changing of these settings to only the first <input type="number" name="restrict_settings_adm" min=0 max=999 minlength=1 maxlength=3 value=%d required /> owners %s. ''' % (restrict_settings_adm, direct_note) settings_form += '<br/>' field = 'restrict_owners_adm' restrict_owners_adm = settings_dict.get(field, default_vgrid_settings_limit) if direct_dict.get(field, False): direct_note = _reset_note else: direct_note = _keep_note settings_form += ''' <h4>Restrict Owner Administration</h4> Restrict administration of owners to only the first <input type="number" name="restrict_owners_adm" min=0 max=999 minlength=1 maxlength=3 value=%d required /> owners %s. ''' % (restrict_owners_adm, direct_note) settings_form += '<br/>' field = 'restrict_members_adm' restrict_members_adm = settings_dict.get(field, default_vgrid_settings_limit) if direct_dict.get(field, False): direct_note = _reset_note else: direct_note = _keep_note settings_form += ''' <h4>Restrict Member Administration</h4> Restrict administration of members to only the first <input type="number" name="restrict_members_adm" min=0 max=999 minlength=1 maxlength=3 value=%d required /> owners %s. ''' % (restrict_members_adm, direct_note) settings_form += '<br/>' field = 'restrict_resources_adm' restrict_resources_adm = settings_dict.get(field, default_vgrid_settings_limit) if direct_dict.get(field, False): direct_note = _reset_note else: direct_note = _keep_note settings_form += ''' <h4>Restrict Resource Administration</h4> Restrict administration of resources to only the first <input type="number" name="restrict_resources_adm" min=0 max=999 minlength=1 maxlength=3 value=%d required /> owners %s. ''' % (restrict_resources_adm, direct_note) settings_form += '<br/>' if vgrid_restrict_write_support(configuration): settings_form += '''<p>All write access options below can be set to owners, members or none. By default only owners can write web pages while owners and members can edit data in the shared folders. In effect setting write access to <em>members</em> means that owners and members have full access. Similarly setting a write access flag to <em>owners</em> means that only owners can modify the data, while members can only read and use it. Finally setting a write access flag to <em>none</em> means that neither owners nor members can modify the data there, effectively making it read-only. Some options are not yet supported and thus are disabled below. </p> ''' writable_options = [ ("Shared files write access", "write_shared_files", keyword_members), ("Private web page write access", "write_priv_web", keyword_owners), ("Public web page write access", "write_pub_web", keyword_owners), ] else: writable_options = [] for (title, field, default) in writable_options: settings_form += '<h4>%s</h4>' % title if direct_dict.get(field, False): choices = _valid_write_access + _reset_choice else: choices = _valid_write_access + _keep_choice for (key, val) in choices: disabled = '' # TODO: remove these artifical limits once we support changing # TODO: also add check for vgrid web reshare in sharelink then if field == 'write_shared_files' and val == keyword_owners: disabled = 'disabled' elif field == 'write_priv_web' and val in [ keyword_members, keyword_none ]: disabled = 'disabled' elif field == 'write_pub_web' and val in [ keyword_members, keyword_none ]: disabled = 'disabled' checked = '' if settings_dict.get(field, default) == val: checked = "checked" settings_form += ''' <input type="radio" name="%s" value="%s" %s %s /> %s ''' % (field, val, checked, disabled, key) settings_form += '<br/>' sharelink_options = [("Limit sharelink creation to", "create_sharelink")] for (title, field) in sharelink_options: settings_form += '<h4>%s</h4>' % title if direct_dict.get(field, False): choices = _valid_sharelink + _reset_choice else: choices = _valid_sharelink + _keep_choice for (key, val) in choices: checked = '' if settings_dict.get(field, keyword_owners) == val: checked = "checked" settings_form += ''' <input type="radio" name="%s" value="%s" %s/> %s ''' % (field, val, checked, key) settings_form += '<br/>' field = 'request_recipients' request_recipients = settings_dict.get(field, default_vgrid_settings_limit) if direct_dict.get(field, False): direct_note = _reset_note else: direct_note = _keep_note settings_form += ''' <h4>Request Recipients</h4> Notify only first <input type="number" name="request_recipients" min=0 max=999 minlength=1 maxlength=3 value=%d required /> owners about access requests %s. ''' % (request_recipients, direct_note) settings_form += '<br/>' bool_options = [ ("Hidden", "hidden"), ] for (title, field) in bool_options: settings_form += '<h4>%s</h4>' % title if direct_dict.get(field, False): choices = _valid_bool + _reset_choice else: choices = _valid_bool + _keep_choice for (key, val) in choices: checked, inherit_note = '', '' if settings_dict.get(field, False) == val: checked = "checked" if direct_dict.get(field, False) != \ settings_dict.get(field, False): inherit_note = ''' <span class="warningtext iconspace"> Forced by a parent %(vgrid_label)s. Please disable there first if you want to change the value here.</span>''' % settings_dict settings_form += ''' <input type="radio" name="%s" value="%s" %s /> %s ''' % (field, val, checked, key) settings_form += '%s<br/>' % inherit_note settings_form += '<br/>' settings_form += ''' <input type="submit" value="Save settings" /> </fieldset> </form> ''' output_objects.append({ 'object_type': 'html_form', 'text': settings_form % settings_dict }) # Checking/fixing of missing components output_objects.append({ 'object_type': 'sectionheader', 'text': "Repair/Add Components" }) target_op = 'updatevgrid' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) settings_dict.update({'target_op': target_op, 'csrf_token': csrf_token}) output_objects.append({ 'object_type': 'html_form', 'text': ''' <form method="%(form_method)s" action="%(target_op)s.py"> <input type="hidden" name="%(csrf_field)s" value="%(csrf_token)s" /> <input type="hidden" name="vgrid_name" value="%(vgrid_name)s" /> <input type="submit" value="Repair components" /> </form> ''' % settings_dict }) (owners_status, owners_direct) = vgrid_owners(vgrid_name, configuration, False) if not owners_status: logger.error("failed to load owners for %s: %s" % (vgrid_name, owners_direct)) return (output_objects, returnvalues.SYSTEM_ERROR) (members_status, members_direct) = vgrid_members(vgrid_name, configuration, False) if not members_status: logger.error("failed to load members for %s: %s" % (vgrid_name, members_direct)) return (output_objects, returnvalues.SYSTEM_ERROR) (resources_status, resources_direct) = vgrid_resources(vgrid_name, configuration, False) if not resources_status: logger.error("failed to load resources for %s: %s" % (vgrid_name, resources_direct)) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'sectionheader', 'text': "Delete %s " % vgrid_name }) if len(owners_direct) > 1 or members_direct or resources_direct: output_objects.append({ 'object_type': 'html_form', 'text': ''' To delete <b>%(vgrid)s</b> first remove all resources, members and owners ending with yourself. ''' % { 'vgrid': vgrid_name } }) else: output_objects.append({ 'object_type': 'html_form', 'text': ''' <p>As the last owner you can leave and delete <b>%(vgrid)s</b> including all associated shared files and components.<br/> </p> <p class="warningtext"> You cannot undo such delete operations, so please use with great care! </p> ''' % { 'vgrid': vgrid_name } }) target_op = "rmvgridowner" csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) js_name = 'rmlastvgridowner' helper = html_post_helper( js_name, '%s.py' % target_op, { 'vgrid_name': vgrid_name, 'cert_id': client_id, 'flags': 'f', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) output_objects.append({ 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really leave and delete %s?' % vgrid_name), 'class': 'removelink iconspace', 'title': 'Leave and delete %s' % vgrid_name, 'text': 'Leave and delete %s' % vgrid_name }) # Spacing output_objects.append({ 'object_type': 'html_form', 'text': ''' <div class="vertical-spacer"></div> ''' }) return (output_objects, returnvalues.OK)
def main(client_id, user_arguments_dict): """Main function used by front end""" (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) patterns = accepted['job_id'] 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) if not configuration.site_enable_jobs: output_objects.append({ 'object_type': 'error_text', 'text': '''Job execution is not enabled on this system''' }) return (output_objects, returnvalues.SYSTEM_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.mrsl_files_dir, client_dir)) + os.sep status = returnvalues.OK filelist = [] for pattern in patterns: pattern = pattern.strip() # Backward compatibility - all_jobs keyword should match all jobs if pattern == all_jobs: pattern = '*' # 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 + '.mRSL') 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): # 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 # Insert valid job files in filelist for later treatment match.append(abs_path) # Now actually treat list of allowed matchings and notify if no # (allowed) match^I if not match: output_objects.append({ 'object_type': 'error_text', 'text': '%s: You do not have any matching job IDs!' % pattern }) status = returnvalues.CLIENT_ERROR else: filelist += match # job feasibility is hard on the server, limit if len(filelist) > 100: output_objects.append({ 'object_type': 'error_text', 'text': 'Too many matching jobs (%s)!' % len(filelist) }) return (output_objects, returnvalues.CLIENT_ERROR) checkcondjobs = [] for filepath in filelist: # Extract job_id from filepath (replace doesn't modify filepath) mrsl_file = filepath.replace(base_dir, '') job_id = mrsl_file.replace('.mRSL', '') checkcondjob = {'object_type': 'checkcondjob', 'job_id': job_id} dict = unpickle(filepath, logger) if not dict: checkcondjob['message'] = \ ('The file containing the information ' \ 'for job id %s could not be opened! ' \ 'You can only check feasibility of ' \ 'your own jobs!' ) % job_id checkcondjobs.append(checkcondjob) status = returnvalues.CLIENT_ERROR continue # Is the job status pending? possible_check_states = ['QUEUED', 'RETRY', 'FROZEN'] if not dict['STATUS'] in possible_check_states: checkcondjob['message'] = \ 'You can only check feasibility of jobs with status: %s.'\ % ' or '.join(possible_check_states) checkcondjobs.append(checkcondjob) continue # Actually check feasibility feasible_res = job_feasibility(configuration, dict) checkcondjob.update(feasible_res) checkcondjobs.append(checkcondjob) output_objects.append({ 'object_type': 'checkcondjobs', 'checkcondjobs': checkcondjobs }) 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) defaults = signature()[1] output_objects.append({ 'object_type': 'header', 'text': 'Remove Resource Owner' }) (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) unique_resource_name = accepted['unique_resource_name'][-1] cert_id = accepted['cert_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) if not is_owner(client_id, unique_resource_name, configuration.resource_home, logger): output_objects.append({ 'object_type': 'error_text', 'text': 'You must be an owner of %s to remove another owner!' % unique_resource_name }) return (output_objects, returnvalues.CLIENT_ERROR) # is_owner incorporates unique_resource_name verification - no need to # specifically check for illegal directory traversal if not is_user(cert_id, configuration.mig_server_home): output_objects.append({'object_type': 'error_text', 'text' : '%s is not a valid %s user!' % \ (cert_id, configuration.short_title) }) return (output_objects, returnvalues.CLIENT_ERROR) # reject remove if cert_id is not an owner if not resource_is_owner(unique_resource_name, cert_id, configuration): output_objects.append({ 'object_type': 'error_text', 'text': '%s is not an owner of %s.' % (cert_id, unique_resource_name) }) return (output_objects, returnvalues.CLIENT_ERROR) # Remove owner (rm_status, rm_msg) = resource_remove_owners(configuration, unique_resource_name, [cert_id]) if not rm_status: output_objects.append({ 'object_type': 'error_text', 'text': 'Could not remove owner, reason: %s' % rm_msg }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': '%s was successfully removed and is no longer an owner of %s!' % (cert_id, unique_resource_name) }) output_objects.append({'object_type': 'link', 'destination': 'resadmin.py?unique_resource_name=%s' % \ unique_resource_name, 'class': 'adminlink iconspace', 'title': 'Administrate resource', 'text': 'Manage resource'}) return (output_objects, returnvalues.OK)
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) title_entry = find_entry(output_objects, 'title') title_entry['text'] = 'Delete runtime environment' output_objects.append({ 'object_type': 'header', 'text': 'Delete runtime environment' }) 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) re_name = accepted['re_name'][-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) if not valid_dir_input(configuration.re_home, re_name): logger.warning( "possible illegal directory traversal attempt re_name '%s'" % re_name) output_objects.append({ 'object_type': 'error_text', 'text': 'Illegal runtime environment name: "%s"' % re_name }) return (output_objects, returnvalues.CLIENT_ERROR) # Check whether re_name represents a runtime environment if not is_runtime_environment(re_name, configuration): output_objects.append({ 'object_type': 'error_text', 'text': "No such runtime environment: '%s'" % re_name }) return (output_objects, returnvalues.CLIENT_ERROR) (re_dict, load_msg) = get_re_dict(re_name, configuration) if not re_dict: output_objects.append({ 'object_type': 'error_text', 'text': 'Could not read runtime environment details for %s: %s' % (re_name, load_msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) # Make sure the runtime environment belongs to the user trying to delete it if client_id != re_dict['CREATOR']: output_objects.append({'object_type': 'error_text', 'text': \ 'You are not the owner of runtime environment "%s"' % re_name}) return (output_objects, returnvalues.CLIENT_ERROR) # Prevent delete if the runtime environment is used by any resources actives = resources_using_re(configuration, re_name) # If the runtime environment is active, an error message is printed, along # with a list of the resources using the runtime environment if actives: output_objects.append({ 'object_type': 'error_text', 'text': "Can't delete runtime environment '%s' in use by resources:" % re_name }) output_objects.append({'object_type': 'list', 'list': actives}) output_objects.append({ 'object_type': 'link', 'destination': 'redb.py', 'class': 'infolink iconspace', 'title': 'Show runtime environments', 'text': 'Show runtime environments' }) return (output_objects, returnvalues.CLIENT_ERROR) # Delete the runtime environment (del_status, msg) = delete_runtimeenv(re_name, configuration) # If something goes wrong when trying to delete runtime environment # re_name, an error is displayed. if not del_status: output_objects.append({ 'object_type': 'error_text', 'text': 'Could not remove %s runtime environment: %s' % (re_name, msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) # If deletion of runtime environment re_name is successful, we just # return OK else: output_objects.append({ 'object_type': 'text', 'text': 'Successfully deleted runtime environment: "%s"' % re_name }) output_objects.append({ 'object_type': 'link', 'destination': 'redb.py', 'class': 'infolink iconspace', 'title': 'Show runtime environments', 'text': 'Show runtime environments' }) return (output_objects, returnvalues.OK)