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) output_objects.append( {'object_type': 'header', 'text': 'Public project links'}) 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) vgrid_public_base = configuration.vgrid_public_base linklist = [] for public_vgrid_dir in os.listdir(vgrid_public_base): if os.path.exists(os.path.join(vgrid_public_base, public_vgrid_dir, 'index.html')): # public project listing is enabled, link to the vgrid public page new_link = {'object_type': 'link', 'text': public_vgrid_dir, 'destination': '%s/vgrid/%s/path/index.html' % (get_site_base_url(configuration), public_vgrid_dir)} linklist.append(new_link) output_objects.append({'object_type': 'linklist', 'links': linklist}) 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, op_title=False, op_menu=client_id) defaults = signature()[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) iosessionid = accepted['iosessionid'][-1] sandboxkey = accepted['sandboxkey'][-1] exe_name = accepted['exe_name'][-1] status = returnvalues.OK # Web format for cert access and no header for SID access if client_id: output_objects.append({ 'object_type': 'title', 'text': 'SSS job activity checker' }) output_objects.append({ 'object_type': 'header', 'text': 'SSS job activity checker' }) else: output_objects.append({'object_type': 'start'}) # check that the job exists, iosessionid is ok (does symlink exist?) if iosessionid and os.path.islink(configuration.webserver_home + iosessionid): msg = 'jobactive' else: if sandboxkey and exe_name: (result, msg) = \ get_sandbox_exe_stop_command(configuration.sandbox_home, sandboxkey, exe_name, logger) if result: msg = 'stop_command: %s' % msg else: msg = 'jobinactive' status = returnvalues.ERROR # Status code line followed by raw output if not client_id: output_objects.append({'object_type': 'script_status', 'text': ''}) output_objects.append({ 'object_type': 'binary', 'data': '%s' % status[0] }) output_objects.append({'object_type': 'binary', 'data': msg}) 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) logger = configuration.logger logger.debug('oiddiscover: %s' % user_arguments_dict) output_objects.append({'object_type': 'header', 'text': 'OpenID Discovery for %s' % configuration.short_title}) 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) # Force to raw file output unless something else is explicitly requested. # Relies on delay_format option in run_cgi_script_possibly_with_cert if not user_arguments_dict.get('output_format', []): user_arguments_dict['output_format'] = ['file'] discovery_doc = generate_openid_discovery_doc(configuration) output_objects.append({'object_type': 'text', 'text': 'Advertising valid OpenID endpoints:'}) # make sure we always have at least one output_format entry output_format = user_arguments_dict.get('output_format', []) + ['file'] if output_format[0] == 'file': headers = [('Content-Type', 'application/xrds+xml'), ('Content-Disposition', 'attachment; filename=oid.xrds'), ('Content-Length', '%s' % len(discovery_doc))] start_entry = find_entry(output_objects, 'start') start_entry['headers'] = headers # output discovery_doc as raw xrds doc in any case output_objects.append({'object_type': 'file_output', 'lines': [discovery_doc]}) else: output_objects.append({'object_type': 'binary', 'data': discovery_doc}) 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, op_menu=client_id) defaults = signature()[1] (validate_status, accepted) = validate_input( user_arguments_dict, defaults, output_objects, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) show = accepted['show'][-1].lower() search = accepted['search'][-1].lower() # Topic to generator-function mapping - add new topics here by adding an # anchor_name -> {'title': topic, 'generator': generator_function, # 'arg': generator_args} # entry. default_args = (configuration, output_objects) all_docs = { 'mrsl': {'title': 'Job description: mRSL', 'generator': mrsl_keywords, 'args': default_args}, 'resconf': {'title': 'Resource configuration', 'generator': resconf_keywords, 'args': default_args}, 'outformats': {'title': 'Valid outputformats', 'generator': valid_outputformats, 'args': default_args}, 'runtimeenv': {'title': 'Runtime Environments', 'generator': runtime_environments, 'args': default_args}, 'credits': {'title': 'License and Acknowledgements', 'generator': license_information, 'args': default_args}, } output_objects.append({'object_type': 'header', 'text': '%s On-demand Documentation' % configuration.short_title}) if not show: output_objects.append({'object_type': 'text', 'text': ''' This is the integrated help system for %s. You can search for a documentation topic or select the particular section directly. Please note that the integrated help is rather limited to short overviews and technical specifications.''' % configuration.short_title}) output_objects.append({'object_type': 'text', 'text': ''' You can find more user friendly tutorials and examples on the official site support pages:'''}) output_objects.append({'object_type': 'link', 'destination': configuration.site_external_doc, 'class': 'urllink iconspace', 'title': 'external documentation', 'text': 'external %s documentation' % configuration.site_title, 'plain_text': configuration.site_external_doc}) html = '<br />Filter (using *,? etc.)' html += "<form method='get' action='docs.py'>" html += "<input type='hidden' name='show' value='' />" html += "<input type='text' name='search' value='' />" html += "<input type='submit' value='Filter' />" html += '</form><br />' output_objects.append({'object_type': 'html_form', 'text': html}) # Fall back to show all topics if not search and not show: search = '*' if search: # Pattern matching: select all topics that _contain_ search pattern # i.e. like re.search rather than re.match search_keys = [] for (key, val) in all_docs.items(): # Match any prefix and suffix. # No problem with extra '*'s since'***' also matches 'a') topic = val['title'] if fnmatch.fnmatch(topic.lower(), '*' + search + '*'): search_keys.append(key) output_objects.append( {'object_type': 'header', 'text': 'Documentation topics:'}) for key in search_keys: display_topic(output_objects, key, all_docs) if not search_keys: output_objects.append( {'object_type': 'text', 'text': 'No topics matching %s' % search}) if show: # Pattern matching: select all topics that _contain_ search pattern # i.e. like re.search rather than re.match show_keys = [] for (key, val) in all_docs.items(): # Match any prefix and suffix. # No problem with extra '*'s since'***' also matches 'a') topic = val['title'] logger.info("match show %s vs %s or %s" % (show, topic, key)) if fnmatch.fnmatch(topic.lower(), '*' + show + '*') or \ show == key: logger.info("found show match for %s" % show) show_keys.append(key) for key in show_keys: logger.info("show doc for %s" % key) display_doc(output_objects, key, all_docs) if not show_keys: output_objects.append( {'object_type': 'text', 'text': 'No topics matching %s' % show}) 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, op_title=False, op_menu=client_id) defaults = signature()[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) remote_ip = str(os.getenv('REMOTE_ADDR')) unique_resource_name = accepted['unique_resource_name'][-1] exe = accepted['exe'][-1] cputime = int(accepted['cputime'][-1]) nodecount = int(accepted['nodecount'][-1]) localjobname = accepted['localjobname'][-1] sandboxkey = accepted['sandboxkey'][-1] execution_delay = int(accepted['execution_delay'][-1]) exe_pgid = int(accepted['exe_pgid'][-1]) status = returnvalues.OK if not configuration.site_enable_resources: output_objects.append({ 'object_type': 'error_text', 'text': "Resources are not enabled on this site" }) return (output_objects, returnvalues.CLIENT_ERROR) # No header and footer here output_objects.append({'object_type': 'start'}) output_objects.append({'object_type': 'script_status', 'text': ''}) # Please note that base_dir must end in slash to avoid access to other # resource dirs when own name is a prefix of another resource name base_dir = os.path.abspath( os.path.join(configuration.resource_home, unique_resource_name)) + os.sep if not is_resource(unique_resource_name, configuration.resource_home): output_objects.append({ 'object_type': 'error_text', 'text': "Failure: You must be an owner of '%s' to get the PGID!" % unique_resource_name }) return (output_objects, returnvalues.CLIENT_ERROR) # is_resource incorporates unique_resource_name verification - no need to # specifically check for illegal directory traversal on that variable. (load_status, resource_conf) = \ get_resource_configuration(configuration.resource_home, unique_resource_name, logger) if not load_status: logger.error("Invalid requestnewjob - no resouce_conf for: %s : %s" % (unique_resource_name, resource_conf)) output_objects.append({ 'object_type': 'error_text', 'text': 'invalid request: no such resource!' }) return (output_objects, returnvalues.CLIENT_ERROR) # Check that resource address matches request source to make DoS harder proxy_fqdn = resource_conf.get('FRONTENDPROXY', None) try: check_source_ip(remote_ip, unique_resource_name, proxy_fqdn) except ValueError as vae: logger.error("Invalid requestnewjob: %s (%s)" % (vae, accepted)) output_objects.append({ 'object_type': 'error_text', 'text': 'invalid request: %s' % vae }) return (output_objects, returnvalues.CLIENT_ERROR) if resource_conf.get('SANDBOX', False): if sandboxkey == '': logger.error("Missing sandboxkey for sandbox resource: %s" % unique_resource_name) output_objects.append({ 'object_type': 'error_text', 'text': 'sandbox must set sandboxkey in job requests!' }) return (output_objects, returnvalues.CLIENT_ERROR) # resource is a sandbox and a sandboxkey was received if resource_conf['SANDBOXKEY'] != sandboxkey: logger.error("Incorrect sandboxkey for sandbox resource: %s : %s" % (unique_resource_name, sandboxkey)) output_objects.append({ 'object_type': 'error_text', 'text': 'sandbox provided an invalid sandboxkey!' }) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: add full session ID check here # If the server is under heavy usage, requestjobs might come too fast and # thereby make the usage even heavier. The resource request a new job again # because the requestjob process is finished, resulting in more load. A # resource should not be able to have more than one job request at a time. # A "jobrequest_pending" file in the resource's home directory means that # a requestnewjob is processed. The file is also deleted when a resource # is started. This locking by file is not good if the MiG server runs on a # NFS file system. The lock file contains a timestamp used to autoexpire # old locks if a job wasn't handed out within the requested cputime. lock_file = os.path.abspath( os.path.join(base_dir, 'jobrequest_pending.%s' % exe)) filehandle = None now = time.time() try: lock_until = now + min(300.0, float(cputime)) except Exception as exc: logger.error('invalid cputime in requestnewjob: %s (%s)' % (cputime, exc)) output_objects.append({ 'object_type': 'error_text', 'text': 'invalid cputime: %s - must be a number!' % cputime }) return (output_objects, returnvalues.CLIENT_ERROR) try: filehandle = open(lock_file, 'r+') except IOError as ioe: output_objects.append({ 'object_type': 'text', 'text': 'No jobrequest_pending.%s lock found - creating one' % exe }) if filehandle: try: fcntl.flock(filehandle.fileno(), fcntl.LOCK_EX) filehandle.seek(0, 0) lock_content = filehandle.read() if lock_content: try: expire_time = float(lock_content) except Exception: # Old expire file - force lock to expire expire_time = 0.0 if now < expire_time: logger.error('invalid cputime in requestnewjob: %s' % cputime) output_objects.append({ 'object_type': 'error_text', 'text': 'requestnewjob is locked until last requestnewjob for' 'this exe (%s) has returned.' % exe }) return (output_objects, returnvalues.CLIENT_ERROR) else: logger.info('requestnewjob found expired lock ' '(%.2f < %.2f) - allowing new request.' % (now, expire_time)) filehandle.seek(0, 0) filehandle.write('%.2f' % lock_until) filehandle.close() except IOError as ioe: logger.error('Could not get exclusive lock in requestnewjob') output_objects.append({ 'object_type': 'error_text', 'text': '''Could not get exclusive lock. Your last job request for %s has been received on the %s server. The job should be available shortly. If you receive this message often, please increase the timeout for job requests.''' % (exe, configuration.short_title) }) return (output_objects, returnvalues.CLIENT_ERROR) else: # create file try: filehandle = open(lock_file, 'w') fcntl.flock(filehandle.fileno(), fcntl.LOCK_EX) filehandle.seek(0, 0) filehandle.write('%.2f' % lock_until) filehandle.close() except IOError as ioe: logger.error('Failed to create jobrequest_pending lock: %s' % ioe) output_objects.append({ 'object_type': 'error_text', 'text': 'Failed to create jobrequest_pending lock!' }) return (output_objects, returnvalues.ERROR) # Tell "grid_script" that the resource requests a job message = 'RESOURCEREQUEST %s %s %s %s %s %s %s\n' % ( exe, unique_resource_name, cputime, nodecount, localjobname, execution_delay, exe_pgid, ) if not send_message_to_grid_script(message, logger, configuration): logger.error('could not send resource request for %s to grid_script!' % unique_resource_name) output_objects.append({ 'object_type': 'error_text', 'text': 'Fatal error: could not handle resource job request' }) return (output_objects, returnvalues.ERROR) output_objects.append({ 'object_type': 'text', 'text': 'REQUESTNEWJOB OK. The job will ' 'be sent to the resource: %s.%s %s %s (sandboxkey: %s)' % (unique_resource_name, exe, str(exe_pgid), os.getenv('REMOTE_ADDR'), sandboxkey) }) 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)
o.out('Please use HTTPS with session id for authenticating job requests!') o.reply_and_exit(o.ERROR) # TODO: add session ID check here remote_ip = str(os.getenv('REMOTE_ADDR')) fieldstorage = cgi.FieldStorage() user_arguments_dict = fieldstorage_to_dict(fieldstorage) defaults = signature()[1] output_objects = [] # IMPORTANT: validate all input args before doing ANYTHING with them! (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: logger.error("input validation for %s failed: %s" % (client_id, accepted)) o.out('Invalid input arguments received!') o.reply_and_exit(o.ERROR) exe = accepted['exe'][-1] unique_resource_name = accepted['unique_resource_name'][-1] cputime = accepted['cputime'][-1] nodecount = accepted['nodecount'][-1] localjobname = accepted['localjobname'][-1] #sandboxkey = accepted['sandboxkey'][-1] execution_delay = accepted['execution_delay'][-1]
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) defaults = signature(configuration)[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) openid_error = ', '.join(accepted['modauthopenid.error']) openid_referrer = accepted['modauthopenid.referrer'][-1] # OpenID server errors may return here with redirect url set redirect_url = accepted['redirect_url'][-1] main_url = os.path.join(configuration.migserver_https_sid_url, 'public/') client_addr = environ.get('REMOTE_ADDR', '0.0.0.0') client_refer = environ.get('HTTP_REFERER', '') title_entry = find_entry(output_objects, 'title') title_entry[ 'text'] = '%s OpenID Response Handler' % configuration.short_title add_import = ''' <script type="text/javascript" src="/images/js/jquery.ajaxhelpers.js"></script> ''' add_init = ''' function delayed_redirect(redir_url, redirmsg_select) { setTimeout(function() { $(redirmsg_select).fadeIn(4000); }, 1000); setTimeout(function() { location.href=redir_url; }, 15000); } ''' add_ready = ''' var action = "login", oid_title, oid_url, tag_prefix; oid_title = "%s"; oid_url = "%s"; tag_prefix = "extoid_"; check_oid_available(action, oid_title, oid_url, tag_prefix); oid_title = "%s"; var oid_url = "%s"; tag_prefix = "migoid_"; check_oid_available(action, oid_title, oid_url, tag_prefix); ''' % (configuration.user_ext_oid_title, configuration.user_ext_oid_provider, configuration.user_mig_oid_title, configuration.user_mig_oid_provider) title_entry['script']['advanced'] += add_import title_entry['script']['init'] += add_init title_entry['script']['ready'] += add_ready title_entry['skipmenu'] = True # NOTE: keep empty header for narrow page layout header_entry = {'object_type': 'header', 'text': ''} output_objects.append(header_entry) logger.info('oidresponse for %r at %s from %r: %s' % (client_id, client_addr, openid_referrer, openid_error)) html = "" add_action = ''' <p class="fadein_msg spinner iconleftpad iconspace hidden"> Redirecting to main page in a moment ... </p> <script type="text/javascript"> delayed_redirect("%s", ".fadein_msg"); </script> ''' % main_url err_txt, report_txt = '', '' report_fail = """, so you cannot currently use it for authentication to %s. <br /> Please report the problem to your OpenID identity provider if it persists. """ % configuration.short_title if redirect_url: report_fail += """<br />The error happened on access to %s .""" \ % redirect_url if 'no_idp_found' in openid_error: err_txt += "OpenID server did not respond!" report_txt += """It appears the requested OpenID authentication service is offline""" + report_fail elif 'canceled' in openid_error: # Sign up requests after login end here and may need autologout if openid_referrer.find('/autocreate.py') != -1: logger.info('oidresponse force autologut for %r at %s from %r' % (client_id, client_addr, openid_referrer)) logger.debug('oidresponse env: %s' % environ) err_txt += "OpenID sign up reached an inconsistent login state" report_txt += """Sign up to %s only works if you are <em>not</em> already logged in to the OpenID service. """ % configuration.short_title identity = environ.get('REMOTE_USER', None) # NOTE: for cgi-sid we don't have the oid username so use a dummy if not identity: if configuration.user_mig_oid_provider and \ openid_referrer.startswith( configuration.migserver_https_mig_oid_url): identity = configuration.user_mig_oid_provider identity = os.path.join(identity, client_id) elif configuration.user_ext_oid_provider and \ openid_referrer.startswith( configuration.migserver_https_ext_oid_url): identity = configuration.user_ext_oid_provider identity = os.path.join(identity, client_id) else: logger.error('oidresponse from unexpected ref: %s' % client_refer) if identity: autologout_url = openid_basic_logout_url( configuration, identity, main_url) add_action = ''' <p class="fadein_msg spinner iconleftpad iconspace hidden"> Redirecting through auto-logout to clean up for your next %s sign up attempt ... </p> <script type="text/javascript"> delayed_redirect("%s", ".fadein_msg"); </script> ''' % (configuration.short_title, autologout_url) logger.info('oidresponse for %r at %s from %r with action: %s' % (client_id, client_addr, openid_referrer, add_action)) else: err_txt += "OpenID login canceled!" report_txt += """You have to either enter your OpenID login and accept that it is used for %s login or choose another login method. """ % configuration.short_title else: err_txt += "OpenID server error!" report_txt += """It appears there's a problem with the requested OpenID authentication service""" + report_fail html += """<h2>OpenID Service Reported an Error</h2> <div class='errortext'> %s (error code(s): %s) </div> <div class='warningtext'> %s </div> """ % (err_txt, openid_error, report_txt) var_map = { 'migoid_url': configuration.migserver_https_mig_oid_url, 'migoid_title': configuration.user_mig_oid_title, 'extoid_url': configuration.migserver_https_ext_oid_url, 'extoid_title': configuration.user_ext_oid_title, 'migcert_url': configuration.migserver_https_mig_cert_url, 'migcert_title': configuration.user_mig_cert_title, 'extcert_url': configuration.migserver_https_ext_cert_url, 'extcert_title': configuration.user_ext_cert_title, 'short_title': configuration.short_title } output_objects.append({'object_type': 'html_form', 'text': html % var_map}) if add_action: output_objects.append({'object_type': 'html_form', 'text': add_action}) 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) defaults = signature()[1] (validate_status, accepted) = validate_input( user_arguments_dict, defaults, output_objects, allow_rejects=False, # NOTE: path can use wildcards, 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'] current_dir = accepted['current_dir'][-1].lstrip('/') share_id = accepted['share_id'][-1] status = returnvalues.OK read_mode, write_mode = True, True # Either authenticated user client_id set or sharelink ID if client_id: user_id = client_id target_dir = client_id_dir(client_id) base_dir = configuration.user_home redirect_name = configuration.site_user_redirect redirect_path = redirect_name id_args = '' root_link_name = 'USER HOME' main_class = "user_ls" page_title = 'User Files' userstyle = True widgets = True visibility_mods = ''' .%(main_class)s .disable_read { display: none; } .%(main_class)s .disable_write { display: none; } ''' 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) # then include shared by %(owner)s on page header user_id = 'anonymous user through share ID %s' % share_id target_dir = os.path.join(share_mode, share_id) base_dir = configuration.sharelink_home redirect_name = 'share_redirect' redirect_path = os.path.join(redirect_name, share_id) id_args = 'share_id=%s;' % share_id root_link_name = '%s' % share_id main_class = "sharelink_ls" page_title = 'Shared Files' userstyle = False widgets = False # default to include file info if flags == '': flags += 'f' if share_mode == 'read-only': write_mode = False visibility_mods = ''' .%(main_class)s .enable_write { display: none; } .%(main_class)s .disable_read { display: none; } ''' elif share_mode == 'write-only': read_mode = False visibility_mods = ''' .%(main_class)s .enable_read { display: none; } .%(main_class)s .disable_write { display: none; } ''' else: visibility_mods = ''' .%(main_class)s .disable_read { display: none; } .%(main_class)s .disable_write { display: none; } ''' 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) visibility_toggle = ''' <style> %s </style> ''' % (visibility_mods % { 'main_class': main_class }) # 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 if not os.path.isdir(base_dir): logger.error('%s called on missing base_dir: %s' % (op_name, base_dir)) output_objects.append({ 'object_type': 'error_text', 'text': 'No such %s!' % page_title.lower() }) return (output_objects, returnvalues.CLIENT_ERROR) title_entry = find_entry(output_objects, 'title') title_entry['text'] = page_title title_entry['skipwidgets'] = not widgets title_entry['skipuserstyle'] = not userstyle user_settings = title_entry.get('user_settings', {}) open_button_id = 'open_fancy_upload' form_method = 'post' csrf_limit = get_csrf_limit(configuration) fill_helpers = { 'dest_dir': current_dir + os.sep, 'share_id': share_id, 'flags': flags, 'tmp_flags': flags, 'long_set': long_list(flags), 'recursive_set': recursive(flags), 'all_set': all(flags), 'fancy_open': open_button_id, 'fancy_dialog': fancy_upload_html(configuration), 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit } target_op = 'uploadchunked' 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}) (cf_import, cf_init, cf_ready) = confirm_js(configuration) (fu_import, fu_init, fu_ready) = fancy_upload_js(configuration, 'function() { location.reload(); }', share_id, csrf_token) add_import = ''' %s %s ''' % (cf_import, fu_import) add_init = ''' %s %s %s %s ''' % (cf_init, fu_init, select_all_javascript(), selected_file_actions_javascript()) add_ready = ''' %s %s /* wrap openFancyUpload in function to avoid event data as argument */ $("#%s").click(function() { openFancyUpload(); }); $("#checkall_box").click(toggleChecked); ''' % (cf_ready, fu_ready, open_button_id) # TODO: can we update style inline to avoid explicit themed_styles? styles = themed_styles( configuration, advanced=['jquery.fileupload.css', 'jquery.fileupload-ui.css'], skin=['fileupload-ui.custom.css'], user_settings=user_settings) styles['advanced'] += ''' %s ''' % visibility_toggle title_entry['style'] = styles title_entry['script']['advanced'] += add_import title_entry['script']['init'] += add_init title_entry['script']['ready'] += add_ready title_entry['script']['body'] = ' class="%s"' % main_class output_objects.append({'object_type': 'header', 'text': page_title}) # TODO: move to output html handler output_objects.append({ 'object_type': 'html_form', 'text': confirm_html(configuration) }) # Shared URL helpers ls_url_template = 'ls.py?%scurrent_dir=%%(rel_dir_enc)s;flags=%s' % \ (id_args, flags) csrf_token = make_csrf_token(configuration, form_method, 'rm', client_id, csrf_limit) rm_url_template = 'rm.py?%spath=%%(rel_path_enc)s;%s=%s' % \ (id_args, csrf_field, csrf_token) rmdir_url_template = 'rm.py?%spath=%%(rel_path_enc)s;flags=r;%s=%s' % \ (id_args, csrf_field, csrf_token) editor_url_template = 'editor.py?%spath=%%(rel_path_enc)s' % id_args redirect_url_template = '/%s/%%(rel_path_enc)s' % redirect_path location_pre_html = """ <div class='files'> <table class='files'> <tr class=title><td class=centertext> Working directory: </td></tr> <tr><td class='centertext'> """ output_objects.append({ 'object_type': 'html_form', 'text': location_pre_html }) # Use current_dir nav location links for pattern in pattern_list[:1]: links = [] links.append({ 'object_type': 'link', 'text': root_link_name, 'destination': ls_url_template % { 'rel_dir_enc': '.' } }) prefix = '' parts = os.path.normpath(current_dir).split(os.sep) for i in parts: if i == ".": continue prefix = os.path.join(prefix, i) links.append({ 'object_type': 'link', 'text': i, 'destination': ls_url_template % { 'rel_dir_enc': quote(prefix) } }) output_objects.append({ 'object_type': 'multilinkline', 'links': links, 'sep': ' %s ' % os.sep }) location_post_html = """ </td></tr> </table> </div> <br /> """ output_objects.append({ 'object_type': 'html_form', 'text': location_post_html }) more_html = """ <div class='files if_full'> <form method='%(form_method)s' name='fileform' onSubmit='return selectedFilesAction();'> <table class='files'> <tr class=title><td class=centertext colspan=2> Advanced file actions </td></tr> <tr><td> Action on paths selected below (please hold mouse cursor over button for a description): </td> <td class=centertext> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='v' /> <input type='submit' title='Show concatenated contents (cat)' onClick='document.pressed=this.value' value='cat' /> <input type='submit' onClick='document.pressed=this.value' value='head' title='Show first lines (head)' /> <input type='submit' onClick='document.pressed=this.value' value='tail' title='Show last lines (tail)' /> <input type='submit' onClick='document.pressed=this.value' value='wc' title='Count lines/words/chars (wc)' /> <input type='submit' onClick='document.pressed=this.value' value='stat' title='Show details (stat)' /> <input type='submit' onClick='document.pressed=this.value' value='touch' title='Update timestamp (touch)' /> <input type='submit' onClick='document.pressed=this.value' value='truncate' title='truncate! (truncate)' /> <input type='submit' onClick='document.pressed=this.value' value='rm' title='delete! (rm)' /> <input type='submit' onClick='document.pressed=this.value' value='rmdir' title='Remove directory (rmdir)' /> <input type='submit' onClick='document.pressed=this.value' value='submit' title='Submit file (submit)' /> </td></tr> </table> </form> </div> """ % { 'form_method': form_method } output_objects.append({'object_type': 'html_form', 'text': more_html}) dir_listings = [] output_objects.append({ 'object_type': 'dir_listings', 'dir_listings': dir_listings, 'flags': flags, 'redirect_name': redirect_name, 'redirect_path': redirect_path, 'share_id': share_id, 'ls_url_template': ls_url_template, 'rm_url_template': rm_url_template, 'rmdir_url_template': rmdir_url_template, 'editor_url_template': editor_url_template, 'redirect_url_template': redirect_url_template, }) first_match = None 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 current_path = os.path.normpath(os.path.join(base_dir, current_dir)) unfiltered_match = glob.glob(current_path + 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): logger.warning('%s tried to %s restricted path %s ! (%s)' % (user_id, op_name, abs_path, pattern)) continue match.append(abs_path) if not first_match: first_match = 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 # Never show any ls output in write-only mode (css hide is not enough!) if not read_mode: continue for abs_path in match: if abs_path + os.sep == base_dir: relative_path = '.' else: relative_path = abs_path.replace(base_dir, '') entries = [] dir_listing = { 'object_type': 'dir_listing', 'relative_path': relative_path, 'entries': entries, 'flags': flags, } try: gdp_iolog(configuration, client_id, environ['REMOTE_ADDR'], 'accessed', [relative_path]) except GDPIOLogError 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)) continue handle_ls(configuration, output_objects, entries, base_dir, abs_path, flags, 0) dir_listings.append(dir_listing) output_objects.append({ 'object_type': 'html_form', 'text': """<br/> <div class='files disable_read'> <p class='info icon'>""" }) # Shared message for text (e.g. user scripts) and html-format if not read_mode: # Please note that we use verbatim to get info icon right in html output_objects.append({ 'object_type': 'verbatim', 'text': """ This is a write-only share so you do not have access to see the files, only upload data and create directories. """ }) output_objects.append({ 'object_type': 'html_form', 'text': """ </p> </div> <div class='files enable_read'> <form method='get' action='ls.py'> <table class='files'> <tr class=title><td class=centertext> Filter paths (wildcards like * and ? are allowed) <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> <input type='text' name='path' value='' /> <input type='submit' value='Filter' /> </td></tr> </table> </form> </div> """ % fill_helpers }) # Short/long format buttons fill_helpers['tmp_flags'] = flags + 'l' htmlform = """ <table class='files if_full'> <tr class=title><td class=centertext colspan=4> File view options </td></tr> <tr><td colspan=4><br /></td></tr> <tr class=title><td>Parameter</td><td>Setting</td><td>Enable</td><td>Disable</td></tr> <tr><td>Long format</td><td> %(long_set)s</td><td> <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />" % entry fill_helpers['tmp_flags'] = flags.replace('l', '') htmlform += """ <input type='submit' value='On' /><br /> </form> </td><td> <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />" % entry htmlform += """ <input type='submit' value='Off' /><br /> </form> </td></tr> """ # Recursive output fill_helpers['tmp_flags'] = flags + 'r' htmlform += """ <!-- Non-/recursive list buttons --> <tr><td>Recursion</td><td> %(recursive_set)s</td><td>""" % fill_helpers htmlform += """ <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />" % entry fill_helpers['tmp_flags'] = flags.replace('r', '') htmlform += """ <input type='submit' value='On' /><br /> </form> </td><td> <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />"\ % entry htmlform += """ <input type='submit' value='Off' /><br /> </form> </td></tr> """ htmlform += """ <!-- Show dot files buttons --> <tr><td>Show hidden files</td><td> %(all_set)s</td><td>""" % fill_helpers fill_helpers['tmp_flags'] = flags + 'a' htmlform += """ <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />" % entry fill_helpers['tmp_flags'] = flags.replace('a', '') htmlform += """ <input type='submit' value='On' /><br /> </form> </td><td> <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />" % entry htmlform += """ <input type='submit' value='Off' /><br /> </form> </td></tr> </table> """ # show flag buttons after contents to limit clutter output_objects.append({'object_type': 'html_form', 'text': htmlform}) # create additional action forms if first_match: htmlform = """ <br /> <div class='files disable_write'> <p class='info icon'> This is a read-only share so you do not have access to edit or add files, only view data. </p> </div> <table class='files enable_write if_full'> <tr class=title><td class=centertext colspan=3> Edit file </td></tr> <tr><td> Fill in the path of a file to edit and press 'edit' to open that file in the<br /> online file editor. Alternatively a file can be selected for editing through<br /> the listing of personal files. </td><td colspan=2 class=righttext> <form name='editor' method='get' action='editor.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> <input type='text' name='path' size=50 value='' required /> <input type='submit' value='edit' /> </form> </td></tr> </table> <br />""" % fill_helpers target_op = 'mkdir' 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}) htmlform += """ <table class='files enable_write'> <tr class=title><td class=centertext colspan=4> Create directory </td></tr> <tr><td> Name of new directory to be created in current directory (%(dest_dir)s) </td><td class=righttext colspan=3> <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='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> <input name='path' size=50 required /> <input type='submit' value='Create' name='mkdirbutton' /> </form> </td></tr> </table> <br /> """ % fill_helpers target_op = 'textarea' 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}) htmlform += """ <form enctype='multipart/form-data' method='%(form_method)s' action='%(target_op)s.py'> <input type='hidden' name='%(csrf_field)s' value='%(csrf_token)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <table class='files enable_write if_full'> <tr class='title'><td class=centertext colspan=4> Upload file </td></tr> <tr><td colspan=4> Upload file to current directory (%(dest_dir)s) </td></tr> <tr class='if_full'><td colspan=2> Extract package files (.zip, .tar.gz, .tar.bz2) </td><td colspan=2> <input type=checkbox name='extract_0' /> </td></tr> <tr class='if_full'><td colspan=2> Submit mRSL files (also .mRSL files included in packages) </td><td colspan=2> <input type=checkbox name='submitmrsl_0' /> </td></tr> <tr><td> File to upload </td><td class=righttext colspan=3> <input name='fileupload_0_0_0' type='file'/> </td></tr> <tr><td> Optional remote filename (extra useful in windows) </td><td class=righttext colspan=3> <input name='default_remotefilename_0' type='hidden' value='%(dest_dir)s'/> <input name='remotefilename_0' type='text' size='50' value='%(dest_dir)s'/> <input type='submit' value='Upload' name='sendfile'/> </td></tr> </table> </form> %(fancy_dialog)s <table class='files enable_write'> <tr class='title'><td class='centertext'> Upload files efficiently (using chunking). </td></tr> <tr><td class='centertext'> <button id='%(fancy_open)s'>Open Upload dialog</button> </td></tr> </table> <script type='text/javascript' > setUploadDest('%(dest_dir)s'); </script> """ % fill_helpers output_objects.append({'object_type': 'html_form', 'text': htmlform}) 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=client_id) output_objects.append({ 'object_type': 'header', 'text': '%s One-click resource' % configuration.short_title }) 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) debug = ('true' == accepted['debug'][0].lower()) console = ('true' == accepted['console'][0].lower()) if not configuration.site_enable_sandboxes: output_objects.append({ 'object_type': 'text', 'text': '''Sandbox resources 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) (status, result) = get_resource(client_id, configuration, logger) if not status: output_objects.append({'object_type': 'html_form', 'text': result}) return (output_objects, returnvalues.CLIENT_ERROR) fields = { 'sandboxkey': result[0], 'resource_name': result[1], 'cookie': result[2], 'cputime': result[3], 'codebase': '%s/sid_redirect/%s.oneclick/'\ % (configuration.migserver_https_sid_url, result[0]), 'oneclick_code': 'MiG.oneclick.Applet.class', 'resource_code': 'MiG.oneclick.Resource.class', 'oneclick_archive': 'MiGOneClickCodebase.jar', 'info_code': 'JavaInfoApplet.class', 'info_archive': '', 'server': configuration.migserver_https_sid_url, 'site' : configuration.short_title, } if debug: body = """ DEBUG input vars: %s """ % fields output_objects.append({'object_type': 'text', 'text': body}) elif console: body = \ """ codebase: %(codebase)s code: %(resource_code)s archive: %(oneclick_archive)s server: %(server)s sandboxkey: %(sandboxkey)s resource_name: %(resource_name)s cputime: %(cputime)s """ % fields output_objects.append({'object_type': 'text', 'text': body}) else: body = """ <object type='application/x-java-applet' height='600' width='800'> <param name='codebase' value='%(codebase)s' /> <param name='code' value='%(oneclick_code)s' /> <param name='archive' value='%(oneclick_archive)s' /> <param name='server' value='%(server)s'> <param name='sandboxkey' value='%(sandboxkey)s'> <param name='resource_name' value='%(resource_name)s'> <param name='cputime' value='%(cputime)s'> OneClick applet failed to run (requires Java plug-in). </object> <p> Your computer will act as a %(site)s One-click resource as long as this browser window/tab remains open. </p> <h3>Java requirements and background</h3> Please note that if you get no applet picture above with status text, it is a likely indicator that you do not have the required Java plugin installed in your browser. You can download and install it from <a class='urllink iconspace' href='http://www.java.com/en/download/manual.jsp'> Sun Java Downloads</a>. The browser probably needs to be restarted after the installation before the plugin will be enabled.<br /> Other Java implementations may <i>appear</i> to work but not really deliver job results correctly, so if you want to be sure, please install the Sun Java plugin.<br /> Your browser provides the following Java information:<br /> <object type='application/x-java-applet' height='60' width='400'> <param name='codebase' value='%(codebase)s' /> <param name='code' value='%(info_code)s' /> Java plugin not installed or disabled. </object> """ % fields output_objects.append({'object_type': 'html_form', 'text': body}) 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, op_menu=client_id) defaults = signature()[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) queue = accepted['queue'][-1] action = accepted['action'][-1] iosessionid = accepted['iosessionid'][-1] msg = accepted['msg'][-1] msg_id = accepted['msg_id'][-1] # Web format for cert access and no header for SID access if client_id: output_objects.append({ 'object_type': 'header', 'text': 'Message queue %s' % action }) else: output_objects.append({'object_type': 'start'}) # Always return at least a basic file_output entry file_entry = { 'object_type': 'file_output', 'lines': [], 'wrap_binary': True, 'wrap_targets': ['lines'] } if not action in valid_actions: output_objects.append({'object_type': 'error_text', 'text' : 'Invalid action "%s" (supported: %s)' % \ (action, ', '.join(valid_actions))}) output_objects.append(file_entry) 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) # Find user home from session or certificate if iosessionid: client_home = os.path.realpath( os.path.join(configuration.webserver_home, iosessionid)) client_dir = os.path.basename(client_home) elif client_id: client_dir = client_id_dir(client_id) else: output_objects.append({ 'object_type': 'error_text', 'text': 'Either certificate or session ID is required' }) output_objects.append(file_entry) return (output_objects, returnvalues.CLIENT_ERROR) # Please note that base_dir must end in slash to avoid access to other # user dirs when own name is a prefix of another user name base_dir = os.path.abspath( os.path.join(configuration.user_home, client_dir)) + os.sep if not os.path.isdir(base_dir): output_objects.append({ 'object_type': 'error_text', 'text': 'No matching session or user home!' }) output_objects.append(file_entry) return (output_objects, returnvalues.CLIENT_ERROR) mqueue_base = os.path.join(base_dir, mqueue_prefix) + os.sep default_queue_dir = os.path.join(mqueue_base, default_mqueue) # Create mqueue base and default queue dir if missing if not os.path.exists(default_queue_dir): try: os.makedirs(default_queue_dir) except: pass # IMPORTANT: path must be expanded to abs for proper chrooting queue_path = os.path.abspath(os.path.join(mqueue_base, queue)) if not valid_user_path(configuration, queue_path, mqueue_base): output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid queue name: "%s"' % queue }) output_objects.append(file_entry) return (output_objects, returnvalues.CLIENT_ERROR) lock_path = os.path.join(mqueue_base, lock_name) lock_handle = open(lock_path, 'a') fcntl.flock(lock_handle.fileno(), fcntl.LOCK_EX) status = returnvalues.OK if action == "interactive": form_method = 'post' csrf_limit = get_csrf_limit(configuration) fill_helpers = { 'queue': queue, 'msg': msg, 'form_method': form_method, 'csrf_field': csrf_field, 'csrf_limit': csrf_limit, } target_op = 'mqueue' 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': 'text', 'text': ''' Fill in the fields below to control and access your personal message queues. Jobs can receive from and send to the message queues during execution, and use them as a means of job inter-communication. Expect message queue operations to take several seconds on the resources, however. That is, use it for tasks like orchestrating long running jobs, and not for low latency communication. ''' }) html = ''' <form name="mqueueform" method="%(form_method)s" action="%(target_op)s.py"> <table class="mqueue"> <tr><td class=centertext> </td></tr> <tr><td> Action:<br /> <input type="hidden" name="%(csrf_field)s" value="%(csrf_token)s" /> <input type=radio name=action value="create" onclick="javascript: document.mqueueform.queue.disabled=false; document.mqueueform.msg.disabled=true;" />create queue <input type=radio name=action checked value="send" onclick="javascript: document.mqueueform.queue.disabled=false; document.mqueueform.msg.disabled=false;" />send message to queue <input type=radio name=action value="receive" onclick="javascript: document.mqueueform.queue.disabled=false; document.mqueueform.msg.disabled=true;" />receive message from queue <input type=radio name=action value="remove" onclick="javascript: document.mqueueform.queue.disabled=false; document.mqueueform.msg.disabled=true;" />remove queue <input type=radio name=action value="listqueues" onclick="javascript: document.mqueueform.queue.disabled=true; document.mqueueform.msg.disabled=true;" />list queues <input type=radio name=action value="listmessages" onclick="javascript: document.mqueueform.queue.disabled=false; document.mqueueform.msg.disabled=true;" />list messages <input type=radio name=action value="show" onclick="javascript: document.mqueueform.queue.disabled=false; document.mqueueform.msg.disabled=true;" />show message </td></tr> <tr><td> Queue:<br /> <input class="fillwidth" type=text name=queue value="%(queue)s" /> </td></tr> <tr><td> <div id="msgfieldf"> <input class="fillwidth" type=text name=msg value="%(msg)s" /><br /> </div> </td></tr> <tr><td> <input type="submit" value="Apply" /> </td></tr> </table> </form> ''' % fill_helpers output_objects.append({'object_type': 'html_form', 'text': html}) output_objects.append({ 'object_type': 'text', 'text': ''' Further live job control is avalable through the live I/O interface. They provide a basic interface for centrally managing input and output files for active jobs. ''' }) output_objects.append({ 'object_type': 'link', 'destination': 'liveio.py', 'text': 'Live I/O interface' }) return (output_objects, returnvalues.OK) elif action == 'create': try: os.mkdir(queue_path) output_objects.append({ 'object_type': 'text', 'text': 'New "%s" queue created' % queue }) except Exception as err: output_objects.append({'object_type': 'error_text', 'text' : 'Could not create "%s" queue: "%s"' % \ (queue, err)}) status = returnvalues.CLIENT_ERROR elif action == 'remove': try: for entry in os.listdir(queue_path): os.remove(os.path.join(queue_path, entry)) os.rmdir(queue_path) output_objects.append({ 'object_type': 'text', 'text': 'Existing "%s" queue removed' % queue }) except Exception as err: output_objects.append({'object_type': 'error_text', 'text' : 'Could not remove "%s" queue: "%s"' % \ (queue, err)}) status = returnvalues.CLIENT_ERROR elif action == 'send': try: if not msg_id: msg_id = "%.0f" % time.time() msg_path = os.path.join(queue_path, msg_id) msg_fd = open(msg_path, 'w') msg_fd.write(msg) msg_fd.close() output_objects.append({ 'object_type': 'text', 'text': 'Message sent to "%s" queue' % queue }) except Exception as err: output_objects.append({'object_type': 'error_text', 'text' : 'Could not send to "%s" queue: "%s"' % \ (queue, err)}) status = returnvalues.CLIENT_ERROR elif action == 'receive': try: if not msg_id: messages = os.listdir(queue_path) messages.sort() if messages: msg_id = messages[0] if msg_id: message_path = os.path.join(queue_path, msg_id) message_fd = open(message_path, 'r') message = message_fd.readlines() message_fd.close() os.remove(message_path) file_entry['path'] = os.path.basename(message_path) else: message = [mqueue_empty] # Update file_output entry for raw data with output_format=file file_entry['lines'] = message except Exception as err: output_objects.append({ 'object_type': 'error_text', 'text': 'Could not receive from "%s" queue: "%s"' % (queue, err) }) status = returnvalues.CLIENT_ERROR elif action == 'show': try: if not msg_id: messages = os.listdir(queue_path) messages.sort() if messages: msg_id = messages[0] if msg_id: message_path = os.path.join(queue_path, msg_id) message_fd = open(message_path, 'r') message = message_fd.readlines() message_fd.close() file_entry['path'] = os.path.basename(message_path) else: message = [mqueue_empty] # Update file_output entry for raw data with output_format=file file_entry['lines'] = message except Exception as err: output_objects.append({ 'object_type': 'error_text', 'text': 'Could not show %s from "%s" queue: "%s"' % (msg_id, queue, err) }) status = returnvalues.CLIENT_ERROR elif action == 'listmessages': try: messages = os.listdir(queue_path) messages.sort() output_objects.append({'object_type': 'list', 'list': messages}) except Exception as err: output_objects.append({'object_type': 'error_text', 'text' : 'Could not list "%s" queue: "%s"' % \ (queue, err)}) status = returnvalues.CLIENT_ERROR elif action == 'listqueues': try: queues = [i for i in os.listdir(mqueue_base) if \ os.path.isdir(os.path.join(mqueue_base, i))] queues.sort() output_objects.append({'object_type': 'list', 'list': queues}) except Exception as err: output_objects.append({ 'object_type': 'error_text', 'text': 'Could not list queues: "%s"' % err }) status = returnvalues.CLIENT_ERROR else: output_objects.append({ 'object_type': 'error_text', 'text': 'Unexpected mqueue action: "%s"' % action }) status = returnvalues.SYSTEM_ERROR lock_handle.close() output_objects.append(file_entry) output_objects.append({ 'object_type': 'link', 'destination': 'mqueue.py?queue=%s;msg=%s' % (queue, msg), 'text': 'Back to message queue interaction' }) 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, op_menu=False) 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) oid_url = accepted['url'][-1] openid_status = { 'object_type': 'openid_status', 'server': None, 'status': None, 'error': "" } # IMPORTANT: we only allow ping of configured openid servers to avoid abuse # otherwise the urlopen could be tricked to use e.g. file:/etc/passwd or # be used to attack remote sites if oid_url in configuration.user_openid_providers: # TODO: build url from conf ping_url = oid_url.replace("/id/", "/ping") openid_status['server'] = ping_url logger.debug("%s openid server on %s" % (op_name, ping_url)) try: # Never use proxies ping_status = urllib.urlopen(ping_url, proxies={}) http_status = ping_status.getcode() data = ping_status.read() ping_status.close() if http_status == 200: # TODO: better parsing if "<h1>True</h1>" in data: openid_status['status'] = "online" else: openid_status['status'] = "down" openid_status['error'] = data else: openid_status['status'] = "down" openid_status['error'] = "server returned error code %s" % \ http_status except Exception as exc: openid_status['status'] = "down" openid_status['error'] = "unexpected server response (%s)" % exc if openid_status['status'] == "online": logger.info("%s on %s succeeded" % (op_name, oid_url)) else: logger.error("%s against %s returned error: " % (op_name, oid_url) + " %(error)s (%(status)s)" % openid_status) elif oid_url in configuration.user_openidconnect_providers: # TODO: build url from conf # ping_url = oid_url.replace( # "/oauth/nam/.well-known/openid-configuration", "") ping_url = oid_url openid_status['server'] = ping_url logger.debug("%s openid connect server on %s" % (op_name, ping_url)) try: # Never use proxies ping_status = urllib.urlopen(ping_url, proxies={}) http_status = ping_status.getcode() data = ping_status.read() ping_status.close() if http_status == 200: # TODO: better parsing if "authorization_endpoint" in data: openid_status['status'] = "online" else: openid_status['status'] = "down" openid_status['error'] = data else: openid_status['status'] = "down" openid_status['error'] = "server returned error code %s" % \ http_status except Exception as exc: openid_status['status'] = "down" openid_status['error'] = "unexpected server response (%s)" % exc if openid_status['status'] == "online": logger.info("%s on %s succeeded" % (op_name, oid_url)) else: logger.error("%s against %s returned error: " % (op_name, oid_url) + " %(error)s (%(status)s)" % openid_status) else: logger.error("%s against %s is not a valid openid provider" % (op_name, oid_url)) openid_status['server'] = "no such server configured" openid_status['status'] = "unavailable" openid_status['error'] = "OpenID login from %s not enabled" % oid_url output_objects.append(openid_status) 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, op_title=False, op_menu=client_id) defaults = signature()[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) remote_ip = str(os.getenv('REMOTE_ADDR')) res_type = accepted['type'][-1] unique_resource_name = accepted['unique_resource_name'][-1] exe_name = accepted['exe_name'][-1] status = returnvalues.OK if not configuration.site_enable_resources: output_objects.append({ 'object_type': 'error_text', 'text': "Resources are not enabled on this site" }) return (output_objects, returnvalues.CLIENT_ERROR) # Web format for cert access and no header for SID access if client_id: output_objects.append({ 'object_type': 'title', 'text': 'Load resource script PGID' }) output_objects.append({ 'object_type': 'header', 'text': 'Load resource script PGID' }) else: output_objects.append({'object_type': 'start'}) # Please note that base_dir must end in slash to avoid access to other # resource dirs when own name is a prefix of another resource name base_dir = os.path.abspath( os.path.join(configuration.resource_home, unique_resource_name)) + os.sep 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 '%s' to get the PGID!" % 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 on that variable. # exe_name is not automatically checked however - do it manually if not valid_dir_input(base_dir, 'EXE_' + exe_name + '.PGID'): # out of bounds - rogue resource!?!? output_objects.append({ 'object_type': 'error_text', 'text': 'invalid exe_name! %s' % exe_name }) logger.error('''getrespgid called with illegal parameter(s) in what appears to be an illegal directory traversal attempt!: unique_resource_name %s, exe %s, client_id %s''' % (unique_resource_name, exe_name, client_id)) return (output_objects, returnvalues.CLIENT_ERROR) # Check that resource address matches request source to make DoS harder try: check_source_ip(remote_ip, unique_resource_name) except ValueError as vae: output_objects.append({ 'object_type': 'error_text', 'text': 'invalid request: %s' % vae }) logger.error("Invalid put pgid: %s" % vae) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: add full session ID check here if 'FE' == res_type: pgid_path = os.path.join(base_dir, 'FE.PGID') elif 'EXE' == res_type: pgid_path = os.path.join(base_dir + 'EXE_%s.PGID' % exe_name) else: output_objects.append({ 'object_type': 'error_text', 'text': "Unknown type: '%s'" % res_type }) return (output_objects, returnvalues.CLIENT_ERROR) try: pgid_file = open(pgid_path, 'r+') fcntl.flock(pgid_file, fcntl.LOCK_EX) pgid_file.seek(0, 0) pgid = pgid_file.readline().strip() fcntl.flock(pgid_file, fcntl.LOCK_UN) pgid_file.close() msg = "%s\n'%s' PGID succesfully retrieved." % (pgid, res_type) except Exception as err: if 'FE' == res_type: msg = "Resource frontend: '%s' is stopped." % unique_resource_name elif 'EXE' == res_type: msg = ("Error reading PGID for resource: '%s' EXE: '%s'\n" 'Either resource has never been started or a server ' 'error occured.') % (unique_resource_name, exe_name) status = returnvalues.CLIENT_ERROR # Status code line followed by raw output if not client_id: output_objects.append({'object_type': 'script_status', 'text': ''}) output_objects.append({ 'object_type': 'binary', 'data': '%s' % status[0] }) output_objects.append({'object_type': 'binary', 'data': msg}) 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) 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, 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) # TODO: this handler is incomplete and NOT yet hooked up with Xgi-bin return (output_objects, returnvalues.SYSTEM_ERROR) 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) filename = accepted['filename'][-1] patterns = accepted['path'] iosessionid = accepted['iosessionid'][-1] file_startpos = accepted['file_startpos'][-1] file_endpos = accepted['file_endpos'][-1] if file_startpos: file_startpos = int(file_startpos) else: file_startpos = -1 if file_endpos: file_endpos = int(file_endpos) else: file_endpos = -1 # Legacy naming if filename: patterns = [filename] valid_methods = ['GET', 'PUT', 'DELETE'] action = os.getenv('REQUEST_METHOD') if not action in valid_methods: output_objects.append({ 'object_type': 'error_text', 'text': '''Only accepting %s requests''' % ', '.join(valid_methods) }) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: introduce CSRF protection here when clients support it # if not safe_handler(configuration, action, 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 job IO session ID if client_id: user_id = client_id target_dir = client_id_dir(client_id) base_dir = configuration.user_home page_title = 'User range file access' widgets = True userstyle = True elif iosessionid: user_id = iosessionid target_dir = iosessionid base_dir = configuration.webserver_home page_title = 'Create Shared Directory' widgets = False userstyle = 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/iosession id!' }) return (output_objects, returnvalues.CLIENT_ERROR) 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 + 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 access file '%s': Permission denied" % (op_name, pattern) }) status = returnvalues.CLIENT_ERROR for abs_path in match: relative_path = abs_path.replace(base_dir, '') if action == 'GET': if not do_get(configuration, output_objects, abs_path, file_startpos, file_endpos): output_objects.append({ 'object_type': 'error_text', 'text': '''Could not gett %r''' % pattern }) status = returnvalues.SYSTEM_ERROR elif action == 'PUT': if not do_put(configuration, output_objects, abs_path, file_startpos, file_endpos): output_objects.append({ 'object_type': 'error_text', 'text': '''Could not put %r''' % pattern }) status = returnvalues.SYSTEM_ERROR elif action == 'DELETE': if not do_delete(configuration, output_objects, abs_path): output_objects.append({ 'object_type': 'error_text', 'text': '''Could not delete %r''' % pattern }) status = returnvalues.SYSTEM_ERROR else: output_objects.append({ 'object_type': 'error_text', 'text': 'Unsupported action: %r' % action }) return (output_objects, returnvalues.CLIENT_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, op_title=False, op_menu=client_id) defaults = signature()[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) remote_ip = str(os.getenv('REMOTE_ADDR')) res_type = accepted['type'][-1] unique_resource_name = accepted['unique_resource_name'][-1] exe_name = accepted['exe_name'][-1] pgid = accepted['pgid'][-1] status = returnvalues.OK if not configuration.site_enable_resources: output_objects.append({ 'object_type': 'error_text', 'text': "Resources are not enabled on this site" }) return (output_objects, returnvalues.CLIENT_ERROR) # Web format for cert access and no header for SID access if client_id: output_objects.append({ 'object_type': 'title', 'text': 'Load resource script PGID' }) output_objects.append({ 'object_type': 'header', 'text': 'Load resource script PGID' }) else: output_objects.append({'object_type': 'start'}) # Please note that base_dir must end in slash to avoid access to other # resource dirs when own name is a prefix of another resource name base_dir = os.path.abspath( os.path.join(configuration.resource_home, unique_resource_name)) + os.sep # We do not have a trusted base dir here since there's no certificate data. # Manually check input variables if not valid_dir_input(configuration.resource_home, unique_resource_name): # out of bounds - rogue resource!?!? msg = 'invalid unique_resource_name! %s' % unique_resource_name logger.error( 'putrespgid FE called with illegal parameter(s) in what appears to be an illegal directory traversal attempt!: unique_resource_name %s, exe %s, client_id %s' % (unique_resource_name, exe_name, client_id)) return (output_objects, returnvalues.CLIENT_ERROR) if not valid_dir_input(base_dir, 'EXE_%s.PGID' % exe_name): # out of bounds - rogue resource!?!? msg = 'invalid unique_resource_name / exe_name! %s / %s' \ % (unique_resource_name, exe_name) logger.error( 'putrespgid EXE called with illegal parameter(s) in what appears to be an illegal directory traversal attempt!: unique_resource_name %s, exe %s, client_id %s' % (unique_resource_name, exe_name, client_id)) return (output_objects, returnvalues.CLIENT_ERROR) (load_status, resource_conf) = \ get_resource_configuration(configuration.resource_home, unique_resource_name, logger) if not load_status: logger.error("Invalid putrespgid - no resouce_conf for: %s : %s" % (unique_resource_name, resource_conf)) output_objects.append({ 'object_type': 'error_text', 'text': 'invalid request: no such resource!' }) return (output_objects, returnvalues.CLIENT_ERROR) # Check that resource address matches request source to make DoS harder proxy_fqdn = resource_conf.get('FRONTENDPROXY', None) try: check_source_ip(remote_ip, unique_resource_name, proxy_fqdn) except ValueError as vae: logger.error("Invalid put pgid: %s" % vae) output_objects.append({ 'object_type': 'error_text', 'text': 'invalid request: %s' % vae }) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: add full session ID check here if 'FE' == res_type: (result, msg) = put_fe_pgid(configuration.resource_home, unique_resource_name, pgid, logger, True) if result: msg += '(%s) (%s)' % (remote_ip, res_type) elif 'EXE' == res_type: (result, msg) = put_exe_pgid(configuration.resource_home, unique_resource_name, exe_name, pgid, logger, True) if result: msg += '(%s) (%s)' % (remote_ip, res_type) else: msg = "Unknown type: '%s'" % res_type status = returnvalues.CLIENT_ERROR # Status code line followed by raw output if not client_id: output_objects.append({'object_type': 'script_status', 'text': ''}) output_objects.append({ 'object_type': 'binary', 'data': '%s' % status[0] }) output_objects.append({'object_type': 'binary', 'data': msg}) 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] client_dir = client_id_dir(client_id) logger.debug('in peersaction: %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) 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'] = 'Save Peers' output_objects.append({'object_type': 'header', 'text': 'Save Peers'}) admin_email = configuration.admin_email smtp_server = configuration.smtp_server user_pending = os.path.abspath(configuration.user_pending) user_map = get_full_user_map(configuration) user_dict = user_map.get(client_id, None) # Optional site-wide limitation of peers permission if not user_dict or \ not peers_permit_allowed(configuration, user_dict): logger.warning("user %s is not allowed to permit peers!" % client_id) output_objects.append({ 'object_type': 'error_text', 'text': 'Only privileged users can permit external peers!' }) return (output_objects, returnvalues.CLIENT_ERROR) action = accepted['action'][-1].strip() label = accepted['peers_label'][-1].strip() kind = accepted['peers_kind'][-1].strip() raw_expire = accepted['peers_expire'][-1].strip() peers_content = accepted['peers_content'] peers_format = accepted['peers_format'][-1].strip() peers_invite = accepted['peers_invite'][-1].strip() do_invite = (peers_invite.lower() in ['on', 'true', 'yes', '1']) try: expire = datetime.datetime.strptime(raw_expire, '%Y-%m-%d') if datetime.datetime.now() > expire: raise ValueError("specified expire value is in the past!") except Exception as exc: logger.error("expire %r could not be parsed into a (future) date" % raw_expire) output_objects.append({ 'object_type': 'text', 'text': 'No valid expire provided - using default: %d days' % default_expire_days }) expire = datetime.datetime.now() expire += datetime.timedelta(days=default_expire_days) expire = expire.date().isoformat() if not action in peer_actions: output_objects.append({ 'object_type': 'error_text', 'text': 'Unsupported peer action %r - only %s are allowed' % (action, ', '.join(peer_actions)) }) return (output_objects, returnvalues.CLIENT_ERROR) if not kind in peer_kinds: output_objects.append({ 'object_type': 'error_text', 'text': 'Unsupported peer kind %r - only %s are allowed' % (kind, ', '.join(peer_kinds)) }) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: implement and enable more formats? if peers_format not in ("csvform", 'userid'): output_objects.append({ 'object_type': 'error_text', 'text': 'Only Import Peers is implemented so far!' }) return (output_objects, returnvalues.CLIENT_ERROR) peers_path = os.path.join(configuration.user_settings, client_dir, peers_filename) try: all_peers = load(peers_path) except Exception as exc: logger.warning("could not load peers from: %s" % exc) all_peers = {} # Extract peer(s) from request (peers, err) = parse_peers(configuration, peers_content, peers_format) if not err and not peers: err = ["No valid peers provided"] if err: output_objects.append({ 'object_type': 'error_text', 'text': 'Parsing failed: %s' % '.\n '.join(err) }) output_objects.append({ 'object_type': 'link', 'destination': 'peers.py', 'text': 'Back to peers' }) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: general cases of operation here: # * import multiple peers in one go (add new, update existing) # * add one or more new peers # * update one or more existing peers # * remove one or more existing peers # * accept one or more pending requests # * reject one or more pending requests # The kind and expire values are generally applied for all included peers. # NOTE: we check all peers before any action for user in peers: fill_distinguished_name(user) peer_id = user['distinguished_name'] cur_peer = all_peers.get(peer_id, {}) if 'add' == action and cur_peer: output_objects.append({ 'object_type': 'error_text', 'text': 'Peer %r already exists!' % peer_id }) return (output_objects, returnvalues.CLIENT_ERROR) elif 'update' == action and not cur_peer: output_objects.append({ 'object_type': 'error_text', 'text': 'Peer %r does not exists!' % peer_id }) return (output_objects, returnvalues.CLIENT_ERROR) elif 'remove' == action and not cur_peer: output_objects.append({ 'object_type': 'error_text', 'text': 'Peer %r does not exists!' % peer_id }) return (output_objects, returnvalues.CLIENT_ERROR) elif 'accept' == action and cur_peer: output_objects.append({ 'object_type': 'error_text', 'text': 'Peer %r already accepted!' % peer_id }) return (output_objects, returnvalues.CLIENT_ERROR) elif 'reject' == action and cur_peer: output_objects.append({ 'object_type': 'error_text', 'text': 'Peer %r already accepted!' % peer_id }) return (output_objects, returnvalues.CLIENT_ERROR) elif 'import' == action and cur_peer: # Only warn on import with existing match output_objects.append({ 'object_type': 'text', 'text': 'Updating existing peer %r' % peer_id }) # Now apply changes for user in peers: peer_id = user['distinguished_name'] cur_peer = all_peers.get(peer_id, {}) user.update({'label': label, 'kind': kind, 'expire': expire}) if 'add' == action: all_peers[peer_id] = user elif 'update' == action: all_peers[peer_id] = user elif 'remove' == action: del all_peers[peer_id] elif 'accept' == action: all_peers[peer_id] = user elif 'reject' == action: pass elif 'import' == action: all_peers[peer_id] = user logger.info("%s peer %s" % (action, peer_id)) try: dump(all_peers, peers_path) logger.debug('%s %s peers %s in %s' % (client_id, action, all_peers, peers_path)) output_objects.append({ 'object_type': 'text', 'text': "Completed %s peers" % action }) for user in peers: output_objects.append({ 'object_type': 'text', 'text': "%(distinguished_name)s" % user }) if action in ['import', 'add', 'update']: client_name = extract_field(client_id, 'full_name') client_email = extract_field(client_id, 'email') if do_invite: succeeded, failed = [], [] email_header = '%s Invitation' % configuration.short_title email_msg_template = """Hi %%s, This is an automatic email sent on behalf of %s who vouched for you to get a user account on %s. You can accept the invitation by going to %%s entering a password of your choice and submitting the form. If you do not want a user account you can safely ignore this email. We would be grateful if you report any abuse of the invitation system to the site administrators (%s). """ % (client_name, configuration.short_title, admin_email) for peer_user in peers: peer_name = peer_user['full_name'] peer_email = peer_user['email'] peer_url = os.path.join( configuration.migserver_https_sid_url, 'cgi-sid', 'reqoid.py') peer_req = {} for field in peers_fields: peer_req[field] = peer_user.get(field, '') peer_req['comment'] = 'Invited by %s (%s) for %s purposes' \ % (client_name, client_email, kind) # Mark ID fields as readonly in the form to limit errors peer_req['ro_fields'] = keyword_auto peer_url += '?%s' % urllib.urlencode(peer_req) email_msg = email_msg_template % (peer_name, peer_url) logger.info( 'Sending invitation: to: %s, header: %s, msg: %s, smtp_server: %s' % (peer_email, email_header, email_msg, smtp_server)) if send_email(peer_email, email_header, email_msg, logger, configuration): succeeded.append(peer_email) else: failed.append(peer_email) if failed: output_objects.append({ 'object_type': 'error_text', 'text': """An error occured trying to email the peer invitation to %s . Please inform the site admins (%s) if the problem persists. """ % (', '.join(failed), admin_email) }) if succeeded: output_objects.append({ 'object_type': 'text', 'text': """Sent invitation to %s with a link to a mostly pre-filled %s account request form with the exact ID fields you provided here.""" % (', '.join(succeeded), configuration.short_title) }) else: output_objects.append({ 'object_type': 'text', 'text': """Please tell your peers to request an account at %s with the exact ID fields you provided here and importantly mentioning the purpose and your email (%s) in the sign up Comment field. Alternatively you can use the invite button to send out an email with a link to a mostly prefilled request form.""" % (configuration.short_title, client_email) }) except Exception as exc: logger.error('Failed to save %s peers to %s: %s' % (client_id, peers_path, exc)) output_objects.append({ 'object_type': 'error_text', 'text': ''' Could not %s peers %r. Please contact the site admins on %s if this error persists. ''' % (action, label, admin_email) }) return (output_objects, returnvalues.SYSTEM_ERROR) if action in ["accept", "reject"]: changed = [(i['distinguished_name'], i) for i in peers] if not manage_pending_peers(configuration, client_id, "remove", changed): logger.warning('could not update pending peers for %s after %s' % (client_id, action)) logger.info('%s completed for %s peers for %s in %s' % (action, label, client_id, peers_path)) user_lines = [] pretty_peers = {'label': label, 'kind': kind, 'expire': expire} for user in peers: user_lines.append(user['distinguished_name']) pretty_peers['user_lines'] = '\n'.join(user_lines) email_header = '%s Peers %s' % (configuration.short_title, action) email_msg = """Received %s peers from %s """ % (action, client_id) email_msg += """ Kind: %(kind)s , Expire: %(expire)s, Label: %(label)s , Peers: %(user_lines)s """ % pretty_peers 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 about your %s peers to the site administrators. Please manually inform them (%s) if the problem persists. ''' % (action, admin_email) }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': ''' Informed the site admins about your %s peers action to let them accept peer account requests you already validated.''' % action }) output_objects.append({ 'object_type': 'link', 'destination': 'peers.py', 'text': 'Back to peers' }) 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, 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, op_menu=client_id) defaults = signature()[1] (validate_status, accepted) = validate_input( user_arguments_dict, defaults, output_objects, allow_rejects=False, # NOTE: path can use wildcards, 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'] current_dir = accepted['current_dir'][-1].lstrip('/') share_id = accepted['share_id'][-1] show_dest = accepted['with_dest'][0].lower() == 'true' status = returnvalues.OK # NOTE: in contrast to 'ls' we never include write operations here read_mode, write_mode = True, False visibility_mods = ''' .%(main_class)s .enable_write { display: none; } .%(main_class)s .disable_read { display: none; } .%(main_class)s .if_full { display: none; } ''' # Either authenticated user client_id set or sharelink ID if client_id: user_id = client_id target_dir = client_id_dir(client_id) base_dir = configuration.user_home redirect_name = configuration.site_user_redirect redirect_path = redirect_name id_args = '' root_link_name = 'USER HOME' main_class = "user_expand" page_title = 'User Files - Path Expansion' userstyle = True widgets = True elif share_id: try: (share_mode, _) = extract_mode_id(configuration, share_id) except ValueError 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) # then include shared by %(owner)s on page header user_id = 'anonymous user through share ID %s' % share_id target_dir = os.path.join(share_mode, share_id) base_dir = configuration.sharelink_home redirect_name = 'share_redirect' redirect_path = os.path.join(redirect_name, share_id) id_args = 'share_id=%s;' % share_id root_link_name = '%s' % share_id main_class = "sharelink_expand" page_title = 'Shared Files - Path Expansion' userstyle = False widgets = False 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) visibility_toggle = ''' <style> %s </style> ''' % (visibility_mods % {'main_class': main_class}) # 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 if not os.path.isdir(base_dir): logger.error('%s called on missing base_dir: %s' % (op_name, base_dir)) output_objects.append({'object_type': 'error_text', 'text': 'No such %s!' % page_title.lower() }) return (output_objects, returnvalues.CLIENT_ERROR) title_entry = find_entry(output_objects, 'title') title_entry['text'] = page_title title_entry['skipwidgets'] = not widgets title_entry['skipuserstyle'] = not userstyle fill_helpers = {'dest_dir': current_dir + os.sep, 'share_id': share_id, 'flags': flags, 'tmp_flags': flags, 'long_set': long_list(flags), 'recursive_set': recursive(flags), 'all_set': all(flags)} add_import, add_init, add_ready = '', '', '' title_entry['style']['advanced'] += ''' %s ''' % visibility_toggle title_entry['script']['advanced'] += add_import title_entry['script']['init'] += add_init title_entry['script']['ready'] += add_ready title_entry['script']['body'] = ' class="%s"' % main_class output_objects.append({'object_type': 'header', 'text': page_title}) # Shared URL helpers ls_url_template = 'ls.py?%scurrent_dir=%%(rel_dir_enc)s;flags=%s' % \ (id_args, flags) redirect_url_template = '/%s/%%(rel_path_enc)s' % redirect_path location_pre_html = """ <div class='files'> <table class='files'> <tr class=title><td class=centertext> Working directory: </td></tr> <tr><td class='centertext'> """ output_objects.append( {'object_type': 'html_form', 'text': location_pre_html}) # Use current_dir nav location links for pattern in pattern_list[:1]: links = [] links.append({'object_type': 'link', 'text': root_link_name, 'destination': ls_url_template % {'rel_dir_enc': '.'}}) prefix = '' parts = os.path.normpath(current_dir).split(os.sep) for i in parts: if i == ".": continue prefix = os.path.join(prefix, i) links.append({'object_type': 'link', 'text': i, 'destination': ls_url_template % {'rel_dir_enc': quote(prefix)}}) output_objects.append( {'object_type': 'multilinkline', 'links': links, 'sep': ' %s ' % os.sep}) location_post_html = """ </td></tr> </table> </div> <br /> """ output_objects.append( {'object_type': 'html_form', 'text': location_post_html}) dir_listings = [] output_objects.append({ 'object_type': 'dir_listings', 'dir_listings': dir_listings, 'flags': flags, 'redirect_name': redirect_name, 'redirect_path': redirect_path, 'share_id': share_id, 'ls_url_template': ls_url_template, 'rm_url_template': '', 'rmdir_url_template': '', 'editor_url_template': '', 'redirect_url_template': redirect_url_template, 'show_dest': show_dest, }) first_match = None 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 current_path = os.path.normpath(os.path.join(base_dir, current_dir)) unfiltered_match = glob.glob(current_path + 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): logger.warning('%s tried to %s restricted path %s ! (%s)' % (user_id, op_name, abs_path, pattern)) continue match.append(abs_path) if not first_match: first_match = 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: if abs_path + os.sep == base_dir: relative_path = '.' else: relative_path = abs_path.replace(base_dir, '') entries = [] dir_listing = { 'object_type': 'dir_listing', 'relative_path': relative_path, 'entries': entries, 'flags': flags, } dest = '' if show_dest: if os.path.isfile(abs_path): dest = os.path.basename(abs_path) elif recursive(flags): # references to '.' or similar are stripped by abspath if abs_path + os.sep == base_dir: dest = '' else: # dest = os.path.dirname(abs_path).replace(base_dir, "") dest = os.path.basename(abs_path) + os.sep handle_expand(configuration, output_objects, entries, base_dir, abs_path, flags, dest, 0, show_dest) dir_listings.append(dir_listing) output_objects.append({'object_type': 'html_form', 'text': """ <div class='files disable_read'> <form method='get' action='ls.py'> <table class='files'> <tr class=title><td class=centertext> Filter paths (wildcards like * and ? are allowed) <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> <input type='text' name='path' value='' /> <input type='submit' value='Filter' /> </td></tr> </table> </form> </div> """ % fill_helpers}) # Short/long format buttons fill_helpers['tmp_flags'] = flags + 'l' htmlform = """ <table class='files if_full'> <tr class=title><td class=centertext colspan=4> File view options </td></tr> <tr><td colspan=4><br /></td></tr> <tr class=title><td>Parameter</td><td>Setting</td><td>Enable</td><td>Disable</td></tr> <tr><td>Long format</td><td> %(long_set)s</td><td> <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />" % entry fill_helpers['tmp_flags'] = flags.replace('l', '') htmlform += """ <input type='submit' value='On' /><br /> </form> </td><td> <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />" % entry htmlform += """ <input type='submit' value='Off' /><br /> </form> </td></tr> """ # Recursive output fill_helpers['tmp_flags'] = flags + 'r' htmlform += """ <!-- Non-/recursive list buttons --> <tr><td>Recursion</td><td> %(recursive_set)s</td><td>""" % fill_helpers htmlform += """ <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />" % entry fill_helpers['tmp_flags'] = flags.replace('r', '') htmlform += """ <input type='submit' value='On' /><br /> </form> </td><td> <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />"\ % entry htmlform += """ <input type='submit' value='Off' /><br /> </form> </td></tr> """ htmlform += """ <!-- Show dot files buttons --> <tr><td>Show hidden files</td><td> %(all_set)s</td><td>""" % fill_helpers fill_helpers['tmp_flags'] = flags + 'a' htmlform += """ <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />" % entry fill_helpers['tmp_flags'] = flags.replace('a', '') htmlform += """ <input type='submit' value='On' /><br /> </form> </td><td> <form method='get' action='ls.py'> <input type='hidden' name='output_format' value='html' /> <input type='hidden' name='flags' value='%(tmp_flags)s' /> <input type='hidden' name='share_id' value='%(share_id)s' /> <input name='current_dir' type='hidden' value='%(dest_dir)s' /> """ % fill_helpers for entry in pattern_list: htmlform += "<input type='hidden' name='path' value='%s' />" % entry htmlform += """ <input type='submit' value='Off' /><br /> </form> </td></tr> </table> """ # show flag buttons after contents to limit clutter output_objects.append({'object_type': 'html_form', 'text': htmlform}) return (output_objects, status)