Example #1
0
 def post(self):
     folder_path = request.values.get('folderPath',
                                      request.values.get('folder_path', ''))
     if not folder_path:
         abort(400)
     if not folder_path.startswith('/'):
         abort(400)
     folder = find_resource(folder_path)
     if not folder:
         abort(404)
     if access_level(folder.query_permissions()) < ACCESS_LEVEL_WRITE:
         abort(403)
     if not request.authorization:
         abort(403)
     key = find_key(request.authorization.password)
     if not key:
         abort(403)
     message_type = request.values['type']
     parameters = json.loads(request.values['parameters'])
     sender_controller_id = key.access_as_controller_id  # None if access as user
     sender_user_id = key.access_as_user_id  # None if access as controller
     message_queue.add(folder.id,
                       None,
                       message_type,
                       parameters,
                       sender_controller_id=sender_controller_id,
                       sender_user_id=sender_user_id)
     return {'status': 'ok'}
Example #2
0
 def post(self):
     folder_path = request.values.get('folderPath',
                                      request.values.get('folder_path', ''))
     if not folder_path:
         abort(400)
     folder = find_resource(folder_path)
     if not folder:
         abort(404)
     if access_level(folder.query_permissions()) < ACCESS_LEVEL_WRITE:
         abort(403)
     type = request.values['type']
     parameters = json.loads(request.values['parameters'])
     auth_code = request.values.get('authCode',
                                    '')  # fix(soon): migrate away from this
     if auth_code:
         key = find_key_by_code(
             auth_code
         )  # fix(faster): we already looked up key in access_level function
     elif request.authorization:
         key = find_key(request.authorization.password)
     if not key:
         abort(403)
     sender_controller_id = key.access_as_controller_id  # None if access as user
     sender_user_id = key.access_as_user_id  # None if access as controller
     message_queue.add(folder.id,
                       type,
                       parameters,
                       sender_controller_id=sender_controller_id,
                       sender_user_id=sender_user_id)
     return {'status': 'ok'}
Example #3
0
def manage_web_socket(ws):
    ws_conn = WebSocketConnection(ws)

    # handle key-based authentication
    if request.authorization:
        logging.debug('ws connect with auth')
        auth = request.authorization
        key = find_key(
            auth.password)  # key is provided as HTTP basic auth password
        if not key:
            logging.debug('key not found')
            return  # would be nice to abort(403), but doesn't look like you can do that inside a websocket handler
        ws_conn.controller_id = key.access_as_controller_id
        ws_conn.user_id = key.access_as_user_id
        ws_conn.auth_method = 'key'
        if ws_conn.controller_id:
            try:
                controller_status = ControllerStatus.query.filter(
                    ControllerStatus.id == ws_conn.controller_id).one()
                controller_status.last_connect_timestamp = datetime.datetime.utcnow(
                )
                controller_status.client_version = auth.username  # client should pass version in HTTP basic auth user name
                db.session.commit()
            except NoResultFound:
                logging.debug(
                    'warning: unable to find controller status record')

    # handle regular user authentication
    elif current_user.is_authenticated:
        logging.debug('ws connect with web browser session')
        ws_conn.user_id = current_user.id
        ws_conn.auth_method = 'user'

    # register this socket to receive outgoing messages
    socket_sender.register(ws_conn)

    # process incoming messages
    while not ws_conn.ws.closed:
        message = ws_conn.ws.receive()
        if message:
            try:
                message_struct = json.loads(message)
            except json.JSONDecodeError:
                break  # if client sends bad message; close this connection
            process_web_socket_message(message_struct, ws_conn)
        time.sleep(0.05)  # sleep to let other stuff run

    # websocket has been closed
    ws_conn.log_disconnect()
    socket_sender.unregister(ws_conn)
    db.session.close()
