class CompletedTaskRunAPI(APIBase):

    """
    Class for the domain object TaskRun.

    """

    __class__ = TaskRun

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, oid):
        """Get taskruns for all completed tasks. Need admin access"""
        try:
            ensure_authorized_to('read', self.__class__)
            # check admin access
            if 'api_key' in request.args.keys():
                apikey = request.args['api_key']
                user = user_repo.get_by(api_key=apikey)
                if not user or user.admin is False:
                    raise BadRequest("Insufficient privilege to the request")
            else:
                raise BadRequest("Insufficient privilege to the request")

            # set filter from args
            filters = {}
            for k in request.args.keys():
                if k not in ['limit', 'offset', 'api_key']:
                    # 'exported' column belongs to Task class
                    # ignore it for attr check in TaskRun class
                    # but add it to filter so that its checked
                    # against Task class in filter_completed_task_runs_by
                    if k not in ['exported']:
                        # Raise an error if the k arg is not a column
                        getattr(self.__class__, k)
                    filters[k] = request.args[k]

            # set limit, offset
            limit, offset, orderby = self._set_limit_and_offset()
            # query database to obtain the requested data
            query = task_repo.filter_completed_task_runs_by(limit=limit, offset=offset, **filters)
            json_response = self._create_json_response(query, oid)
            return Response(json_response, mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e,
                target=self.__class__.__name__.lower(),
            action='GET')

    def post(self):
        raise MethodNotAllowed(valid_methods=['GET'])

    def delete(self, oid=None):
        raise MethodNotAllowed(valid_methods=['GET'])

    def put(self, oid=None):
        raise MethodNotAllowed(valid_methods=['GET'])
예제 #2
0
파일: vmcp.py 프로젝트: pkdevbox/pybossa
class VmcpAPI(APIBase):
    """Class for CernVM plugin api.

    Returns signed object to start a CernVM instance.

    """
    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, oid=None):
        """Return signed VMCP for CernVM requests."""
        if current_app.config.get('VMCP_KEY') is None:
            message = "The server is not configured properly, contact the admins"
            error = self._format_error(status_code=501, message=message)
            return Response(json.dumps(error),
                            status=error['status_code'],
                            mimetype='application/json')

        pkey = (current_app.root_path + '/../keys/' +
                current_app.config.get('VMCP_KEY'))
        if not os.path.exists(pkey):
            message = "The server is not configured properly (private key is missing), contact the admins"
            error = self._format_error(status_code=501, message=message)
            return Response(json.dumps(error),
                            status=error['status_code'],
                            mimetype='application/json')

        if request.args.get('cvm_salt') is None:
            message = "cvm_salt parameter is missing"
            error = self._format_error(status_code=415, message=message)
            return Response(json.dumps(error),
                            status=error['status_code'],
                            mimetype='application/json')

        salt = request.args.get('cvm_salt')
        data = request.args.copy()
        signed_data = pybossa.vmcp.sign(data, salt, pkey)
        return Response(json.dumps(signed_data),
                        200,
                        mimetype='application/json')

    def _format_error(self, status_code=None, message=None):
        return dict(action=request.method,
                    status="failed",
                    status_code=status_code,
                    target='vmcp',
                    exception_cls='vmcp',
                    exception_msg=message)

    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def post(self):
        raise MethodNotAllowed
예제 #3
0
class CompletedTaskRunAPI(APIBase):
    """
    Class for the domain object TaskRun.

    """

    __class__ = TaskRun

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, oid):
        """Get taskruns for all completed tasks and gold tasks. Need admin access"""
        try:
            if not (current_user.is_authenticated() and current_user.admin):
                raise Unauthorized("Insufficient privilege to the request")

            # set filter from args
            filters = {}
            for k in request.args.keys():
                if k not in ['limit', 'offset', 'api_key', 'last_id']:
                    # 'exported' column belongs to Task class
                    # ignore it for attr check in TaskRun class
                    # but add it to filter so that its checked
                    # against Task class in filter_completed_taskruns_gold_taskruns_by
                    if k not in ['exported']:
                        # Raise an error if the k arg is not a column
                        getattr(self.__class__, k)
                    filters[k] = request.args[k]

            # set limit, offset
            limit, offset, orderby = self._set_limit_and_offset()
            last_id = request.args.get('last_id', 0)
            results = task_repo.filter_completed_taskruns_gold_taskruns_by(
                limit=limit, offset=offset, last_id=last_id, **filters)
            json_response = self._create_json_response(results, oid)
            return Response(json_response, mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='GET')

    def post(self):
        raise MethodNotAllowed(valid_methods=['GET'])

    def delete(self, oid=None):
        raise MethodNotAllowed(valid_methods=['GET'])

    def put(self, oid=None):
        raise MethodNotAllowed(valid_methods=['GET'])
