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'])
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
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'])
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")
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,
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.
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'])
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
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
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')
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