Пример #1
0
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
Пример #2
0
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)
Пример #3
0
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
Пример #4
0
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
Пример #5
0
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)
Пример #6
0
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)
Пример #7
0
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)
Пример #8
0
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("&nbsp;<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)
Пример #9
0
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)
Пример #10
0
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)
Пример #11
0
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)