예제 #4
0
class TaskAPI(APIBase):
    """Class for domain object Task."""

    __class__ = Task
    reserved_keys = set(['id', 'created', 'state'])

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, oid):
        """Get an object.
        Returns an item from the DB with the request.data JSON object or all
        the items if oid == None
        :arg self: The class of the object to be retrieved
        :arg integer oid: the ID of the object in the DB
        :returns: The JSON item/s stored in the DB
        """
        try:
            ensure_authorized_to('read', self.__class__)
            query = self._db_query(oid)
            json_response = self._create_json_response(query, oid)
            check_user = current_user.is_authenticated()
            data = json.loads(json_response)
            if check_user == True:
                task_run = task_repo.get_task_run_by(
                    project_id=data['project_id'],
                    task_id=data['id'],
                    user=current_user)
            else:
                task_run = task_repo.get_task_run_by(
                    project_id=data['project_id'],
                    task_id=data['id'],
                    user_ip=request.remote_addr)
            data['info']['processed'] = True if task_run else False
            json_response = json.dumps(data)

            return Response(json_response, mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='GET')

    def _forbidden_attributes(self, data):
        for key in data.keys():
            if key in self.reserved_keys:
                raise BadRequest("Reserved keys in payload")
예제 #5
0
from category import CategoryAPI
from vmcp import VmcpAPI
from user import UserAPI
from token import TokenAPI
from pybossa.core import project_repo, task_repo

blueprint = Blueprint('api', __name__)

cors_headers = ['Content-Type', 'Authorization']

error = ErrorStatus()


@blueprint.route('/')
@crossdomain(origin='*', headers=cors_headers)
@ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
def index():  # pragma: no cover
    """Return dummy text for welcome page."""
    return 'The PyBossa API'


