def invite_share_link_helper(configuration, client_id, share_dict, output_format, form_append=''): """Build share link invitation helper dict to fill strings""" fill_helpers = { 'vgrid_label': configuration.site_vgrid_label, 'short_title': configuration.short_title, 'output_format': output_format, # Legacy: make sure single_file is always set 'single_file': False } fill_helpers.update(share_dict) if fill_helpers['single_file']: fill_helpers['share_url'] = "%s/share_redirect/%s" \ % (configuration.migserver_https_sid_url, fill_helpers['share_id']) else: fill_helpers['share_url'] = "%s/sharelink/%s" \ % (configuration.migserver_https_sid_url, fill_helpers['share_id']) fill_helpers['name'] = extract_field(client_id, 'full_name') fill_helpers['email'] = extract_field(client_id, 'email') fill_helpers['form_append'] = form_append fill_helpers['auto_msg'] = '''Hi, %(name)s (%(email)s) has shared %(short_title)s data with you on: %(share_url)s --- Optional invitation message follows below ---''' % fill_helpers return fill_helpers
def send_system_notification(user_id, category, message, configuration): """Send system notification to *user_id* through grid_notify""" logger = configuration.logger if not configuration.site_enable_notify: logger.warning("System notify helper is disabled in configuration!") return False if not user_id: logger.error("Invalid user_id: %s" % user_id) return False client_id = expand_openid_alias(user_id, configuration) if not client_id or not extract_field(client_id, 'email'): logger.error("send_system_notification: Invalid user_id: %s" % user_id) return False if not isinstance(category, list): logger.error("send_system_notification: category must be a list") return False notification = { 'category': category, 'user_id': user_id, 'message': message, 'timestamp': time.time(), } pickled_notification = pickle.dumps(notification) return send_message_to_grid_notify(pickled_notification, configuration.logger, configuration)
def recv_notification(configuration, path): """Read notification event from file""" logger = configuration.logger # logger.debug("read_notification: %s" % file) status = True new_notification = unpickle(path, logger) if not new_notification: logger.error("Failed to unpickle: %s" % path) return False user_id = new_notification.get('user_id', '') # logger.debug("Received user_id: '%s'" % user_id) if not user_id: status = False logger.error("Missing user_id in notification: %s" % path) else: client_id = expand_openid_alias(user_id, configuration) # logger.debug("resolved client_id: '%s'" % client_id) if not client_id or not extract_field(client_id, 'email'): status = False logger.error("Failed to resolve client_id from user_id: '%s'" % user_id) if status: category = new_notification.get('category', []) # logger.debug("Received category: %s" % category) if not isinstance(category, list): status = False logger.error("Received category: %s must be a list" % category) if status: logger.info("Received event: %s, from: '%s'" % (category, client_id)) new_timestamp = new_notification.get('timestamp') message = new_notification.get('message', '') # logger.debug("Received message: %s" % message) client_dict = received_notifications.get(client_id, {}) if not client_dict: received_notifications[client_id] = client_dict files_list = client_dict.get('files', []) if not files_list: client_dict['files'] = files_list if path in files_list: logger.warning("Skipping prevoursly received notification: '%s'" % path) else: files_list.append(path) client_dict['timestamp'] = min( client_dict.get('timestamp', sys.maxint), new_timestamp) messages_dict = client_dict.get('messages', {}) if not messages_dict: client_dict['messages'] = messages_dict header = " ".join(category) if not header: header = '* UNKNOWN *' body_dict = messages_dict.get(header, {}) if not body_dict: messages_dict[header] = body_dict message_count = body_dict.get(message, 0) body_dict[message] = message_count + 1 return status
def send_notifications(configuration): """Generate message and send notification to users""" logger = configuration.logger # logger.debug("send_notifications") result = [] for (client_id, client_dict) in received_notifications.iteritems(): timestamp = client_dict.get('timestamp', 0) timestr = ( datetime.fromtimestamp(timestamp)).strftime('%d/%m/%Y %H:%M:%S') client_name = extract_field(client_id, 'full_name') client_email = extract_field(client_id, 'email') recipient = "%s <%s>" % (client_name, client_email) total_events = 0 notify_message = "" messages_dict = client_dict.get('messages', {}) for (header, value) in messages_dict.iteritems(): if notify_message: notify_message += "\n\n" notify_message += "= %s =\n" % header for (message, events) in value.iteritems(): notify_message += "#%s : %s\n" % (events, message) total_events += events subject = "System notification: %s new events" % total_events notify_message = "Found %s new events since: %s\n\n" \ % (total_events, timestr) \ + notify_message status = send_email(recipient, subject, notify_message, logger, configuration) if status: logger.info("Send email with %s events to: %s" % (total_events, recipient)) result.append(client_id) else: logger.error("Failed to send email to: '%s', '%s'" % (recipient, client_id)) return result
def get_twofactor_secrets(configuration, client_id): """Load twofactor base32 key and OTP uri for QR code. Generates secret for user if not already done. Actual twofactor login requirement is not enabled here, however. """ _logger = configuration.logger if pyotp is None: raise Exception("The pyotp module is missing and required for 2FA") if configuration.site_enable_gdp: client_id = get_base_client_id(configuration, client_id, expand_oid_alias=False) # NOTE: 2FA secret key is a standalone file in user settings dir # Try to load existing and generate new one if not there. # We need the base32-encoded form as returned here. b32_key = load_twofactor_key(client_id, configuration) if not b32_key: b32_key = reset_twofactor_key(client_id, configuration) totp = get_totp(client_id, b32_key, configuration) # URI-format for otp auth is # otpauth://<otptype>/(<issuer>:)<accountnospaces>? # secret=<secret>(&issuer=<issuer>)(&image=<imageuri>) # which we pull out of pyotp directly. # We could display with Google Charts helper like this example # https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr& # chl=otpauth://totp/Example:[email protected]? # secret=JBSWY3DPEHPK3PXP&issuer=Example # but we prefer to use the QRious JS library to keep it local. if configuration.user_openid_alias: username = extract_field(client_id, configuration.user_openid_alias) else: username = client_id otp_uri = totp.provisioning_uri(username, issuer_name=configuration.short_title) # IMPORTANT: pyotp unicode breaks wsgi when inserted - force utf8! otp_uri = force_utf8(otp_uri) # Google img examle # img_url = 'https://www.google.com/chart?' # img_url += urllib.urlencode([('cht', 'qr'), ('chld', 'M|0'), # ('chs', '200x200'), ('chl', otp_uri)]) # otp_img = '<img src="%s" />' % img_url return (b32_key, totp.interval, otp_uri)
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False) client_dir = client_id_dir(client_id) defaults = signature()[1] (validate_status, accepted) = validate_input_and_cert( user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) logger.debug("User: %s executing %s" % (client_id, op_name)) if not configuration.site_enable_jupyter: output_objects.append({ 'object_type': 'error_text', 'text': 'The Jupyter service is not enabled on the system' }) return (output_objects, returnvalues.SYSTEM_ERROR) if not configuration.site_enable_sftp_subsys and not \ configuration.site_enable_sftp: output_objects.append({ 'object_type': 'error_text', 'text': 'The required sftp service is not enabled on the system' }) return (output_objects, returnvalues.SYSTEM_ERROR) if configuration.site_enable_sftp: sftp_port = configuration.user_sftp_port if configuration.site_enable_sftp_subsys: sftp_port = configuration.user_sftp_subsys_port requested_service = accepted['service'][-1] service = { k: v for options in configuration.jupyter_services for k, v in options.items() if options['service_name'] == requested_service } if not service: valid_services = [ options['name'] for options in configuration.jupyter_services ] output_objects.append({ 'object_type': 'error_text', 'text': '%s is not a valid jupyter service, ' 'allowed include %s' % (requested_service, valid_services) }) return (output_objects, returnvalues.SYSTEM_ERROR) valid_service = valid_jupyter_service(configuration, service) if not valid_service: output_objects.append({ 'object_type': 'error_text', 'text': 'The service %s appears to be misconfigured, ' 'please contact a system administrator about this issue' % requested_service }) return (output_objects, returnvalues.SYSTEM_ERROR) host = get_host_from_service(configuration, service) # Get an active jupyterhost if host is None: logger.error("No active jupyterhub host could be found") output_objects.append({ 'object_type': 'error_text', 'text': 'Failed to establish connection to the %s Jupyter service' % service['service_name'] }) output_objects.append({ 'object_type': 'link', 'destination': 'jupyter.py', 'text': 'Back to Jupyter services overview' }) return (output_objects, returnvalues.SYSTEM_ERROR) remote_user = unescape(os.environ.get('REMOTE_USER', '')).strip() if not remote_user: logger.error("Can't connect to jupyter with an empty REMOTE_USER " "environment variable") output_objects.append({ 'object_type': 'error_text', 'text': 'Failed to establish connection to the Jupyter service' }) return (output_objects, returnvalues.CLIENT_ERROR) # Ensure the remote_user dict can be http posted remote_user = str(remote_user) # TODO, activate admin info # remote_user = {'USER': username, 'IS_ADMIN': is_admin(client_id, # configuration, # logger)} # Regular sftp path mnt_path = os.path.join(configuration.jupyter_mount_files_dir, client_dir) # Subsys sftp path subsys_path = os.path.join(configuration.mig_system_files, 'jupyter_mount') # sftp session path link_home = configuration.sessid_to_jupyter_mount_link_home user_home_dir = os.path.join(configuration.user_home, client_dir) # Preparing prerequisites if not os.path.exists(mnt_path): os.makedirs(mnt_path) if not os.path.exists(link_home): os.makedirs(link_home) if configuration.site_enable_sftp_subsys: if not os.path.exists(subsys_path): os.makedirs(subsys_path) # Make sure ssh daemon does not complain tighten_key_perms(configuration, client_id) url_base = '/' + service['service_name'] url_home = url_base + '/home' url_auth = host + url_base + '/hub/login' url_data = host + url_base + '/hub/user-data' # Does the client home dir contain an active mount key # If so just keep on using it. jupyter_mount_files = [ os.path.join(mnt_path, jfile) for jfile in os.listdir(mnt_path) if jfile.endswith('.jupyter_mount') ] logger.info("User: %s mount files: %s" % (client_id, "\n".join(jupyter_mount_files))) logger.debug("Remote-User %s" % remote_user) active_mounts = [] for jfile in jupyter_mount_files: jupyter_dict = unpickle(jfile, logger) if not jupyter_dict: # Remove failed unpickle logger.error("Failed to unpickle %s removing it" % jfile) remove_jupyter_mount(jfile, configuration) else: # Mount has been timed out if not is_active(jupyter_dict): remove_jupyter_mount(jfile, configuration) else: # Valid mount active_mounts.append({'path': jfile, 'state': jupyter_dict}) logger.debug( "User: %s active keys: %s" % (client_id, "\n".join([mount['path'] for mount in active_mounts]))) # If multiple are active, remove oldest active_mount, old_mounts = get_newest_mount(active_mounts) for mount in old_mounts: remove_jupyter_mount(mount['path'], configuration) # A valid active key is already present redirect straight to the jupyter # service, pass most recent mount information if active_mount is not None: mount_dict = mig_to_mount_adapt(active_mount['state']) user_dict = mig_to_user_adapt(active_mount['state']) logger.debug("Existing header values, Mount: %s User: %s" % (mount_dict, user_dict)) auth_header = {'Remote-User': remote_user} json_data = {'data': {'Mount': mount_dict, 'User': user_dict}} if configuration.site_enable_workflows: workflows_dict = mig_to_workflows_adapt(active_mount['state']) if not workflows_dict: # No cached workflows session could be found -> refresh with a # one workflow_session_id = get_workflow_session_id( configuration, client_id) if not workflow_session_id: workflow_session_id = create_workflow_session_id( configuration, client_id) # TODO get this dynamically url = configuration.migserver_https_sid_url + \ '/cgi-sid/workflowsjsoninterface.py?output_format=json' workflows_dict = { 'WORKFLOWS_URL': url, 'WORKFLOWS_SESSION_ID': workflow_session_id } logger.debug("Existing header values, Workflows: %s" % workflows_dict) json_data['workflows_data'] = {'Session': workflows_dict} with requests.session() as session: # Authenticate and submit data response = session.post(url_auth, headers=auth_header) if response.status_code == 200: response = session.post(url_data, json=json_data) if response.status_code != 200: logger.error( "Jupyter: User %s failed to submit data %s to %s" % (client_id, json_data, url_data)) else: logger.error( "Jupyter: User %s failed to authenticate against %s" % (client_id, url_auth)) # Redirect client to jupyterhub return jupyter_host(configuration, output_objects, remote_user, url_home) # Create a new keyset # Create login session id session_id = generate_random_ascii(2 * session_id_bytes, charset='0123456789abcdef') # Generate private/public keys (mount_private_key, mount_public_key) = generate_ssh_rsa_key_pair(encode_utf8=True) # Known hosts sftp_addresses = socket.gethostbyname_ex( configuration.user_sftp_show_address or socket.getfqdn()) # Subsys sftp support if configuration.site_enable_sftp_subsys: # Restrict possible mount agent auth_content = [] restrict_opts = 'no-agent-forwarding,no-port-forwarding,no-pty,' restrict_opts += 'no-user-rc,no-X11-forwarding' restrictions = '%s' % restrict_opts auth_content.append('%s %s\n' % (restrictions, mount_public_key)) # Write auth file write_file('\n'.join(auth_content), os.path.join(subsys_path, session_id + '.authorized_keys'), logger, umask=027) logger.debug("User: %s - Creating a new jupyter mount keyset - " "private_key: %s public_key: %s " % (client_id, mount_private_key, mount_public_key)) jupyter_dict = { 'MOUNT_HOST': configuration.short_title, 'SESSIONID': session_id, 'USER_CERT': client_id, # don't need fraction precision, also not all systems provide fraction # precision. 'CREATED_TIMESTAMP': int(time.time()), 'MOUNTSSHPRIVATEKEY': mount_private_key, 'MOUNTSSHPUBLICKEY': mount_public_key, # Used by the jupyterhub to know which host to mount against 'TARGET_MOUNT_ADDR': "@" + sftp_addresses[0] + ":", 'PORT': sftp_port } client_email = extract_field(client_id, 'email') if client_email: jupyter_dict.update({'USER_EMAIL': client_email}) if configuration.site_enable_workflows: workflow_session_id = get_workflow_session_id(configuration, client_id) if not workflow_session_id: workflow_session_id = create_workflow_session_id( configuration, client_id) # TODO get this dynamically url = configuration.migserver_https_sid_url + \ '/cgi-sid/workflowsjsoninterface.py?output_format=json' jupyter_dict.update({ 'WORKFLOWS_URL': url, 'WORKFLOWS_SESSION_ID': workflow_session_id }) # Only post the required keys, adapt to API expectations mount_dict = mig_to_mount_adapt(jupyter_dict) user_dict = mig_to_user_adapt(jupyter_dict) workflows_dict = mig_to_workflows_adapt(jupyter_dict) logger.debug("User: %s Mount header: %s" % (client_id, mount_dict)) logger.debug("User: %s User header: %s" % (client_id, user_dict)) if workflows_dict: logger.debug("User: %s Workflows header: %s" % (client_id, workflows_dict)) # Auth and pass a new set of valid mount keys auth_header = {'Remote-User': remote_user} json_data = {'data': {'Mount': mount_dict, 'User': user_dict}} if workflows_dict: json_data['workflows_data'] = {'Session': workflows_dict} # First login with requests.session() as session: # Authenticate response = session.post(url_auth, headers=auth_header) if response.status_code == 200: response = session.post(url_data, json=json_data) if response.status_code != 200: logger.error( "Jupyter: User %s failed to submit data %s to %s" % (client_id, json_data, url_data)) else: logger.error("Jupyter: User %s failed to authenticate against %s" % (client_id, url_auth)) # Update pickle with the new valid key jupyter_mount_state_path = os.path.join(mnt_path, session_id + '.jupyter_mount') pickle(jupyter_dict, jupyter_mount_state_path, logger) # Link jupyter pickle state file linkdest_new_jupyter_mount = os.path.join(mnt_path, session_id + '.jupyter_mount') linkloc_new_jupyter_mount = os.path.join(link_home, session_id + '.jupyter_mount') make_symlink(linkdest_new_jupyter_mount, linkloc_new_jupyter_mount, logger) # Link userhome linkloc_user_home = os.path.join(link_home, session_id) make_symlink(user_home_dir, linkloc_user_home, logger) return jupyter_host(configuration, output_objects, remote_user, url_home)
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False) client_dir = client_id_dir(client_id) defaults = signature()[1] (validate_status, accepted) = validate_input_and_cert( user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) logger.debug("User: %s executing %s" % (client_id, op_name)) if not configuration.site_enable_cloud: output_objects.append({ 'object_type': 'error_text', 'text': 'The cloud service is not enabled on the system' }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'header', 'text': 'Cloud Instance Management' }) user_map = get_full_user_map(configuration) user_dict = user_map.get(client_id, None) # Optional limitation of cload access vgrid permission if not user_dict or not cloud_access_allowed(configuration, user_dict): output_objects.append({ 'object_type': 'error_text', 'text': "You don't have permission to access the cloud facilities on " "this site" }) return (output_objects, returnvalues.CLIENT_ERROR) return_status = returnvalues.OK action = accepted['action'][-1] # NOTE: instance_X may be empty list - fall back to empty string instance_id = ([''] + accepted['instance_id'])[-1] instance_label = ([''] + accepted['instance_label'])[-1] instance_image = ([''] + accepted['instance_image'])[-1] accept_terms = (([''] + accepted['accept_terms'])[-1] in ('yes', 'on')) cloud_id = accepted['service'][-1] service = { k: v for options in configuration.cloud_services for k, v in options.items() if options['service_name'] == cloud_id } if not service: valid_services = [ options['service_name'] for options in configuration.cloud_services ] output_objects.append({ 'object_type': 'error_text', 'text': '%s is not among the valid cloud services: %s' % (cloud_id, ', '.join(valid_services)) }) return (output_objects, returnvalues.CLIENT_ERROR) valid_service = valid_cloud_service(configuration, service) if not valid_service: output_objects.append({ 'object_type': 'error_text', 'text': 'The service %s appears to be misconfigured, ' 'please contact a system administrator about this issue' % cloud_id }) return (output_objects, returnvalues.SYSTEM_ERROR) service_title = service['service_title'] if not action in valid_actions: output_objects.append({ 'object_type': 'error_text', 'text': '%s is not a valid action ' 'allowed actions include %s' % (action, ', '.join(valid_actions)) }) return (output_objects, returnvalues.CLIENT_ERROR) elif action in cloud_edit_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) cloud_flavor = service.get("service_flavor", "openstack") user_home_dir = os.path.join(configuration.user_home, client_dir) client_email = extract_field(client_id, 'email') if not client_email: logger.error("could not extract client email for %s!" % client_id) output_objects.append({ 'object_type': 'error_text', 'text': "No client ID found - can't continue" }) return (output_objects, returnvalues.SYSTEM_ERROR) ssh_auth_msg = "Login requires your private key for your public key:" instance_missing_msg = "Found no '%s' instance at %s. Please contact a " \ + "site administrator if it should be there." _label = instance_label if instance_id and not _label: _, _label, _ = cloud_split_instance_id(configuration, client_id, instance_id) if "create" == action: if not accept_terms: logger.error("refusing create without accepting terms for %s!" % client_id) output_objects.append({ 'object_type': 'error_text', 'text': "You MUST accept the cloud user terms to create instances" }) return (output_objects, returnvalues.CLIENT_ERROR) # Load all instances and make sure none contains label in ID saved_instances = cloud_load_instance(configuration, client_id, cloud_id, keyword_all) for (saved_id, instance) in saved_instances.items(): if instance_label == instance.get('INSTANCE_LABEL', saved_id): logger.error("Refused %s re-create %s cloud instance %s!" % (client_id, cloud_id, instance_label)) output_objects.append({ 'object_type': 'error_text', 'text': "You already have an instance with the label '%s'!" % instance_label }) return (output_objects, returnvalues.CLIENT_ERROR) max_instances = lookup_user_service_value( configuration, client_id, service, 'service_max_user_instances') max_user_instances = int(max_instances) # NOTE: a negative max value means unlimited but 0 or more is enforced if max_user_instances >= 0 and \ len(saved_instances) >= max_user_instances: logger.error("Refused %s create additional %s cloud instances!" % (client_id, cloud_id)) output_objects.append({ 'object_type': 'error_text', 'text': "You already have the maximum allowed %s instances (%d)!" % (service_title, max_user_instances) }) return (output_objects, returnvalues.CLIENT_ERROR) if not instance_label: logger.error("Refused %s create unlabelled %s cloud instance!" % (client_id, cloud_id)) output_objects.append({ 'object_type': 'error_text', 'text': "No instance label provided!" }) return (output_objects, returnvalues.CLIENT_ERROR) # Lookup user-specific allowed images (colon-separated image names) allowed_images = allowed_cloud_images(configuration, client_id, cloud_id, cloud_flavor) if not allowed_images: output_objects.append({ 'object_type': 'error_text', 'text': "No valid / allowed cloud images found!" }) return (output_objects, returnvalues.CLIENT_ERROR) if not instance_image: instance_image = allowed_images[0] logger.info("No image specified - using first for %s in %s: %s" % (client_id, cloud_id, instance_image)) image_id = None for (img_name, img_id, img_alias) in allowed_images: if instance_image == img_name: image_id = img_id break if not image_id: logger.error("No matching image ID found for %s in %s: %s" % (client_id, cloud_id, instance_image)) output_objects.append({ 'object_type': 'error_text', 'text': "No such image found: %s" % instance_image }) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: remove this direct key injection if we can delay it cloud_settings = load_cloud(client_id, configuration) raw_keys = cloud_settings.get('authkeys', '').split('\n') auth_keys = [i.split('#', 1)[0].strip() for i in raw_keys] auth_keys = [i for i in auth_keys if i] if not auth_keys: logger.error("No cloud pub keys setup for %s - refuse create" % client_id) output_objects.append({ 'object_type': 'error_text', 'text': """ You haven't provided any valid ssh pub key(s) for cloud instance login, which is stricly required for all use. Please do so before you try again. """ }) output_objects.append({ 'object_type': 'link', 'destination': 'setup.py?topic=cloud', 'text': 'Open cloud setup', 'class': 'cloudsetuplink iconspace', 'title': 'open cloud setup', 'target': '_blank' }) return (output_objects, returnvalues.CLIENT_ERROR) logger.debug("Continue create for %s with auth_keys: %s" % (client_id, auth_keys)) # Create a new internal keyset and session id (priv_key, pub_key) = generate_ssh_rsa_key_pair(encode_utf8=True) session_id = generate_random_ascii(session_id_bytes, charset='0123456789abcdef') # We make sure to create instance with a globally unique ID on the # cloud while only showing the requested instance_label to the user. instance_id = cloud_build_instance_id(configuration, client_email, instance_label, session_id) # TODO: make more fields flexible/conf cloud_dict = { 'INSTANCE_ID': instance_id, 'INSTANCE_LABEL': instance_label, 'INSTANCE_IMAGE': instance_image, 'IMAGE_ID': image_id, 'AUTH_KEYS': auth_keys, 'USER_CERT': client_id, 'INSTANCE_PRIVATE_KEY': priv_key, 'INSTANCE_PUBLIC_KEY': pub_key, # don't need fraction precision, also not all systems provide fraction # precision. 'CREATED_TIMESTAMP': int(time.time()), # Init unset ssh address and leave for floating IP assigment below 'INSTANCE_SSH_IP': '', 'INSTANCE_SSH_PORT': 22, } (action_status, action_msg) = create_cloud_instance(configuration, client_id, cloud_id, cloud_flavor, instance_id, image_id, auth_keys) if not action_status: logger.error( "%s %s cloud instance %s for %s failed: %s" % (action, cloud_id, instance_id, client_id, action_msg)) output_objects.append({ 'object_type': 'error_text', 'text': 'Your %s instance %s at %s did not succeed: %s' % (action, instance_label, service_title, action_msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) # On success the action_msg contains the assigned floating IP address instance_ssh_fqdn = action_msg cloud_dict['INSTANCE_SSH_IP'] = instance_ssh_fqdn if not cloud_save_instance(configuration, client_id, cloud_id, instance_id, cloud_dict): logger.error("save new %s cloud instance %s for %s failed" % (cloud_id, instance_id, client_id)) output_objects.append({ 'object_type': 'error_text', 'text': 'Error saving your %s cloud instance setup' % service_title }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': "%s instance %s at %s: %s" % (action, instance_label, service_title, "success") }) output_objects.append({ 'object_type': 'html_form', 'text': _ssh_help(configuration, client_id, cloud_id, cloud_dict, instance_id) }) elif "delete" == action: saved_instance = cloud_load_instance(configuration, client_id, cloud_id, instance_id) if not saved_instance: logger.error("no saved %s cloud instance %s for %s to delete" % (cloud_id, instance_id, client_id)) output_objects.append({ 'object_type': 'error_text', 'text': instance_missing_msg % (_label, service_title) }) return (output_objects, returnvalues.CLIENT_ERROR) (action_status, action_msg) = delete_cloud_instance(configuration, client_id, cloud_id, cloud_flavor, instance_id) if not action_status: logger.error( "%s %s cloud instance %s for %s failed: %s" % (action, cloud_id, instance_id, client_id, action_msg)) output_objects.append({ 'object_type': 'error_text', 'text': 'Your %s instance %s at %s did not succeed: %s' % (action, _label, service_title, action_msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) if not cloud_purge_instance(configuration, client_id, cloud_id, instance_id): logger.error("purge %s cloud instance %s for %s failed" % (cloud_id, instance_id, client_id)) output_objects.append({ 'object_type': 'error_text', 'text': 'Error deleting your %s cloud instance setup' % service_title }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': "%s instance %s at %s: %s" % (action, _label, service_title, "success") }) elif "status" == action: saved_instance = cloud_load_instance(configuration, client_id, cloud_id, instance_id) if not saved_instance: logger.error("no saved %s cloud instance %s for %s to query" % (cloud_id, instance_id, client_id)) output_objects.append({ 'object_type': 'error_text', 'text': instance_missing_msg % (_label, service_title) }) return (output_objects, returnvalues.CLIENT_ERROR) (action_status, action_msg) = status_of_cloud_instance(configuration, client_id, cloud_id, cloud_flavor, instance_id) if not action_status: logger.error( "%s %s cloud instance %s for %s failed: %s" % (action, cloud_id, instance_id, client_id, action_msg)) output_objects.append({ 'object_type': 'error_text', 'text': 'Your %s instance %s at %s did not succeed: %s' % (action, _label, service_title, action_msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': "%s instance %s at %s: %s" % (action, _label, service_title, action_msg) }) # Show instance access details if running if action_msg in ('ACTIVE', 'RUNNING'): # Only include web console if explicitly configured if configuration.user_cloud_console_access: (console_status, console_msg) = web_access_cloud_instance( configuration, client_id, cloud_id, cloud_flavor, instance_id) if not console_status: logger.error( "%s cloud instance %s console for %s failed: %s" % \ (cloud_id, instance_id, client_id, console_msg)) output_objects.append({ 'object_type': 'error_text', 'text': 'Failed to get instance %s at %s console: %s' % (_label, service_title, console_msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) logger.info("%s cloud instance %s console for %s: %s" % (cloud_id, instance_id, client_id, console_msg)) output_objects.append({ 'object_type': 'link', 'destination': console_msg, 'text': 'Open web console', 'class': 'consolelink iconspace', 'title': 'open web console', 'target': '_blank' }) output_objects.append({'object_type': 'text', 'text': ''}) output_objects.append({ 'object_type': 'html_form', 'text': _ssh_help(configuration, client_id, cloud_id, saved_instance, instance_id) }) output_objects.append({'object_type': 'text', 'text': ''}) elif "start" == action: saved_instance = cloud_load_instance(configuration, client_id, cloud_id, instance_id) if not saved_instance: logger.error("no saved %s cloud instance %s for %s to start" % (cloud_id, instance_id, client_id)) output_objects.append({ 'object_type': 'error_text', 'text': instance_missing_msg % (_label, service_title) }) return (output_objects, returnvalues.CLIENT_ERROR) (action_status, action_msg) = start_cloud_instance(configuration, client_id, cloud_id, cloud_flavor, instance_id) if not action_status: logger.error( "%s %s cloud instance %s for %s failed: %s" % (action, cloud_id, instance_id, client_id, action_msg)) output_objects.append({ 'object_type': 'error_text', 'text': 'Your %s instance %s at %s did not succeed: %s' % (action, _label, service_title, action_msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': "%s instance %s at %s: %s" % (action, _label, service_title, "success") }) output_objects.append({ 'object_type': 'html_form', 'text': _ssh_help(configuration, client_id, cloud_id, saved_instance, instance_id) }) elif action in ("softrestart", "hardrestart"): boot_strength = action.replace("restart", "").upper() saved_instance = cloud_load_instance(configuration, client_id, cloud_id, instance_id) if not saved_instance: logger.error("no saved %s cloud instance %s for %s to restart" % (cloud_id, instance_id, client_id)) output_objects.append({ 'object_type': 'error_text', 'text': instance_missing_msg % (_label, service_title) }) return (output_objects, returnvalues.CLIENT_ERROR) (action_status, action_msg) = restart_cloud_instance(configuration, client_id, cloud_id, cloud_flavor, instance_id, boot_strength) if not action_status: logger.error( "%s %s cloud instance %s for %s failed: %s" % (action, cloud_id, instance_id, client_id, action_msg)) output_objects.append({ 'object_type': 'error_text', 'text': 'Your %s instance %s at %s did not succeed: %s' % (action, _label, service_title, action_msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': "%s instance %s at %s: %s" % (action, _label, service_title, "success") }) output_objects.append({ 'object_type': 'html_form', 'text': _ssh_help(configuration, client_id, cloud_id, saved_instance, instance_id) }) elif "stop" == action: saved_instance = cloud_load_instance(configuration, client_id, cloud_id, instance_id) if not saved_instance: logger.error("no saved %s cloud instance %s for %s to %s" % (cloud_id, instance_id, client_id, action)) output_objects.append({ 'object_type': 'error_text', 'text': instance_missing_msg % (_label, service_title) }) return (output_objects, returnvalues.CLIENT_ERROR) (action_status, action_msg) = stop_cloud_instance(configuration, client_id, cloud_id, cloud_flavor, instance_id) if not action_status: logger.error( "%s %s cloud instance %s for %s failed: %s" % (action, cloud_id, instance_id, client_id, action_msg)) output_objects.append({ 'object_type': 'error_text', 'text': 'Your %s instance %s at %s did not succeed: %s' % (action, _label, service_title, action_msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': "%s instance %s at %s: %s" % (action, _label, service_title, "success") }) elif "webaccess" == action: saved_instance = cloud_load_instance(configuration, client_id, cloud_id, instance_id) if not saved_instance: logger.error("no saved %s cloud instance %s for %s to query" % (cloud_id, instance_id, client_id)) output_objects.append({ 'object_type': 'error_text', 'text': instance_missing_msg % (_label, service_title) }) return (output_objects, returnvalues.CLIENT_ERROR) if not configuration.user_cloud_console_access: logger.error("web console not enabled in conf!") output_objects.append({ 'object_type': 'error_text', 'text': 'Site does not expose cloud web console!' }) return (output_objects, returnvalues.CLIENT_ERROR) (action_status, action_msg) = web_access_cloud_instance(configuration, client_id, cloud_id, cloud_flavor, instance_id) if not action_status: logger.error( "%s %s cloud instance %s for %s failed: %s" % (action, service_title, instance_id, client_id, action_msg)) output_objects.append({ 'object_type': 'error_text', 'text': 'Your %s instance %s at %s did not succeed: %s' % (action, _label, service_title, action_msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': "%s instance %s at %s" % (action, _label, service_title) }) output_objects.append({ 'object_type': 'link', 'destination': action_msg, 'text': 'Open web console', 'class': 'consolelink iconspace', 'title': 'open web console', 'target': '_blank' }) output_objects.append({'object_type': 'text', 'text': ''}) output_objects.append({ 'object_type': 'html_form', 'text': _ssh_help(configuration, client_id, cloud_id, saved_instance, instance_id) }) elif "updatekeys" == action: saved_instance = cloud_load_instance(configuration, client_id, cloud_id, instance_id) if not saved_instance: logger.error("no saved %s cloud instance %s for %s to update" % (cloud_id, instance_id, client_id)) output_objects.append({ 'object_type': 'error_text', 'text': instance_missing_msg % (_label, service_title) }) return (output_objects, returnvalues.CLIENT_ERROR) cloud_settings = load_cloud(client_id, configuration) auth_keys = cloud_settings.get('authkeys', '').split('\n') (action_status, action_msg) = update_cloud_instance_keys(configuration, client_id, cloud_id, cloud_flavor, instance_id, auth_keys) if not action_status: logger.error( "%s %s cloud instance %s for %s failed: %s" % (action, cloud_id, instance_id, client_id, action_msg)) output_objects.append({ 'object_type': 'error_text', 'text': 'Your %s instance %s at %s did not succeed: %s' % (action, _label, service_title, action_msg) }) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': "%s instance %s at %s: %s" % (action, _label, service_title, "success") }) output_objects.append({ 'object_type': 'html_form', 'text': _ssh_help(configuration, client_id, cloud_id, saved_instance, instance_id) }) output_objects.append({'object_type': 'text', 'text': ssh_auth_msg}) for pub_key in auth_keys: output_objects.append({'object_type': 'text', 'text': pub_key}) else: output_objects.append({ 'object_type': 'error_text', 'text': 'Unknown action: %s' % action }) return_status = returnvalues.CLIENT_ERROR output_objects.append({ 'object_type': 'link', 'destination': 'cloud.py', 'class': 'backlink iconspace', 'title': 'Go back to cloud management', 'text': 'Back to cloud management' }) return (output_objects, return_status)
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False) defaults = signature()[1] (validate_status, accepted) = validate_input_and_cert( user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) title_entry = find_entry(output_objects, 'title') title_entry['text'] = 'Dashboard' # jquery support for tablesorter and confirmation on "leave": add_import, add_init, add_ready = '', '', '' add_init += ''' function roundNumber(num, dec) { var result = Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); return result; } ''' add_ready += ''' $("#jobs_stats").addClass("spinner iconleftpad"); $("#jobs_stats").html("Loading job stats..."); $("#res_stats").addClass("spinner iconleftpad"); $("#res_stats").html("Loading resource stats..."); $("#disk_stats").addClass("spinner iconleftpad"); $("#disk_stats").html("Loading disk stats..."); $("#cert_stats").addClass("spinner iconleftpad"); $("#cert_stats").html("Loading certificate information..."); /* Run certificate request in the background and handle as soon as results come in */ $.ajax({ url: "userstats.py?output_format=json;stats=certificate", type: "GET", dataType: "json", cache: false, success: function(jsonRes, textStatus) { var i = 0; var certificate = null; var renew_days = 30; var day_msecs = 24*60*60*1000; // Grab results from json response and place them in resource status. for(i=0; i<jsonRes.length; i++) { if (jsonRes[i].object_type == "user_stats") { certificate = jsonRes[i].certificate; $("#cert_stats").removeClass("spinner iconleftpad"); $("#cert_stats").empty(); if (certificate.expire == -1) { break; } var expire_date = new Date(certificate.expire); $("#cert_stats").append("Your user certificate expires on " + expire_date + "."); // Use date from time diff in ms to avoid calendar mangling var show_renew = new Date(expire_date.getTime() - renew_days*day_msecs); if(new Date().getTime() > show_renew.getTime()) { $("#cert_stats").addClass("warningtext"); $("#cert_stats").append(" <a class=\'certrenewlink iconspace\' href=\'reqcert.py\'>Renew certificate</a>."); } break; } } } }); /* Run jobs request in the background and handle as soon as results come in */ $.ajax({ url: "userstats.py?output_format=json;stats=jobs", type: "GET", dataType: "json", cache: false, success: function(jsonRes, textStatus) { var i = 0; var jobs = null; // Grab results from json response and place them in job status. for(i=0; i<jsonRes.length; i++) { if (jsonRes[i].object_type == "user_stats") { jobs = jsonRes[i].jobs; //alert("inspect stats result: " + jobs); $("#jobs_stats").removeClass("spinner iconleftpad"); $("#jobs_stats").empty(); $("#jobs_stats").append("You have submitted a total of " + jobs.total + " jobs: " + jobs.parse + " parse, " + jobs.queued + " queued, " + jobs.frozen + " frozen, " + jobs.executing + " executing, " + jobs.finished + " finished, " + jobs.retry + " retry, " + jobs.canceled + " canceled, " + jobs.expired + " expired and " + jobs.failed + " failed."); break; } } } }); /* Run resources request in the background and handle as soon as results come in */ $.ajax({ url: "userstats.py?output_format=json;stats=resources", type: "GET", dataType: "json", cache: false, success: function(jsonRes, textStatus) { var i = 0; var resources = null; // Grab results from json response and place them in resource status. for(i=0; i<jsonRes.length; i++) { if (jsonRes[i].object_type == "user_stats") { resources = jsonRes[i].resources; //alert("inspect resources stats result: " + resources); $("#res_stats").removeClass("spinner iconleftpad"); $("#res_stats").empty(); $("#res_stats").append(resources.resources + " resources providing " + resources.exes + " execution units in total allow execution of your jobs."); break; } } } }); /* Run disk request in the background and handle as soon as results come in */ $.ajax({ url:"userstats.py?output_format=json;stats=disk", type: "GET", dataType: "json", cache: false, success: function(jsonRes, textStatus) { var i = 0; var disk = null; // Grab results from json response and place them in resource status. for(i=0; i<jsonRes.length; i++) { if (jsonRes[i].object_type == "user_stats") { disk = jsonRes[i].disk; //alert("inspect disk stats result: " + disk); $("#disk_stats").removeClass("spinner iconleftpad"); $("#disk_stats").empty(); $("#disk_stats").append("Your own " + disk.own_files +" files and " + disk.own_directories + " directories take up " + roundNumber(disk.own_megabytes, 2) + " MB in total and you additionally share " + disk.vgrid_files + " files and " + disk.vgrid_directories + " directories of " + roundNumber(disk.vgrid_megabytes, 2) + " MB in total."); break; } } } }); ''' title_entry['script']['advanced'] += add_import title_entry['script']['init'] += add_init title_entry['script']['ready'] += add_ready output_objects.append({'object_type': 'header', 'text': 'Dashboard'}) output_objects.append({'object_type': 'sectionheader', 'text': "Welcome to the %s" % configuration.site_title}) welcome_line = "Hi %s" % extract_field(client_id, "full_name") output_objects.append({'object_type': 'text', 'text': welcome_line}) dashboard_info = """ This is your private entry page or your dashboard where you can get a quick status overview and find pointers to help and documentation. When you are logged in with your user credentials/certificate, as you are now, you can navigate your pages using the menu on the left. """ % os.environ output_objects.append({'object_type': 'text', 'text': dashboard_info}) output_objects.append({'object_type': 'sectionheader', 'text': "Your Status"}) output_objects.append({'object_type': 'html_form', 'text': ''' <p> This is a general status overview for your Grid activities. Please note that some of the numbers are cached for a while to keep server load down. </p> <div id="jobs_stats"><!-- for jquery --></div><br /> <div id="res_stats"><!-- for jquery --></div><br /> <div id="disk_stats"><!-- for jquery --></div><br /> <div id="cert_stats"><!-- for jquery --></div><br /> <div id="cert_renew" class="hidden"><a href="reqcert.py">renew certificate</a> </div> '''}) output_objects.append({'object_type': 'sectionheader', 'text': 'Documentation and Help'}) online_help = """ %s includes some built-in documentation like the """ % configuration.site_title output_objects.append({'object_type': 'text', 'text': online_help}) output_objects.append({'object_type': 'link', 'destination': 'docs.py', 'class': 'infolink iconspace', 'title': 'built-in documentation', 'text': 'Docs page'}) project_info = """ but additional help, background information and tutorials are available in the """ output_objects.append({'object_type': 'text', 'text': project_info}) output_objects.append({'object_type': 'link', 'destination': configuration.site_external_doc, 'class': 'urllink iconspace', 'title': 'external documentation', 'text': 'external %s documentation' % configuration.site_title}) output_objects.append({'object_type': 'sectionheader', 'text': "Personal Settings"}) settings_info = """ You can customize your personal pages by opening the Settings page from the navigation menu and entering personal preferences. In that way you can completely redecorate your interface and configure things like notification, profile visibility and remote file access. """ output_objects.append({'object_type': 'text', 'text': settings_info}) #env_info = """Env %s""" % os.environ #output_objects.append({'object_type': 'text', 'text': env_info}) return (output_objects, returnvalues.OK)
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False) defaults = signature()[1] title_entry = find_entry(output_objects, 'title') label = "%s" % configuration.site_vgrid_label title_entry['text'] = '%s send request' % configuration.short_title output_objects.append({ 'object_type': 'header', 'text': '%s send request' % configuration.short_title }) (validate_status, accepted) = validate_input_and_cert( user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) target_id = client_id vgrid_name = accepted['vgrid_name'][-1].strip() visible_user_names = accepted['cert_id'] visible_res_names = accepted['unique_resource_name'] request_type = accepted['request_type'][-1].strip().lower() request_text = accepted['request_text'][-1].strip() protocols = [proto.strip() for proto in accepted['protocol']] use_any = False if any_protocol in protocols: use_any = True protocols = configuration.notify_protocols protocols = [proto.lower() for proto in protocols] if not safe_handler(configuration, 'post', op_name, client_id, get_csrf_limit(configuration), accepted): output_objects.append({ 'object_type': 'error_text', 'text': '''Only accepting CSRF-filtered POST requests to prevent unintended updates''' }) return (output_objects, returnvalues.CLIENT_ERROR) valid_request_types = [ 'resourceowner', 'resourceaccept', 'resourcereject', 'vgridowner', 'vgridmember', 'vgridresource', 'vgridaccept', 'vgridreject', 'plain' ] if not request_type in valid_request_types: output_objects.append({ 'object_type': 'error_text', 'text': '%s is not a valid request_type (valid types: %s)!' % (request_type.lower(), valid_request_types) }) return (output_objects, returnvalues.CLIENT_ERROR) if not protocols: output_objects.append({ 'object_type': 'error_text', 'text': 'No protocol specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) user_map = get_user_map(configuration) reply_to = user_map[client_id][USERID] # Try to point replies to client_id email client_email = extract_field(reply_to, 'email') if request_type == "plain": if not visible_user_names: output_objects.append({ 'object_type': 'error_text', 'text': 'No user ID specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) user_id = visible_user_names[-1].strip() anon_map = anon_to_real_user_map(configuration) if anon_map.has_key(user_id): user_id = anon_map[user_id] if not user_map.has_key(user_id): output_objects.append({ 'object_type': 'error_text', 'text': 'No such user: %s' % user_id }) return (output_objects, returnvalues.CLIENT_ERROR) target_name = user_id user_dict = user_map[user_id] vgrid_access = user_vgrid_access(configuration, client_id) vgrids_allow_email = user_dict[CONF].get('VGRIDS_ALLOW_EMAIL', []) vgrids_allow_im = user_dict[CONF].get('VGRIDS_ALLOW_IM', []) if any_vgrid in vgrids_allow_email: email_vgrids = vgrid_access else: email_vgrids = set(vgrids_allow_email).intersection(vgrid_access) if any_vgrid in vgrids_allow_im: im_vgrids = vgrid_access else: im_vgrids = set(vgrids_allow_im).intersection(vgrid_access) if use_any: # Do not try disabled protocols if ANY was requested if not email_vgrids: protocols = [ proto for proto in protocols if proto not in email_keyword_list ] if not im_vgrids: protocols = [ proto for proto in protocols if proto in email_keyword_list ] if not email_vgrids and [ proto for proto in protocols if proto in email_keyword_list ]: output_objects.append({ 'object_type': 'error_text', 'text': 'You are not allowed to send emails to %s!' % user_id }) return (output_objects, returnvalues.CLIENT_ERROR) if not im_vgrids and [ proto for proto in protocols if proto not in email_keyword_list ]: output_objects.append({ 'object_type': 'error_text', 'text': 'You are not allowed to send instant messages to %s!' % user_id }) return (output_objects, returnvalues.CLIENT_ERROR) for proto in protocols: if not user_dict[CONF].get(proto.upper(), False): if use_any: # Remove missing protocols if ANY protocol was requested protocols = [i for i in protocols if i != proto] else: output_objects.append({ 'object_type': 'error_text', 'text': 'User %s does not accept %s messages!' % (user_id, proto) }) return (output_objects, returnvalues.CLIENT_ERROR) if not protocols: output_objects.append({ 'object_type': 'error_text', 'text': 'User %s does not accept requested protocol(s) messages!' % user_id }) return (output_objects, returnvalues.CLIENT_ERROR) target_list = [user_id] elif request_type in ["vgridaccept", "vgridreject"]: # Always allow accept messages but only between owners/members if not visible_user_names and not visible_res_names: output_objects.append({ 'object_type': 'error_text', 'text': 'No user or resource ID specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) if not vgrid_name: output_objects.append({ 'object_type': 'error_text', 'text': 'No vgrid_name specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) if vgrid_name.upper() == default_vgrid.upper(): output_objects.append({ 'object_type': 'error_text', 'text': 'No requests for %s are allowed!' % default_vgrid }) return (output_objects, returnvalues.CLIENT_ERROR) if not vgrid_is_owner(vgrid_name, client_id, configuration): output_objects.append({ 'object_type': 'error_text', 'text': 'You are not an owner of %s or a parent %s!' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: we support exactly one vgrid but multiple users/resources here if visible_user_names: logger.info("setting user recipients: %s" % visible_user_names) target_list = [user_id.strip() for user_id in visible_user_names] elif visible_res_names: # vgrid resource accept - lookup and notify resource owners logger.info("setting res owner recipients: %s" % visible_res_names) target_list = [] for unique_resource_name in visible_res_names: logger.info("loading res owners for %s" % unique_resource_name) (load_status, res_owners) = resource_owners(configuration, unique_resource_name) if not load_status: output_objects.append({ 'object_type': 'error_text', 'text': 'Could not lookup owners of %s!' % unique_resource_name }) continue logger.info("adding res owners to recipients: %s" % res_owners) target_list += [user_id for user_id in res_owners] target_id = '%s %s owners' % (vgrid_name, label) target_name = vgrid_name elif request_type in ["resourceaccept", "resourcereject"]: # Always allow accept messages between actual resource owners if not visible_user_names: output_objects.append({ 'object_type': 'error_text', 'text': 'No user ID specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) if not visible_res_names: output_objects.append({ 'object_type': 'error_text', 'text': 'No resource ID specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: we support exactly one resource but multiple users here unique_resource_name = visible_res_names[-1].strip() target_name = unique_resource_name res_map = get_resource_map(configuration) if not res_map.has_key(unique_resource_name): output_objects.append({ 'object_type': 'error_text', 'text': 'No such resource: %s' % unique_resource_name }) return (output_objects, returnvalues.CLIENT_ERROR) owners_list = res_map[unique_resource_name][OWNERS] if not client_id in owners_list: output_objects.append({ 'object_type': 'error_text', 'text': 'You are not an owner of %s!' % unique_resource_name }) output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid resource %s message!' % request_type }) return (output_objects, returnvalues.CLIENT_ERROR) target_id = '%s resource owners' % unique_resource_name target_name = unique_resource_name target_list = [user_id.strip() for user_id in visible_user_names] elif request_type == "resourceowner": if not visible_res_names: output_objects.append({ 'object_type': 'error_text', 'text': 'No resource ID specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: we support exactly one resource but multiple users here unique_resource_name = visible_res_names[-1].strip() anon_map = anon_to_real_res_map(configuration.resource_home) if anon_map.has_key(unique_resource_name): unique_resource_name = anon_map[unique_resource_name] target_name = unique_resource_name res_map = get_resource_map(configuration) if not res_map.has_key(unique_resource_name): output_objects.append({ 'object_type': 'error_text', 'text': 'No such resource: %s' % unique_resource_name }) return (output_objects, returnvalues.CLIENT_ERROR) target_list = res_map[unique_resource_name][OWNERS] if client_id in target_list: output_objects.append({ 'object_type': 'error_text', 'text': 'You are already an owner of %s!' % unique_resource_name }) return (output_objects, returnvalues.CLIENT_ERROR) request_dir = os.path.join(configuration.resource_home, unique_resource_name) access_request = { 'request_type': request_type, 'entity': client_id, 'target': unique_resource_name, 'request_text': request_text } if not save_access_request(configuration, request_dir, access_request): output_objects.append({ 'object_type': 'error_text', 'text': 'Could not save request - owners may still manually add you' }) return (output_objects, returnvalues.SYSTEM_ERROR) elif request_type in ["vgridmember", "vgridowner", "vgridresource"]: if not vgrid_name: output_objects.append({ 'object_type': 'error_text', 'text': 'No vgrid_name specified!' }) return (output_objects, returnvalues.CLIENT_ERROR) # default vgrid is read-only if vgrid_name.upper() == default_vgrid.upper(): output_objects.append({ 'object_type': 'error_text', 'text': 'No requests for %s are not allowed!' % default_vgrid }) return (output_objects, returnvalues.CLIENT_ERROR) # stop owner or member request if already an owner # and prevent repeated resource access requests if request_type == 'vgridresource': # NOTE: we support exactly one resource here unique_resource_name = visible_res_names[-1].strip() target_id = entity = unique_resource_name if vgrid_is_resource(vgrid_name, unique_resource_name, configuration): output_objects.append({ 'object_type': 'error_text', 'text': 'You already have access to %s or a parent %s.' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) else: target_id = entity = client_id if vgrid_is_owner(vgrid_name, client_id, configuration): output_objects.append({ 'object_type': 'error_text', 'text': 'You are already an owner of %s or a parent %s!' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) # only ownership requests are allowed for existing members if request_type == 'vgridmember': if vgrid_is_member(vgrid_name, client_id, configuration): output_objects.append({ 'object_type': 'error_text', 'text': 'You are already a member of %s or a parent %s.' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) # Find all VGrid owners configured to receive notifications target_name = vgrid_name (settings_status, settings_dict) = vgrid_settings(vgrid_name, configuration, recursive=True, as_dict=True) if not settings_status: settings_dict = {} request_recipients = settings_dict.get('request_recipients', default_vgrid_settings_limit) # We load and use direct owners first if any - otherwise inherited owners_list = [] for inherited in (False, True): (owners_status, owners_list) = vgrid_owners(vgrid_name, configuration, recursive=inherited) if not owners_status: output_objects.append({ 'object_type': 'error_text', 'text': 'Failed to lookup owners for %s %s - sure it exists?' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) elif owners_list: break if not owners_list: output_objects.append({ 'object_type': 'error_text', 'text': 'Failed to lookup owners for %s %s - sure it exists?' % (vgrid_name, label) }) return (output_objects, returnvalues.CLIENT_ERROR) # Now we have direct or inherited owners to notify target_list = owners_list[:request_recipients] request_dir = os.path.join(configuration.vgrid_home, vgrid_name) access_request = { 'request_type': request_type, 'entity': entity, 'target': vgrid_name, 'request_text': request_text } if not save_access_request(configuration, request_dir, access_request): output_objects.append({ 'object_type': 'error_text', 'text': 'Could not save request - owners may still manually add you' }) return (output_objects, returnvalues.SYSTEM_ERROR) else: output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid request type: %s' % request_type }) return (output_objects, returnvalues.CLIENT_ERROR) # Now send request to all targets in turn # TODO: inform requestor if no owners have mail/IM set in their settings logger.debug("sending notification to recipients: %s" % target_list) for target in target_list: if not target: logger.warning("skipping empty notify target: %s" % target_list) continue # USER_CERT entry is destination notify = [] for proto in protocols: notify.append('%s: SETTINGS' % proto) job_dict = { 'NOTIFY': notify, 'JOB_ID': 'NOJOBID', 'USER_CERT': target, 'EMAIL_SENDER': client_email } notifier = notify_user_thread( job_dict, [target_id, target_name, request_type, request_text, reply_to], 'SENDREQUEST', logger, '', configuration, ) # Try finishing delivery but do not block forever on one message notifier.join(30) output_objects.append({ 'object_type': 'text', 'text': 'Sent %s message to %d people' % (request_type, len(target_list)) }) output_objects.append({ 'object_type': 'text', 'text': """Please make sure you have notifications configured on your Setings page if you expect a reply to this message""" }) return (output_objects, returnvalues.OK)
def initialize_main_variables(client_id, op_title=True, op_header=True, op_menu=True): """Script initialization is identical for most scripts in shared/functionalty. This function should be called in most cases. """ configuration = get_configuration_object() logger = configuration.logger output_objects = [] start_entry = make_start_entry() output_objects.append(start_entry) op_name = os.path.splitext(os.path.basename(requested_page()))[0] if op_title: skipwidgets = not configuration.site_enable_widgets or not client_id skipuserstyle = not configuration.site_enable_styling or not client_id title_object = make_title_entry('%s' % op_name, skipmenu=(not op_menu), skipwidgets=skipwidgets, skipuserstyle=skipuserstyle, skipuserprofile=(not client_id)) # Make sure base_menu is always set for extract_menu # Typicall overriden based on client_id cases below title_object['base_menu'] = configuration.site_default_menu output_objects.append(title_object) if op_header: header_object = make_header_entry('%s' % op_name) output_objects.append(header_object) if client_id: # add the user-defined menu and widgets (if possible) title = find_entry(output_objects, 'title') if title: settings = load_settings(client_id, configuration) # NOTE: loaded settings may be False rather than dict here if not settings: settings = {} title['style'] = themed_styles(configuration, user_settings=settings) title['script'] = themed_scripts(configuration, user_settings=settings) if settings: title['user_settings'] = settings base_menu = settings.get('SITE_BASE_MENU', 'default') if not base_menu in configuration.site_base_menu: base_menu = 'default' if base_menu == 'simple' and configuration.site_simple_menu: title['base_menu'] = configuration.site_simple_menu elif base_menu == 'advanced' and \ configuration.site_advanced_menu: title['base_menu'] = configuration.site_advanced_menu else: title['base_menu'] = configuration.site_default_menu user_menu = settings.get('SITE_USER_MENU', None) if configuration.site_user_menu and user_menu: title['user_menu'] = user_menu if settings.get('ENABLE_WIDGETS', True) and \ configuration.site_script_deps: user_widgets = load_widgets(client_id, configuration) if user_widgets: title['user_widgets'] = user_widgets user_profile = load_profile(client_id, configuration) if user_profile: # These data are used for display in own profile view only profile_image_list = user_profile.get('PUBLIC_IMAGE', []) if profile_image_list: # TODO: copy profile image to /public/avatars/X and use it profile_image = os.path.join( configuration.site_user_redirect, profile_image_list[-1]) else: profile_image = '' user_profile['profile_image'] = profile_image else: user_profile = {} # Always set full name for use in personal user menu full_name = extract_field(client_id, 'full_name') user_profile['full_name'] = full_name title['user_profile'] = user_profile logger.debug('setting user profile: %s' % user_profile) else: # No user so we just enforce default site style and scripts title = find_entry(output_objects, 'title') if title: title['style'] = themed_styles(configuration) title['script'] = themed_scripts(configuration, logged_in=False) return (configuration, logger, output_objects, op_name)
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False) client_dir = client_id_dir(client_id) defaults = signature()[1] (validate_status, accepted) = validate_input_and_cert( user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, ) if not validate_status: return (accepted, returnvalues.CLIENT_ERROR) action = accepted['action'][-1] share_id = accepted['share_id'][-1] path = accepted['path'][-1] read_access = accepted['read_access'][-1].lower() in enabled_strings write_access = accepted['write_access'][-1].lower() in enabled_strings expire = accepted['expire'][-1] # Merge and split invite to make sure 'a@b, c@d' entries are handled invite_list = ','.join(accepted['invite']).split(',') invite_list = [i for i in invite_list if i] invite_msg = accepted['msg'] title_entry = find_entry(output_objects, 'title') title_entry['text'] = 'Share Link' # jquery support for tablesorter and confirmation on delete/redo: # table initially sorted by 5, 4 reversed (active first and in growing age) table_spec = {'table_id': 'sharelinkstable', 'sort_order': '[[5,1],[4,1]]'} (add_import, add_init, add_ready) = man_base_js(configuration, [table_spec], {'width': 600}) title_entry['script']['advanced'] += add_import title_entry['script']['init'] += add_init title_entry['script']['ready'] += add_ready output_objects.append({ 'object_type': 'html_form', 'text': man_base_html(configuration) }) header_entry = {'object_type': 'header', 'text': 'Manage share links'} output_objects.append(header_entry) if not configuration.site_enable_sharelinks: output_objects.append({ 'object_type': 'text', 'text': ''' Share links are disabled on this site. Please contact the site admins %s if you think they should be enabled. ''' % configuration.admin_email }) return (output_objects, returnvalues.OK) logger.info('sharelink %s from %s' % (action, client_id)) logger.debug('sharelink from %s: %s' % (client_id, accepted)) if not action in valid_actions: output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid action "%s" (supported: %s)' % (action, ', '.join(valid_actions)) }) return (output_objects, returnvalues.CLIENT_ERROR) if action in post_actions: if not safe_handler(configuration, 'post', op_name, client_id, get_csrf_limit(configuration), accepted): output_objects.append({ 'object_type': 'error_text', 'text': '''Only accepting CSRF-filtered POST requests to prevent unintended updates''' }) return (output_objects, returnvalues.CLIENT_ERROR) (load_status, share_map) = load_share_links(configuration, client_id) if not load_status: share_map = {} form_method = 'post' csrf_limit = get_csrf_limit(configuration) target_op = 'sharelink' csrf_token = make_csrf_token(configuration, form_method, target_op, client_id, csrf_limit) if action in get_actions: if action == "show": # Table columns to skip skip_list = ['owner', 'single_file', 'expire'] sharelinks = [] for (saved_id, share_dict) in share_map.items(): share_item = build_sharelinkitem_object( configuration, share_dict) js_name = 'delete%s' % hexlify(saved_id) helper = html_post_helper( js_name, '%s.py' % target_op, { 'share_id': saved_id, 'action': 'delete', csrf_field: csrf_token }) output_objects.append({ 'object_type': 'html_form', 'text': helper }) share_item['delsharelink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really remove %s?' % saved_id), 'class': 'removelink iconspace', 'title': 'Remove share link %s' % saved_id, 'text': '' } sharelinks.append(share_item) # Display share links and form to add new ones output_objects.append({ 'object_type': 'sectionheader', 'text': 'Share Links' }) output_objects.append({ 'object_type': 'table_pager', 'entry_name': 'share links', 'default_entries': default_pager_entries }) output_objects.append({ 'object_type': 'sharelinks', 'sharelinks': sharelinks, 'skip_list': skip_list }) output_objects.append({ 'object_type': 'html_form', 'text': '<br/>' }) output_objects.append({ 'object_type': 'sectionheader', 'text': 'Create Share Link' }) submit_button = '''<span> <input type=submit value="Create share link" /> </span>''' sharelink_html = create_share_link_form(configuration, client_id, 'html', submit_button, csrf_token) output_objects.append({ 'object_type': 'html_form', 'text': sharelink_html }) elif action == "edit": header_entry['text'] = 'Edit Share Link' share_dict = share_map.get(share_id, {}) if not share_dict: output_objects.append({ 'object_type': 'error_text', 'text': 'existing share link is required for edit' }) return (output_objects, returnvalues.CLIENT_ERROR) output_objects.append({ 'object_type': 'html_form', 'text': ''' <p> Here you can send invitations for your share link %(share_id)s to one or more comma-separated recipients. </p> ''' % share_dict }) sharelinks = [] share_item = build_sharelinkitem_object(configuration, share_dict) saved_id = share_item['share_id'] js_name = 'delete%s' % hexlify(saved_id) helper = html_post_helper(js_name, '%s.py' % target_op, { 'share_id': saved_id, 'action': 'delete', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) # Hide link to self del share_item['editsharelink'] share_item['delsharelink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really remove %s?' % saved_id), 'class': 'removelink iconspace', 'title': 'Remove share link %s' % saved_id, 'text': '' } sharelinks.append(share_item) output_objects.append({ 'object_type': 'sharelinks', 'sharelinks': sharelinks }) submit_button = '''<span> <input type=submit value="Send invitation(s)" /> </span>''' sharelink_html = invite_share_link_form(configuration, client_id, share_dict, 'html', submit_button, csrf_token) output_objects.append({ 'object_type': 'html_form', 'text': sharelink_html }) output_objects.append({ 'object_type': 'link', 'destination': 'sharelink.py', 'text': 'Return to share link overview' }) return (output_objects, returnvalues.OK) elif action in post_actions: share_dict = share_map.get(share_id, {}) if not share_dict and action != 'create': logger.warning('%s tried to %s missing or not owned link %s!' % (client_id, action, share_id)) output_objects.append({ 'object_type': 'error_text', 'text': '%s requires existing share link' % action }) return (output_objects, returnvalues.CLIENT_ERROR) share_path = share_dict.get('path', path) # Please note that base_dir must end in slash to avoid access to other # user dirs when own name is a prefix of another user name base_dir = os.path.abspath( os.path.join(configuration.user_home, client_dir)) + os.sep rel_share_path = share_path.lstrip(os.sep) # IMPORTANT: path must be expanded to abs for proper chrooting abs_path = os.path.abspath(os.path.join(base_dir, rel_share_path)) relative_path = abs_path.replace(base_dir, '') real_path = os.path.realpath(abs_path) single_file = os.path.isfile(real_path) vgrid_name = in_vgrid_share(configuration, abs_path) if action == 'delete': header_entry['text'] = 'Delete Share Link' (save_status, _) = delete_share_link(share_id, client_id, configuration, share_map) if save_status and vgrid_name: logger.debug("del vgrid sharelink pointer %s" % share_id) (del_status, del_msg) = vgrid_remove_sharelinks(configuration, vgrid_name, [share_id], 'share_id') if not del_status: logger.error("del vgrid sharelink pointer %s failed: %s" % (share_id, del_msg)) return (False, share_map) desc = "delete" elif action == "update": header_entry['text'] = 'Update Share Link' # Try to point replies to client_id email client_email = extract_field(client_id, 'email') if invite_list: invites = share_dict.get('invites', []) + invite_list invites_uniq = list(set([i for i in invites if i])) invites_uniq.sort() share_dict['invites'] = invites_uniq auto_msg = invite_share_link_message(configuration, client_id, share_dict, 'html') msg = '\n'.join(invite_msg) # Now send request to all targets in turn threads = [] for target in invite_list: job_dict = { 'NOTIFY': [target.strip()], 'JOB_ID': 'NOJOBID', 'USER_CERT': client_id, 'EMAIL_SENDER': client_email } logger.debug('invite %s to %s' % (target, share_id)) threads.append( notify_user_thread( job_dict, [auto_msg, msg], 'INVITESHARE', logger, '', configuration, )) # Try finishing delivery but do not block forever on one message notify_done = [False for _ in threads] for _ in range(3): for i in range(len(invite_list)): if not notify_done[i]: logger.debug('check done %s' % invite_list[i]) notify = threads[i] notify.join(3) notify_done[i] = not notify.isAlive() notify_sent, notify_failed = [], [] for i in range(len(invite_list)): if notify_done[i]: notify_sent.append(invite_list[i]) else: notify_failed.append(invite_list[i]) logger.debug('notify sent %s, failed %s' % (notify_sent, notify_failed)) if notify_failed: output_objects.append({ 'object_type': 'html_form', 'text': ''' <p>Failed to send invitation to %s</p>''' % ', '.join(notify_failed) }) if notify_sent: output_objects.append({ 'object_type': 'html_form', 'text': '''<p>Invitation sent to %s</p> <textarea class="fillwidth padspace" rows="%d" readonly="readonly"> %s %s </textarea> ''' % (', '.join(notify_sent), (auto_msg + msg).count('\n') + 3, auto_msg, msg) }) if expire: share_dict['expire'] = expire (save_status, _) = update_share_link(share_dict, client_id, configuration, share_map) desc = "update" elif action == "create": header_entry['text'] = 'Create Share Link' if not read_access and not write_access: output_objects.append({ 'object_type': 'error_text', 'text': 'No access set - please select read, write or both' }) return (output_objects, returnvalues.CLIENT_ERROR) # NOTE: check path here as relative_path is empty for path='/' if not path: output_objects.append({ 'object_type': 'error_text', 'text': 'No path provided!' }) return (output_objects, returnvalues.CLIENT_ERROR) # We refuse sharing of entire home for security reasons elif not valid_user_path( configuration, abs_path, base_dir, allow_equal=False): logger.warning('%s tried to %s restricted path %s ! (%s)' % (client_id, action, abs_path, path)) output_objects.append({ 'object_type': 'error_text', 'text': '''Illegal path "%s": you can only share your own data, and not your entire home direcory.''' % path }) return (output_objects, returnvalues.CLIENT_ERROR) elif not os.path.exists(abs_path): output_objects.append({ 'object_type': 'error_text', 'text': 'Provided path "%s" does not exist!' % path }) return (output_objects, returnvalues.CLIENT_ERROR) # Refuse sharing of (mainly auth) dot dirs in root of user home elif real_path.startswith(os.path.join(base_dir, '.')): output_objects.append({ 'object_type': 'error_text', 'text': 'Provided path "%s" cannot be shared for security reasons' % path }) return (output_objects, returnvalues.CLIENT_ERROR) elif single_file and write_access: output_objects.append({ 'object_type': 'error_text', 'text': '''Individual files cannot be shared with write access - please share a directory with the file in it or only share with read access. ''' }) return (output_objects, returnvalues.CLIENT_ERROR) # We check if abs_path is in vgrid share, but do not worry about # private_base or public_base since they are only available to # owners, who can always share anyway. if vgrid_name is not None and \ not vgrid_is_owner(vgrid_name, client_id, configuration): # share is inside vgrid share so we must check that user is # permitted to create sharelinks there. (load_status, settings_dict) = vgrid_settings(vgrid_name, configuration, recursive=True, as_dict=True) if not load_status: # Probably owners just never saved settings, use defaults settings_dict = {'vgrid_name': vgrid_name} allowed = settings_dict.get('create_sharelink', keyword_owners) if allowed != keyword_members: output_objects.append({ 'object_type': 'error_text', 'text': '''The settings for the %(vgrid_name)s %(vgrid_label)s do not permit you to re-share %(vgrid_label)s shared folders. Please contact the %(vgrid_name)s owners if you think you should be allowed to do that. ''' % { 'vgrid_name': vgrid_name, 'vgrid_label': configuration.site_vgrid_label } }) return (output_objects, returnvalues.CLIENT_ERROR) access_list = [] if read_access: access_list.append('read') if write_access: access_list.append('write') share_mode = '-'.join((access_list + ['only'])[:2]) # TODO: more validity checks here if share_dict: desc = "update" else: desc = "create" # IMPORTANT: always use expanded path share_dict.update({ 'path': relative_path, 'access': access_list, 'expire': expire, 'invites': invite_list, 'single_file': single_file }) attempts = 1 generate_share_id = False if not share_id: attempts = 3 generate_share_id = True for i in range(attempts): if generate_share_id: share_id = generate_sharelink_id(configuration, share_mode) share_dict['share_id'] = share_id (save_status, save_msg) = create_share_link(share_dict, client_id, configuration, share_map) if save_status: logger.info('created sharelink: %s' % share_dict) break else: # ID Collision? logger.warning('could not create sharelink: %s' % save_msg) if save_status and vgrid_name: logger.debug("add vgrid sharelink pointer %s" % share_id) (add_status, add_msg) = vgrid_add_sharelinks(configuration, vgrid_name, [share_dict]) if not add_status: logger.error( "save vgrid sharelink pointer %s failed: %s " % (share_id, add_msg)) return (False, share_map) else: output_objects.append({ 'object_type': 'error_text', 'text': 'No such action %s' % action }) return (output_objects, returnvalues.CLIENT_ERROR) if not save_status: output_objects.append({ 'object_type': 'error_text', 'text': 'Error in %s share link %s: ' % (desc, share_id) + 'save updated share links failed!' }) return (output_objects, returnvalues.CLIENT_ERROR) output_objects.append({ 'object_type': 'text', 'text': '%sd share link %s on %s .' % (desc.title(), share_id, relative_path) }) if action in ['create', 'update']: sharelinks = [] share_item = build_sharelinkitem_object(configuration, share_dict) saved_id = share_item['share_id'] js_name = 'delete%s' % hexlify(saved_id) helper = html_post_helper(js_name, '%s.py' % target_op, { 'share_id': saved_id, 'action': 'delete', csrf_field: csrf_token }) output_objects.append({'object_type': 'html_form', 'text': helper}) share_item['delsharelink'] = { 'object_type': 'link', 'destination': "javascript: confirmDialog(%s, '%s');" % (js_name, 'Really remove %s?' % saved_id), 'class': 'removelink iconspace', 'title': 'Remove share link %s' % saved_id, 'text': '' } sharelinks.append(share_item) output_objects.append({ 'object_type': 'sharelinks', 'sharelinks': sharelinks }) if action == 'create': # NOTE: Leave editsharelink here for use in fileman overlay #del share_item['editsharelink'] output_objects.append({ 'object_type': 'html_form', 'text': '<br />' }) submit_button = '''<span> <input type=submit value="Send invitation(s)" /> </span>''' invite_html = invite_share_link_form(configuration, client_id, share_dict, 'html', submit_button, csrf_token) output_objects.append({ 'object_type': 'html_form', 'text': invite_html }) else: output_objects.append({ 'object_type': 'error_text', 'text': 'Invalid share link action: %s' % action }) return (output_objects, returnvalues.CLIENT_ERROR) output_objects.append({'object_type': 'html_form', 'text': '<br />'}) output_objects.append({ 'object_type': 'link', 'destination': 'sharelink.py', 'text': 'Return to share link overview' }) return (output_objects, returnvalues.OK)