Example #4
0
    def post(self):

        # create a key for a controller
        if 'access_as_controller_id' in request.values:
            try:
                access_as_controller_id = int(request.values['access_as_controller_id'])
            except ValueError:
                abort(400)
            try:
                r = Resource.query.filter(Resource.id == access_as_controller_id, not_(Resource.deleted)).one()
            except NoResultFound:
                abort(400)
            if access_level(r.query_permissions()) < ACCESS_LEVEL_WRITE:  # require write access to the controller to create a key for it
                abort(403)
            organization_id = r.root().id
            if current_user.is_anonymous:
                # handle special case of creating a key using a user-associated key (controllers aren't allowed to create keys)
                key = find_key(request.authorization.password)
                if key.access_as_user_id:
                    creation_user_id = key.access_as_user_id
                else:
                    creation_user_id = None
                    abort(403)
            else:
                creation_user_id = current_user.id
            (k, key_text) = create_key(creation_user_id, organization_id, None, access_as_controller_id, key_text=request.values.get('key'))
            return {'status': 'ok', 'id': k.id, 'secret_key': key_text}

        # create a key for a users
        # fix(later): handle case of creating a user-associated key using a user-associated key (see code above)
        elif 'access_as_user_id' in request.values:
            access_as_user_id = request.values['access_as_user_id']
            try:
                User.query.filter(User.id == access_as_user_id, not_(User.deleted)).one()
            except NoResultFound:
                abort(400)
            organization_id = request.values['organization_id']
            # fix(soon): instead check that (1) access_as_user_id is a member of org and (2) current user has admin access to org;
            # current check is too strict
            if current_user.is_anonymous or current_user.role != current_user.SYSTEM_ADMIN:
                abort(403)
            (k, key_text) = create_key(current_user.id, organization_id, access_as_user_id, None)
            return {'status': 'ok', 'id': k.id, 'secret_key': key_text}
Example #5
0
def _bootstrap_controller_auth(app_config: Dict[str, str]):
    """Create an organization, controller folder, and API key."""
    org_name = app_config.get('TERRAWARE_ORGANIZATION_NAME')
    org_folder = app_config.get('TERRAWARE_ORGANIZATION_FOLDER')
    folder_name = app_config.get('TERRAWARE_CONTROLLER_FOLDER')
    secret_key = app_config.get('TERRAWARE_CONTROLLER_SECRET_KEY')

    if org_name and org_folder and folder_name and secret_key:
        org_resource = Resource.query.filter(
            Resource.name == org_folder,
            Resource.type == Resource.ORGANIZATION_FOLDER).one_or_none()
        if org_resource is not None:
            org_id = org_resource.id
        else:
            logger.info('Creating organization %s', org_name)
            org_id = create_organization(org_name, org_folder)

        controller_resource = Resource.query.filter(
            Resource.name == folder_name,
            Resource.type == Resource.CONTROLLER_FOLDER).one_or_none()
        if controller_resource is None:
            logger.info('Creating controller folder %s', folder_name)
            controller_resource = Resource(name=folder_name,
                                           type=Resource.CONTROLLER_FOLDER,
                                           parent_id=org_id)
            db.session.add(controller_resource)

        key_resource = find_key(secret_key)
        if key_resource is None:
            logger.info('Creating secret key for controller')
            admin_user_id = db.session.query(User.id).order_by(
                User.id).limit(1).scalar()
            (key_resource, _) = create_key(admin_user_id, org_id, None,
                                           controller_resource.id, secret_key)

        db.session.commit()
Example #6
0
def access_level(permissions, controller_id=None):

    # determine current user (if any)
    # fix(soon): handle API auth with key as user if not handled elsewhere
    user_id = current_user.id if current_user.is_authenticated else None

    # handle system admin
    # fix(soon): require that system admins explicitly add themselves to orgs
    if user_id and current_user.role == current_user.SYSTEM_ADMIN:
        return ACCESS_LEVEL_WRITE

    # determine current API client (if any)
    if not controller_id:
        key = None
        if 'authCode' in request.values:  # fix(soon): remove this case
            auth_code = request.values['authCode']
            key = find_key_by_code(auth_code)
        elif request.authorization:
            auth = request.authorization
            key = find_key(
                auth.password)  # key is provided as HTTP basic auth password
        if key:
            if key.access_as_controller_id:
                controller_id = key.access_as_controller_id
            elif key.access_as_user_id:
                user_id = key.access_as_user_id

    # start with no access
    client_access_level = ACCESS_LEVEL_NONE

    # take max level of all applicable permissions
    for permission_record in permissions:
        (type, id, level) = permission_record

        # applies to everyone
        if type == ACCESS_TYPE_PUBLIC:
            client_access_level = max(client_access_level, level)

        # applies if current user is contained within the organization given by the permission ID
        elif type == ACCESS_TYPE_ORG_USERS:
            if user_id:
                try:
                    org_user = OrganizationUser.query.filter(
                        OrganizationUser.user_id == user_id,
                        OrganizationUser.organization_id == id).one()
                    client_access_level = max(client_access_level, level)
                    break
                except NoResultFound:
                    pass

        # applies if current controller is contained within the organization given by the permission ID
        elif type == ACCESS_TYPE_ORG_CONTROLLERS:
            if controller_id:
                try:
                    controller = Resource.query.filter(
                        Resource.id == controller_id,
                        Resource.deleted == False).one()
                    controller_org_id = controller.organization_id if controller.organization_id else controller.root(
                    ).id  # fix(soon): remove this after all resources have org ids
                    if controller_org_id == id:
                        client_access_level = max(client_access_level, level)
                    break
                except NoResultFound:
                    pass

        # applies if permission ID is the same as current user ID
        elif type == ACCESS_TYPE_USER:
            if user_id and user_id == id:
                client_access_level = max(client_access_level, level)

        # applies if permission ID is the same as current controller ID
        elif type == ACCESS_TYPE_CONTROLLER:
            if controller_id and controller_id == id:
                client_access_level = max(client_access_level, level)

    return client_access_level