def register_api(view, endpoint, url, pk='id', pk_type='int'):
    """Register API endpoints.

    Registers new end points for the API using classes.

    """
    view_func = view.as_view(endpoint)
    csrf.exempt(view_func)
    blueprint.add_url_rule(url,
                           view_func=view_func,
예제 #6
0
from completed_task_run import CompletedTaskRunAPI
from pybossa.cache.helpers import n_available_tasks, n_available_tasks_for_user
from pybossa.sched import (get_project_scheduler_and_timeout,
                           get_scheduler_and_timeout, has_lock, release_lock,
                           Schedulers, get_locks)
from pybossa.api.project_by_name import ProjectByNameAPI
from pybossa.api.pwd_manager import get_pwd_manager
from pybossa.data_access import data_access_levels

blueprint = Blueprint('api', __name__)

error = ErrorStatus()


@blueprint.route('/')
@ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
def index():  # pragma: no cover
    """Return dummy text for welcome page."""
    return 'The %s API' % current_app.config.get('BRAND')


@blueprint.before_request
def _api_authentication_with_api_key():
    """ Allow API access with valid api_key."""
    secure_app_access = current_app.config.get('SECURE_APP_ACCESS', False)
    if secure_app_access:
        grant_access_with_api_key(secure_app_access)


def register_api(view, endpoint, url, pk='id', pk_type='int'):
    """Register API endpoints.
예제 #7
0
class APIBase(MethodView):
    """Class to create CRUD methods."""

    hateoas = Hateoas()

    allowed_classes_upload = ['blogpost', 'helpingmaterial', 'announcement']

    def refresh_cache(self, cls_name, oid):
        """Refresh the cache."""
        if caching.get(cls_name):
            caching.get(cls_name)['refresh'](oid)

    def valid_args(self):
        """Check if the domain object args are valid."""
        for k in request.args.keys():
            if k not in ['api_key']:
                getattr(self.__class__, k)

    def options(self, **kwargs):  # pragma: no cover
        """Return '' for Options method."""
        return ''

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, oid):
        """Get an object.

        Returns an item from the DB with the request.data JSON object or all
        the items if oid == None

        :arg self: The class of the object to be retrieved
        :arg integer oid: the ID of the object in the DB
        :returns: The JSON item/s stored in the DB

        """
        try:
            ensure_authorized_to('read', self.__class__)
            query = self._db_query(oid)
            json_response = self._create_json_response(query, oid)
            return Response(json_response, mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='GET')

    def _create_json_response(self, query_result, oid):
        if len(query_result) == 1 and query_result[0] is None:
            raise abort(404)
        items = []
        for result in query_result:
            # This is for n_favs orderby case
            if not isinstance(result, DomainObject):
                if 'n_favs' in result.keys():
                    result = result[0]
            try:
                if (result.__class__ != self.__class__):
                    (item, headline, rank) = result
                else:
                    item = result
                    headline = None
                    rank = None
                datum = self._create_dict_from_model(item)
                if headline:
                    datum['headline'] = headline
                if rank:
                    datum['rank'] = rank
                ensure_authorized_to('read', item)
                items.append(datum)
            except (Forbidden, Unauthorized):
                # Remove last added item, as it is 401 or 403
                if len(items) > 0:
                    items.pop()
            except Exception:  # pragma: no cover
                raise
        if oid is not None:
            ensure_authorized_to('read', query_result[0])
            items = items[0]
        return json.dumps(items)

    def _create_dict_from_model(self, model):
        return self._select_attributes(self._add_hateoas_links(model))

    def _add_hateoas_links(self, item):
        obj = item.dictize()
        related = request.args.get('related')
        if related:
            if item.__class__.__name__ == 'Task':
                obj['task_runs'] = []
                obj['result'] = None
                task_runs = task_repo.filter_task_runs_by(task_id=item.id)
                results = result_repo.filter_by(task_id=item.id,
                                                last_version=True)
                for tr in task_runs:
                    obj['task_runs'].append(tr.dictize())
                for r in results:
                    obj['result'] = r.dictize()

            if item.__class__.__name__ == 'TaskRun':
                tasks = task_repo.filter_tasks_by(id=item.task_id)
                results = result_repo.filter_by(task_id=item.task_id,
                                                last_version=True)
                obj['task'] = None
                obj['result'] = None
                for t in tasks:
                    obj['task'] = t.dictize()
                for r in results:
                    obj['result'] = r.dictize()

            if item.__class__.__name__ == 'Result':
                tasks = task_repo.filter_tasks_by(id=item.task_id)
                task_runs = task_repo.filter_task_runs_by(task_id=item.task_id)
                obj['task_runs'] = []
                for t in tasks:
                    obj['task'] = t.dictize()
                for tr in task_runs:
                    obj['task_runs'].append(tr.dictize())

        links, link = self.hateoas.create_links(item)
        if links:
            obj['links'] = links
        if link:
            obj['link'] = link
        return obj

    def _db_query(self, oid):
        """Returns a list with the results of the query"""
        repo_info = repos[self.__class__.__name__]
        if oid is None:
            limit, offset, orderby = self._set_limit_and_offset()
            results = self._filter_query(repo_info, limit, offset, orderby)
        else:
            repo = repo_info['repo']
            query_func = repo_info['get']
            results = [getattr(repo, query_func)(oid)]
        return results

    def api_context(self, all_arg, **filters):
        if current_user.is_authenticated():
            filters['owner_id'] = current_user.id
        if filters.get('owner_id') and all_arg == '1':
            del filters['owner_id']
        return filters

    def _filter_query(self, repo_info, limit, offset, orderby):
        filters = {}
        for k in request.args.keys():
            if k not in [
                    'limit', 'offset', 'api_key', 'last_id', 'all',
                    'fulltextsearch', 'desc', 'orderby', 'related',
                    'participated', 'full'
            ]:
                # Raise an error if the k arg is not a column
                if self.__class__ == Task and k == 'external_uid':
                    pass
                else:
                    getattr(self.__class__, k)
                filters[k] = request.args[k]
        repo = repo_info['repo']
        filters = self.api_context(all_arg=request.args.get('all'), **filters)
        query_func = repo_info['filter']
        filters = self._custom_filter(filters)
        last_id = request.args.get('last_id')
        if request.args.get('participated'):
            filters['participated'] = get_user_id_or_ip()
        fulltextsearch = request.args.get('fulltextsearch')
        desc = request.args.get('desc') if request.args.get('desc') else False
        desc = fuzzyboolean(desc)
        if last_id:
            results = getattr(repo, query_func)(limit=limit,
                                                last_id=last_id,
                                                fulltextsearch=fulltextsearch,
                                                desc=False,
                                                orderby=orderby,
                                                **filters)
        else:
            results = getattr(repo, query_func)(limit=limit,
                                                offset=offset,
                                                fulltextsearch=fulltextsearch,
                                                desc=desc,
                                                orderby=orderby,
                                                **filters)
        return results

    def _set_limit_and_offset(self):
        try:
            limit = min(100, int(request.args.get('limit')))
        except (ValueError, TypeError):
            limit = 20
        try:
            offset = int(request.args.get('offset'))
        except (ValueError, TypeError):
            offset = 0
        try:
            orderby = request.args.get('orderby') if request.args.get(
                'orderby') else 'id'
        except (ValueError, TypeError):
            orderby = 'updated'
        return limit, offset, orderby

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def post(self):
        """Post an item to the DB with the request.data JSON object.

        :arg self: The class of the object to be inserted
        :returns: The JSON item stored in the DB

        """
        try:
            cls_name = self.__class__.__name__
            self.valid_args()
            data = self._file_upload(request)
            if data is None:
                data = json.loads(request.data)
            self._forbidden_attributes(data)
            inst = self._create_instance_from_request(data)
            repo = repos[self.__class__.__name__]['repo']
            save_func = repos[self.__class__.__name__]['save']
            getattr(repo, save_func)(inst)
            self._log_changes(None, inst)
            self.refresh_cache(cls_name, inst.id)
            return json.dumps(inst.dictize())
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='POST')

    def _create_instance_from_request(self, data):
        data = self.hateoas.remove_links(data)
        inst = self.__class__(**data)
        self._update_object(inst)
        ensure_authorized_to('create', inst)
        self._validate_instance(inst)
        return inst

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def delete(self, oid):
        """Delete a single item from the DB.

        :arg self: The class of the object to be deleted
        :arg integer oid: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7>`_.

        """
        try:
            self.valid_args()
            self._delete_instance(oid)
            cls_name = self.__class__.__name__
            self.refresh_cache(cls_name, oid)
            return '', 204
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='DELETE')

    def _delete_instance(self, oid):
        repo = repos[self.__class__.__name__]['repo']
        query_func = repos[self.__class__.__name__]['get']
        inst = getattr(repo, query_func)(oid)
        if inst is None:
            raise NotFound
        ensure_authorized_to('delete', inst)
        self._file_delete(request, inst)
        self._log_changes(inst, None)
        delete_func = repos[self.__class__.__name__]['delete']
        getattr(repo, delete_func)(inst)
        return inst

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def put(self, oid):
        """Update a single item in the DB.

        :arg self: The class of the object to be updated
        :arg integer oid: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6>`_.

        """
        try:
            self.valid_args()
            cls_name = self.__class__.__name__
            repo = repos[cls_name]['repo']
            query_func = repos[cls_name]['get']
            existing = getattr(repo, query_func)(oid)
            if existing is None:
                raise NotFound
            ensure_authorized_to('update', existing)
            data = self._file_upload(request)
            inst = self._update_instance(existing,
                                         repo,
                                         repos,
                                         new_upload=data)
            self.refresh_cache(cls_name, oid)
            return Response(json.dumps(inst.dictize()),
                            200,
                            mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='PUT')

    def _update_instance(self, existing, repo, repos, new_upload=None):
        data = dict()
        if new_upload is None:
            data = json.loads(request.data)
            self._forbidden_attributes(data)
            # Remove hateoas links
            data = self.hateoas.remove_links(data)
        else:
            self._forbidden_attributes(request.form)
        # may be missing the id as we allow partial updates
        self.__class__(**data)
        old = self.__class__(**existing.dictize())
        for key in data:
            setattr(existing, key, data[key])
        if new_upload:
            existing.media_url = new_upload['media_url']
            existing.info['container'] = new_upload['info']['container']
            existing.info['file_name'] = new_upload['info']['file_name']
        self._update_attribute(existing, old)
        update_func = repos[self.__class__.__name__]['update']
        self._validate_instance(existing)
        getattr(repo, update_func)(existing)
        self._log_changes(old, existing)
        return existing

    def _update_object(self, data_dict):
        """Update object.

        Method to be overriden in inheriting classes which wish to update
        data dict.

        """
        pass

    def _update_attribute(self, new, old):
        """Update object attribute if new value is passed.
        Method to be overriden in inheriting classes which wish to update
        data dict.

        """

    def _select_attributes(self, item_data):
        """Method to be overriden in inheriting classes in case it is not
        desired that every object attribute is returned by the API.
        """
        return item_data

    def _custom_filter(self, query):
        """Method to be overriden in inheriting classes which wish to consider
        specific filtering criteria.
        """
        return query

    def _validate_instance(self, instance):
        """Method to be overriden in inheriting classes which may need to
        validate the creation (POST) or modification (PUT) of a domain object
        for reasons other than business logic ones (e.g. overlapping of a
        project name witht a URL).
        """
        pass

    def _log_changes(self, old_obj, new_obj):
        """Method to be overriden by inheriting classes for logging purposes"""
        pass

    def _forbidden_attributes(self, data):
        """Method to be overriden by inheriting classes that will not allow for
        certain fields to be used in PUT or POST requests"""
        pass

    def _file_upload(self, data):
        """Method that must be overriden by the class to allow file uploads for
        only a few classes."""
        cls_name = self.__class__.__name__.lower()
        content_type = 'multipart/form-data'
        if (content_type in request.headers.get('Content-Type')
                and cls_name in self.allowed_classes_upload):
            tmp = dict()
            for key in request.form.keys():
                tmp[key] = request.form[key]

            if isinstance(self, announcement.Announcement):
                # don't check project id for announcements
                ensure_authorized_to('create', self)
                upload_method = current_app.config.get('UPLOAD_METHOD')
                if request.files.get('file') is None:
                    raise AttributeError
                _file = request.files['file']
                container = "user_%s" % current_user.id
            else:
                ensure_authorized_to('create',
                                     self.__class__,
                                     project_id=tmp['project_id'])
                project = project_repo.get(tmp['project_id'])
                upload_method = current_app.config.get('UPLOAD_METHOD')
                if request.files.get('file') is None:
                    raise AttributeError
                _file = request.files['file']
                if current_user.admin:
                    container = "user_%s" % project.owner.id
                else:
                    container = "user_%s" % current_user.id
            uploader.upload_file(_file, container=container)
            file_url = get_avatar_url(upload_method, _file.filename, container)
            tmp['media_url'] = file_url
            if tmp.get('info') is None:
                tmp['info'] = dict()
            tmp['info']['container'] = container
            tmp['info']['file_name'] = _file.filename
            return tmp
        else:
            return None

    def _file_delete(self, request, obj):
        """Delete file object."""
        cls_name = self.__class__.__name__.lower()
        if cls_name in self.allowed_classes_upload:
            keys = obj.info.keys()
            if 'file_name' in keys and 'container' in keys:
                ensure_authorized_to('delete', obj)
                uploader.delete_file(obj.info['file_name'],
                                     obj.info['container'])
예제 #8
0
파일: api_base.py 프로젝트: rajzone/pybossa
class APIBase(MethodView):
    """Class to create CRUD methods."""

    hateoas = Hateoas()

    slave_session = get_session(db, bind='slave')

    def valid_args(self):
        """Check if the domain object args are valid."""
        for k in request.args.keys():
            if k not in ['api_key']:
                getattr(self.__class__, k)

    @crossdomain(origin='*', headers=cors_headers)
    def options(self):  # pragma: no cover
        """Return '' for Options method."""
        return ''

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, id):
        """Get an object.

        Returns an item from the DB with the request.data JSON object or all
        the items if id == None

        :arg self: The class of the object to be retrieved
        :arg integer id: the ID of the object in the DB
        :returns: The JSON item/s stored in the DB

        """
        try:
            getattr(require, self.__class__.__name__.lower()).read()
            query = self._db_query(id)
            json_response = self._create_json_response(query, id)
            return Response(json_response, mimetype='application/json')
        except Exception as e:
            self.slave_session.rollback()
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='GET')
        finally:
            self.slave_session.close()

    def _create_json_response(self, query_result, id):
        if len(query_result) == 1 and query_result[0] is None:
            raise abort(404)
        items = []
        for item in query_result:
            try:
                items.append(self._create_dict_from_model(item))
                getattr(require, self.__class__.__name__.lower()).read(item)
            except (Forbidden, Unauthorized):
                # Remove last added item, as it is 401 or 403
                items.pop()
            except:  # pragma: no cover
                raise
        if id:
            getattr(require,
                    self.__class__.__name__.lower()).read(query_result[0])
            items = items[0]
        return json.dumps(items)

    def _create_dict_from_model(self, model):
        return self._select_attributes(self._add_hateoas_links(model))

    def _add_hateoas_links(self, item):
        obj = item.dictize()
        links, link = self.hateoas.create_links(item)
        if links:
            obj['links'] = links
        if link:
            obj['link'] = link
        return obj

    def _db_query(self, id):
        """ Returns a list with the results of the query"""
        query = self.slave_session.query(self.__class__)
        if id is None:
            limit, offset = self._set_limit_and_offset()
            query = self._filter_query(query, limit, offset)
        else:
            query = [query.get(id)]
        return query

    def _filter_query(self, query, limit, offset):
        for k in request.args.keys():
            if k not in ['limit', 'offset', 'api_key']:
                # Raise an error if the k arg is not a column
                getattr(self.__class__, k)
                query = query.filter(
                    getattr(self.__class__, k) == request.args[k])
        query = self._custom_filter(query)
        return self._format_query_result(query, limit, offset)

    def _format_query_result(self, query, limit, offset):
        query = query.order_by(self.__class__.id)
        query = query.limit(limit)
        query = query.offset(offset)
        return query.all()

    def _set_limit_and_offset(self):
        try:
            limit = min(100, int(request.args.get('limit')))
        except (ValueError, TypeError):
            limit = 20
        try:
            offset = int(request.args.get('offset'))
        except (ValueError, TypeError):
            offset = 0
        return limit, offset

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def post(self):
        """Post an item to the DB with the request.data JSON object.

        :arg self: The class of the object to be inserted
        :returns: The JSON item stored in the DB

        """
        try:
            self.valid_args()
            data = json.loads(request.data)
            # Clean HATEOAS args
            inst = self._create_instance_from_request(data)
            db.session.add(inst)
            db.session.commit()
            return json.dumps(inst.dictize())
        except IntegrityError:
            db.session.rollback()
            raise
        except Exception as e:
            db.session.rollback()
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='POST')

    def _create_instance_from_request(self, data):
        data = self.hateoas.remove_links(data)
        inst = self.__class__(**data)
        self._update_object(inst)
        getattr(require, self.__class__.__name__.lower()).create(inst)
        return inst

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def delete(self, id):
        """Delete a single item from the DB.

        :arg self: The class of the object to be deleted
        :arg integer id: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7>`_.

        """
        try:
            self.valid_args()
            inst = self._delete_instance(id)
            self._refresh_cache(inst)
            return '', 204
        except Exception as e:
            db.session.rollback()
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='DELETE')

    def _delete_instance(self, id):
        inst = db.session.query(self.__class__).get(id)
        if inst is None:
            raise NotFound
        getattr(require, self.__class__.__name__.lower()).delete(inst)
        db.session.delete(inst)
        db.session.commit()
        return inst

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def put(self, id):
        """Update a single item in the DB.

        :arg self: The class of the object to be updated
        :arg integer id: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6>`_.

        """
        try:
            self.valid_args()
            inst = self._update_instance(id)
            self._refresh_cache(inst)
            return Response(json.dumps(inst.dictize()),
                            200,
                            mimetype='application/json')
        except IntegrityError:
            db.session.rollback()
            raise
        except Exception as e:
            db.session.rollback()
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='PUT')

    def _update_instance(self, id):
        existing = db.session.query(self.__class__).get(id)
        if existing is None:
            raise NotFound
        getattr(require, self.__class__.__name__.lower()).update(existing)
        data = json.loads(request.data)
        # may be missing the id as we allow partial updates
        data['id'] = id
        # Clean HATEOAS args
        data = self.hateoas.remove_links(data)
        inst = self.__class__(**data)
        db.session.merge(inst)
        db.session.commit()
        return inst

    def _update_object(self, data_dict):
        """Update object.

        Method to be overriden in inheriting classes which wish to update
        data dict.

        """
        pass

    def _refresh_cache(self, data_dict):
        """Refresh cache.

        Method to be overriden in inheriting classes which wish to refresh
        cache for given object.

        """
        pass

    def _select_attributes(self, item_data):
        """Method to be overriden in inheriting classes in case it is not
        desired that every object attribute is returned by the API
        """
        return item_data

    def _custom_filter(self, query):
        """Method to be overriden in inheriting classes which wish to consider
        specific filtering criteria
        """
        return query
예제 #9
0
class APIBase(MethodView):
    """Class to create CRUD methods."""

    hateoas = Hateoas()

    def valid_args(self):
        """Check if the domain object args are valid."""
        for k in request.args.keys():
            if k not in ['api_key']:
                getattr(self.__class__, k)

    @crossdomain(origin='*', headers=cors_headers)
    def options(self):  # pragma: no cover
        """Return '' for Options method."""
        return ''

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, oid):
        """Get an object.

        Returns an item from the DB with the request.data JSON object or all
        the items if oid == None

        :arg self: The class of the object to be retrieved
        :arg integer oid: the ID of the object in the DB
        :returns: The JSON item/s stored in the DB

        """
        try:
            ensure_authorized_to('read', self.__class__)
            query = self._db_query(oid)
            json_response = self._create_json_response(query, oid)
            return Response(json_response, mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='GET')

    def _create_json_response(self, query_result, oid):
        if len(query_result) == 1 and query_result[0] is None:
            raise abort(404)
        items = []
        for item in query_result:
            try:
                items.append(self._create_dict_from_model(item))
                ensure_authorized_to('read', item)
            except (Forbidden, Unauthorized):
                # Remove last added item, as it is 401 or 403
                items.pop()
            except Exception:  # pragma: no cover
                raise
        if oid is not None:
            ensure_authorized_to('read', query_result[0])
            items = items[0]
        return json.dumps(items)

    def _create_dict_from_model(self, model):
        return self._select_attributes(self._add_hateoas_links(model))

    def _add_hateoas_links(self, item):
        obj = item.dictize()
        links, link = self.hateoas.create_links(item)
        if links:
            obj['links'] = links
        if link:
            obj['link'] = link
        return obj

    def _db_query(self, oid):
        """Returns a list with the results of the query"""
        repo_info = repos[self.__class__.__name__]
        if oid is None:
            limit, offset = self._set_limit_and_offset()
            results = self._filter_query(repo_info, limit, offset)
        else:
            repo = repo_info['repo']
            query_func = repo_info['get']
            results = [getattr(repo, query_func)(oid)]
        return results

    def api_context(self, all_arg, **filters):
        if current_user.is_authenticated():
            filters['owner_id'] = current_user.id
        if filters.get('owner_id') and all_arg == '1':
            del filters['owner_id']
        return filters

    def _filter_query(self, repo_info, limit, offset):
        filters = {}
        for k in request.args.keys():
            if k not in [
                    'limit', 'offset', 'api_key', 'last_id', 'all',
                    'fulltextsearch'
            ]:
                # Raise an error if the k arg is not a column
                getattr(self.__class__, k)
                filters[k] = request.args[k]
        repo = repo_info['repo']
        filters = self.api_context(all_arg=request.args.get('all'), **filters)
        query_func = repo_info['filter']
        filters = self._custom_filter(filters)
        last_id = request.args.get('last_id')
        fulltextsearch = request.args.get('fulltextsearch')
        if last_id:
            results = getattr(repo, query_func)(limit=limit,
                                                last_id=last_id,
                                                fulltextsearch=fulltextsearch,
                                                **filters)
        else:
            results = getattr(repo, query_func)(limit=limit,
                                                offset=offset,
                                                fulltextsearch=fulltextsearch,
                                                **filters)
        return results

    def _set_limit_and_offset(self):
        try:
            limit = min(100, int(request.args.get('limit')))
        except (ValueError, TypeError):
            limit = 20
        try:
            offset = int(request.args.get('offset'))
        except (ValueError, TypeError):
            offset = 0
        return limit, offset

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def post(self):
        """Post an item to the DB with the request.data JSON object.

        :arg self: The class of the object to be inserted
        :returns: The JSON item stored in the DB

        """
        try:
            self.valid_args()
            data = json.loads(request.data)
            self._forbidden_attributes(data)
            inst = self._create_instance_from_request(data)
            repo = repos[self.__class__.__name__]['repo']
            save_func = repos[self.__class__.__name__]['save']
            getattr(repo, save_func)(inst)
            self._log_changes(None, inst)
            return json.dumps(inst.dictize())
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='POST')

    def _create_instance_from_request(self, data):
        data = self.hateoas.remove_links(data)
        inst = self.__class__(**data)
        self._update_object(inst)
        ensure_authorized_to('create', inst)
        self._validate_instance(inst)
        return inst

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def delete(self, oid):
        """Delete a single item from the DB.

        :arg self: The class of the object to be deleted
        :arg integer oid: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7>`_.

        """
        try:
            self.valid_args()
            self._delete_instance(oid)
            return '', 204
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='DELETE')

    def _delete_instance(self, oid):
        repo = repos[self.__class__.__name__]['repo']
        query_func = repos[self.__class__.__name__]['get']
        inst = getattr(repo, query_func)(oid)
        if inst is None:
            raise NotFound
        ensure_authorized_to('delete', inst)
        self._log_changes(inst, None)
        delete_func = repos[self.__class__.__name__]['delete']
        getattr(repo, delete_func)(inst)
        return inst

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def put(self, oid):
        """Update a single item in the DB.

        :arg self: The class of the object to be updated
        :arg integer oid: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6>`_.

        """
        try:
            self.valid_args()
            inst = self._update_instance(oid)
            return Response(json.dumps(inst.dictize()),
                            200,
                            mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='PUT')

    def _update_instance(self, oid):
        repo = repos[self.__class__.__name__]['repo']
        query_func = repos[self.__class__.__name__]['get']
        existing = getattr(repo, query_func)(oid)
        if existing is None:
            raise NotFound
        ensure_authorized_to('update', existing)
        data = json.loads(request.data)
        self._forbidden_attributes(data)
        # Remove hateoas links
        data = self.hateoas.remove_links(data)
        # may be missing the id as we allow partial updates
        data['id'] = oid
        self.__class__(**data)
        old = self.__class__(**existing.dictize())
        for key in data:
            setattr(existing, key, data[key])
        self._update_attribute(existing, old)
        update_func = repos[self.__class__.__name__]['update']
        self._validate_instance(existing)
        getattr(repo, update_func)(existing)
        self._log_changes(old, existing)
        return existing

    def _update_object(self, data_dict):
        """Update object.

        Method to be overriden in inheriting classes which wish to update
        data dict.

        """
        pass

    def _update_attribute(self, new, old):
        """Update object attribute if new value is passed.
        Method to be overriden in inheriting classes which wish to update
        data dict.

        """

    def _select_attributes(self, item_data):
        """Method to be overriden in inheriting classes in case it is not
        desired that every object attribute is returned by the API.
        """
        return item_data

    def _custom_filter(self, query):
        """Method to be overriden in inheriting classes which wish to consider
        specific filtering criteria.
        """
        return query

    def _validate_instance(self, instance):
        """Method to be overriden in inheriting classes which may need to
        validate the creation (POST) or modification (PUT) of a domain object
        for reasons other than business logic ones (e.g. overlapping of a
        project name witht a URL).
        """
        pass

    def _log_changes(self, old_obj, new_obj):
        """Method to be overriden by inheriting classes for logging purposes"""
        pass

    def _forbidden_attributes(self, data):
        """Method to be overriden by inheriting classes that will not allow for
        certain fields to be used in PUT or POST requests"""
        pass
예제 #10
0
class FavoritesAPI(APIBase):
    """Class API for Favorites."""

    __class__ = Task

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, oid):
        """Return all the tasks favorited by current user."""
        try:
            if current_user.is_anonymous():
                raise abort(401)
            uid = current_user.id
            tasks = task_repo.filter_tasks_by_user_favorites(uid)
            data = self._create_json_response(tasks, oid)
            return Response(data, 200, mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='GET')

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def post(self):
        """Add User ID to task as a favorite."""
        try:
            self.valid_args()
            data = json.loads(request.data)
            if (len(data.keys()) != 1) or ('task_id' not in data.keys()):
                raise AttributeError
            if current_user.is_anonymous():
                raise Unauthorized
            uid = current_user.id
            tasks = task_repo.get_task_favorited(uid, data['task_id'])
            if len(tasks) == 1:
                task = tasks[0]
            if len(tasks) == 0:
                task = task_repo.get_task(data['task_id'])
                if task is None:
                    raise NotFound
                if task.fav_user_ids is None:
                    task.fav_user_ids = [uid]
                else:
                    task.fav_user_ids.append(uid)
                task_repo.update(task)
                self._log_changes(None, task)
            return Response(json.dumps(task.dictize()),
                            200,
                            mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='POST')

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def delete(self, oid):
        """Delete User ID from task as a favorite."""
        try:
            if current_user.is_anonymous():
                raise abort(401)
            uid = current_user.id
            tasks = task_repo.get_task_favorited(uid, oid)
            if tasks == []:
                raise NotFound
            if len(tasks) == 1:
                task = tasks[0]
            idx = task.fav_user_ids.index(uid)
            task.fav_user_ids.pop(idx)
            task_repo.update(task)
            return Response(json.dumps(task.dictize()),
                            200,
                            mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='DEL')

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def put(self, oid):
        try:
            raise MethodNotAllowed
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='PUT')
예제 #11
0
class VmcpAPI(APIBase):
    """Class for CernVM plugin api.

    Returns signed object to start a CernVM instance.

    """
    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, id):
        """Return signed VMCP for CernVM requests."""
        error = dict(action=request.method,
                     status="failed",
                     status_code=None,
                     target='vmcp',
                     exception_cls='vmcp',
                     exception_msg=None)
        try:
            if current_app.config.get('VMCP_KEY'):
                pkey = (current_app.root_path + '/../keys/' +
                        current_app.config.get('VMCP_KEY'))
                if not os.path.exists(pkey):
                    raise IOError
            else:
                raise KeyError
            if request.args.get('cvm_salt'):
                salt = request.args.get('cvm_salt')
            else:
                raise AttributeError
            data = request.args.copy()
            signed_data = pybossa.vmcp.sign(data, salt, pkey)
            return Response(json.dumps(signed_data),
                            200,
                            mimetype='application/json')

        except KeyError:
            error['status_code'] = 501
            error['exception_msg'] = ("The server is not configured properly, \
                                      contact the admins")
            return Response(json.dumps(error),
                            status=error['status_code'],
                            mimetype='application/json')
        except IOError:
            error['status_code'] = 501
            error['exception_msg'] = ("The server is not configured properly \
                                      (private key is missing), contact the \
                                      admins")
            return Response(json.dumps(error),
                            status=error['status_code'],
                            mimetype='application/json')

        except AttributeError:
            error['status_code'] = 415
            error['exception_msg'] = "cvm_salt parameter is missing"
            return Response(json.dumps(error),
                            status=error['status_code'],
                            mimetype='application/json')

    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def post(self):
        raise MethodNotAllowed