Example #7
0
    def get(self, resource_path):
        args = request.values
        result = {}

        # handle case of controller requesting about self
        if resource_path == 'self':
            if 'authCode' in request.values:
                auth_code = request.values.get('authCode', '')  # fix(soon): remove auth codes
                key = find_key_by_code(auth_code)
            elif request.authorization:
                key = find_key(request.authorization.password)
            else:
                key = None
            if key and key.access_as_controller_id:
                try:
                    r = Resource.query.filter(Resource.id == key.access_as_controller_id).one()
                except NoResultFound:
                    abort(404)
            else:
                abort(403)

        # look up the resource record
        else:
            r = find_resource('/' + resource_path)
            if not r:
                abort(404)  # fix(later): revisit to avoid leaking file existance
            if access_level(r.query_permissions()) < ACCESS_LEVEL_READ:
                abort(403)

        # if request meta-data
        if request.values.get('meta', False):
            result = r.as_dict(extended = True)
            if request.values.get('include_path', False):
                result['path'] = r.path()

        # if request data
        else:

            # if folder, return contents list or zip of collection of files
            if r.type >= 10 and r.type < 20:

                # multi-file download
                if 'ids' in args and args.get('download', False):
                    ids = args['ids'].split(',')
                    return batch_download(r, ids)

                # contents list
                else:
                    recursive = request.values.get('recursive', False)
                    type_name = request.values.get('type', None)
                    if type_name:
                        type = resource_type_number(type_name)
                    else:
                        type = None
                    filter = request.values.get('filter', None)
                    extended = request.values.get('extended', False)
                    result = resource_list(r.id, recursive, type, filter, extended)

            # if sequence, return value(s)
            # fix(later): merge with file case?
            elif r.type == Resource.SEQUENCE:

                # get parameters
                text = request.values.get('text', '')
                download = request.values.get('download', False)
                count = int(request.values.get('count', 1))
                start_timestamp = request.values.get('start_timestamp', '')
                end_timestamp = request.values.get('end_timestamp', '')
                if start_timestamp:
                    try:
                        start_timestamp = parse_json_datetime(start_timestamp)
                    except:
                        abort(400, 'Invalid date/time.')
                if end_timestamp:
                    try:
                        end_timestamp = parse_json_datetime(end_timestamp)
                    except:
                        abort(400, 'Invalid date/time.')

                # if filters specified, assume we want a sequence of values
                if text or start_timestamp or end_timestamp or count > 1:

                    # get summary of values
                    if int(request.values.get('summary', False)):
                        return sequence_value_summary(r.id)

                    # get preliminary set of values
                    resource_revisions = ResourceRevision.query.filter(ResourceRevision.resource_id == r.id)

                    # apply filters (if any)
                    if text:
                        resource_revisions = resource_revisions.filter(text in ResourceRevision.data)
                    if start_timestamp:
                        resource_revisions = resource_revisions.filter(ResourceRevision.timestamp >= start_timestamp)
                    if end_timestamp:
                        resource_revisions = resource_revisions.filter(ResourceRevision.timestamp <= end_timestamp)
                    resource_revisions = resource_revisions.order_by('timestamp')
                    if resource_revisions.count() > count:
                        resource_revisions = resource_revisions[-count:]  # fix(later): is there a better/faster way to do this?

                    # return data
                    if download:
                        #timezone = r.root().system_attributes['timezone']  # fix(soon): use this instead of UTC
                        lines = ['utc_timestamp,value\n']
                        for rr in resource_revisions:
                            lines.append('%s,%s\n' % (rr.timestamp.strftime('%Y-%m-%d %H:%M:%S.%f'), rr.data))
                        result = make_response(''.join(lines))
                        result.headers['Content-Type'] = 'application/octet-stream'
                        result.headers['Content-Disposition'] = 'attachment; filename=' + r.name + '.csv'
                        return result
                    else:
                        epoch = datetime.datetime.utcfromtimestamp(0)  # fix(clean): merge with similar code for sequence viewer
                        timestamps = [(rr.timestamp.replace(tzinfo = None) - epoch).total_seconds() for rr in resource_revisions]  # fix(clean): use some sort of unzip function
                        values = [rr.data for rr in resource_revisions]
                        units = json.loads(r.system_attributes).get('units', None)
                        return {'name': r.name, 'units': units, 'timestamps': timestamps, 'values': values}

                # if no filter assume just want current value
                # fix(later): should instead provide all values and have a separate way to get more recent value?
                else:
                    rev = request.values.get('rev')
                    if rev:
                        rev = int(rev)  # fix(soon): save int conversion
                    result = make_response(read_resource(r, revision_id = rev))
                    data_type = json.loads(r.system_attributes)['data_type']
                    if data_type == Resource.IMAGE_SEQUENCE:
                        result.headers['Content-Type'] = 'image/jpeg'
                    else:
                        result.headers['Content-Type'] = 'text/plain'

            # if file, return file data/contents
            else:
                data = read_resource(r)
                if not data:
                    abort(404)
                name = r.name
                if request.values.get('convert_to', request.values.get('convertTo', '')) == 'xls' and r.name.endswith('csv'):
                    data = convert_csv_to_xls(data)
                    name = name.replace('csv', 'xls')
                result = make_response(data)
                result.headers['Content-Type'] = 'application/octet-stream'
                if request.values.get('download', False):
                    result.headers['Content-Disposition'] = 'attachment; filename=' + name
        return result
    def put(self):
        values = json.loads(request.values['values'])
        if 'timestamp' in request.values:
            timestamp = parse_json_datetime(request.values['timestamp'])

            # check for drift
            delta = datetime.datetime.utcnow() - timestamp
            drift = delta.total_seconds()
            # print 'drift', drift
            if abs(drift) > 30:

                # get current controller correction
                # fix(later): support user updates as well?
                auth = request.authorization
                key = find_key(auth.password
                               )  # key is provided as HTTP basic auth password
                if key and key.access_as_controller_id:
                    controller_id = key.access_as_controller_id
                    controller_status = ControllerStatus.query.filter(
                        ControllerStatus.id == controller_id).one()
                    attributes = json.loads(controller_status.attributes)
                    correction = attributes.get('timestamp_correction', 0)

                    # if stored correction is reasonable, use it; otherwise store new correction
                    if abs(correction - drift) > 100:
                        correction = drift
                        attributes['timestamp_correction'] = drift
                        controller_status.attributes = json.dumps(attributes)
                        db.session.commit()
                        # print 'storing new correction (%.2f)' % correction
                    else:
                        pass
                        # print 'applying previous correction (%.2f)' % correction
                    timestamp += datetime.timedelta(seconds=correction)
        else:
            timestamp = datetime.datetime.utcnow()

        # for now, assume all sequences in same folder
        items = list(values.items())
        if items:
            items = sorted(
                items
            )  # sort by keys so we can re-use folder lookup and permission check between items in same folder
            folder_resource = None
            folder_name = None
            for (full_name, value) in items:
                item_folder_name = full_name.rsplit('/', 1)[0]
                if item_folder_name != folder_name:  # if this folder doesn't match the folder resource record we have
                    folder_name = item_folder_name
                    folder_resource = find_resource(folder_name)
                    if folder_resource and access_level(
                            folder_resource.query_permissions(
                            )) < ACCESS_LEVEL_WRITE:
                        folder_resource = None  # don't have write access
                if folder_resource:
                    seq_name = full_name.rsplit('/', 1)[1]
                    try:
                        resource = (Resource.query.filter(
                            Resource.parent_id == folder_resource.id,
                            Resource.name == seq_name,
                            not_(Resource.deleted)).one())
                        update_sequence_value(
                            resource,
                            full_name,
                            timestamp,
                            str(value),
                            emit_message=True
                        )  # fix(later): revisit emit_message
                    except NoResultFound:
                        pass
            db.session.commit()