from time import time
from osm_common.dbbase import deep_update_rfc7396
from osm_nbi.validation import validate_input, ValidationError, is_valid_uuid
+from yaml import safe_load, YAMLError
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
super(Exception, self).__init__(message)
+def deep_get(target_dict, key_list):
+ """
+ Get a value from target_dict entering in the nested keys. If keys does not exist, it returns None
+ Example target_dict={a: {b: 5}}; key_list=[a,b] returns 5; both key_list=[a,b,c] and key_list=[f,h] return None
+ :param target_dict: dictionary to be read
+ :param key_list: list of keys to read from target_dict
+ :return: The wanted value if exist, None otherwise
+ """
+ for key in key_list:
+ if not isinstance(target_dict, dict) or key not in target_dict:
+ return None
+ target_dict = target_dict[key]
+ return target_dict
+
+
def get_iterable(input_var):
"""
Returns an iterable, in case input_var is None it just returns an empty tuple
# static variables for all instance classes
topic = None # to_override
topic_msg = None # to_override
+ quota_name = None # to_override. If not provided topic will be used for quota_name
schema_new = None # to_override
schema_edit = None # to_override
multiproject = True # True if this Topic can be shared by several projects. Then it contains _admin.projects_read
"""
Check whether topic quota is exceeded by the given project
Used by relevant topics' 'new' function to decide whether or not creation of the new item should be allowed
- :param projects: projects (tuple) for which quota should be checked
- :param override: boolean. If true, don't raise ValidationError even though quota be exceeded
+ :param session[project_id]: projects (tuple) for which quota should be checked
+ :param session[force]: boolean. If true, skip quota checking
:return: None
:raise:
DbException if project not found
- ValidationError if quota exceeded and not overridden
+ ValidationError if quota exceeded in one of the projects
"""
- if session["force"] or session["admin"]:
+ if session["force"]:
return
projects = session["project_id"]
for project in projects:
proj = self.auth.get_project(project)
pid = proj["_id"]
- quota = proj.get("quotas", {}).get(self.topic, self.default_quota)
+ quota_name = self.quota_name or self.topic
+ quota = proj.get("quotas", {}).get(quota_name, self.default_quota)
count = self.db.count(self.topic, {"_admin.projects_read": pid})
if count >= quota:
name = proj["name"]
- raise ValidationError("{} quota ({}) exceeded for project {} ({})".format(self.topic, quota, name, pid))
+ raise ValidationError("quota ({}={}) exceeded for project {} ({})".format(quota_name, quota, name, pid),
+ http_code=HTTPStatus.UNAUTHORIZED)
def _validate_input_new(self, input, force=False):
"""
validate_input(input, self.schema_new)
return input
- def _validate_input_edit(self, input, force=False):
+ def _validate_input_edit(self, input, content, force=False):
"""
Validates input user content for an edition. It uses jsonschema. Some overrides will use pyangbind
:param input: user input content for the new topic
def _get_project_filter(session):
"""
Generates a filter dictionary for querying database, so that only allowed items for this project can be
- addressed. Only propietary or public can be used. Allowed projects are at _admin.project_read/write. If it is
+ addressed. Only proprietary or public can be used. Allowed projects are at _admin.project_read/write. If it is
not present or contains ANY mean public.
:param session: contains:
project_id: project list this session has rights to access. Can be empty, one or several
final_content["_admin"]["modified"] = now
return None
- def _send_msg(self, action, content):
- if self.topic_msg:
+ def _send_msg(self, action, content, not_send_msg=None):
+ if self.topic_msg and not_send_msg is not False:
content.pop("_admin", None)
- self.msg.write(self.topic_msg, action, content)
+ if isinstance(not_send_msg, list):
+ not_send_msg.append((self.topic_msg, action, content))
+ else:
+ self.msg.write(self.topic_msg, action, content)
def check_conflict_on_del(self, session, _id, db_content):
"""
pass
@staticmethod
- def _update_input_with_kwargs(desc, kwargs):
+ def _update_input_with_kwargs(desc, kwargs, yaml_format=False):
"""
Update descriptor with the kwargs. It contains dot separated keys
:param desc: dictionary to be updated
:param kwargs: plain dictionary to be used for updating.
+ :param yaml_format: get kwargs values as yaml format.
:return: None, 'desc' is modified. It raises EngineException.
"""
if not kwargs:
update_content = update_content[kitem_old]
if isinstance(update_content, dict):
kitem_old = kitem
+ if not isinstance(update_content.get(kitem_old), (dict, list)):
+ update_content[kitem_old] = {}
elif isinstance(update_content, list):
+ # key must be an index of the list, must be integer
kitem_old = int(kitem)
+ # if index greater than list, extend the list
+ if kitem_old >= len(update_content):
+ update_content += [None] * (kitem_old - len(update_content) + 1)
+ if not isinstance(update_content[kitem_old], (dict, list)):
+ update_content[kitem_old] = {}
else:
raise EngineException(
"Invalid query string '{}'. Descriptor is not a list nor dict at '{}'".format(k, kitem))
- update_content[kitem_old] = v
+ if v is None:
+ del update_content[kitem_old]
+ else:
+ update_content[kitem_old] = v if not yaml_format else safe_load(v)
except KeyError:
raise EngineException(
"Invalid query string '{}'. Descriptor does not contain '{}'".format(k, kitem_old))
except IndexError:
raise EngineException(
"Invalid query string '{}'. Index '{}' out of range".format(k, kitem_old))
+ except YAMLError:
+ raise EngineException("Invalid query string '{}' yaml format".format(k))
+
+ def sol005_projection(self, data):
+ # Projection was moved to child classes
+ return data
- def show(self, session, _id):
+ def show(self, session, _id, api_req=False):
"""
Get complete information on an topic
:param session: contains "username", "admin", "force", "public", "project_id", "set_project"
:param _id: server internal id
+ :param api_req: True if this call is serving an external API request. False if serving internal request.
:return: dictionary, raise exception if not found.
"""
if not self.multiproject:
filter_db = self._get_project_filter(session)
# To allow project&user addressing by name AS WELL AS _id
filter_db[BaseTopic.id_field(self.topic, _id)] = _id
- return self.db.get_one(self.topic, filter_db)
+ data = self.db.get_one(self.topic, filter_db)
+
+ # Only perform SOL005 projection if we are serving an external request
+ if api_req:
+ self.sol005_projection(data)
+
+ return data
+
# TODO transform data for SOL005 URL requests
# TODO remove _admin if not admin
"""
raise EngineException("Method get_file not valid for this topic", HTTPStatus.INTERNAL_SERVER_ERROR)
- def list(self, session, filter_q=None):
+ def list(self, session, filter_q=None, api_req=False):
"""
Get a list of the topic that matches a filter
:param session: contains the used login username and working project
:param filter_q: filter of data to be applied
+ :param api_req: True if this call is serving an external API request. False if serving internal request.
:return: The list, it can be empty if no one match the filter.
"""
if not filter_q:
# TODO transform data for SOL005 URL requests. Transform filtering
# TODO implement "field-type" query string SOL005
- return self.db.get_list(self.topic, filter_q)
+ data = self.db.get_list(self.topic, filter_q)
+
+ # Only perform SOL005 projection if we are serving an external request
+ if api_req:
+ data = [self.sol005_projection(inst) for inst in data]
+
+ return data
def new(self, rollback, session, indata=None, kwargs=None, headers=None):
"""
filter_q.update(self._get_project_filter(session))
return self.db.del_list(self.topic, filter_q)
- def delete_extra(self, session, _id, db_content):
+ def delete_extra(self, session, _id, db_content, not_send_msg=None):
"""
Delete other things apart from database entry of a item _id.
e.g.: other associated elements at database and other file system storage
:param _id: server internal id
:param db_content: The database content of the _id. It is already deleted when reached this method, but the
content is needed in same cases
+ :param not_send_msg: To not send message (False) or store content (list) instead
:return: None if ok or raises EngineException with the problem
"""
pass
- def delete(self, session, _id, dry_run=False):
+ def delete(self, session, _id, dry_run=False, not_send_msg=None):
"""
Delete item by its internal _id
:param session: contains "username", "admin", "force", "public", "project_id", "set_project"
:param _id: server internal id
:param dry_run: make checking but do not delete
+ :param not_send_msg: To not send message (False) or store content (list) instead
:return: operation id (None if there is not operation), raise exception if error or not found, conflict, ...
"""
# To allow addressing projects and users by name AS WELL AS by _id
- filter_q = {BaseTopic.id_field(self.topic, _id): _id}
+ if not self.multiproject:
+ filter_q = {}
+ else:
+ filter_q = self._get_project_filter(session)
+ filter_q[self.id_field(self.topic, _id)] = _id
item_content = self.db.get_one(self.topic, filter_q)
- # TODO add admin to filter, validate rights
- # data = self.get_item(topic, _id)
self.check_conflict_on_del(session, _id, item_content)
if dry_run:
return None
- if self.multiproject:
- filter_q.update(self._get_project_filter(session))
if self.multiproject and session["project_id"]:
- # remove reference from project_read. If not last delete
- # if this topic is not part of session["project_id"] no midification at database is done and an exception
- # is raised
- self.db.set_one(self.topic, filter_q, update_dict=None,
- pull={"_admin.projects_read": {"$in": session["project_id"]}})
- # try to delete if there is not any more reference from projects. Ignore if it is not deleted
- filter_q = {'_id': _id, '_admin.projects_read': [[], ["ANY"]]}
- v = self.db.del_one(self.topic, filter_q, fail_on_empty=False)
- if not v or not v["deleted"]:
+ # remove reference from project_read if there are more projects referencing it. If it last one,
+ # do not remove reference, but delete
+ other_projects_referencing = next((p for p in item_content["_admin"]["projects_read"]
+ if p not in session["project_id"] and p != "ANY"), None)
+
+ # check if there are projects referencing it (apart from ANY, that means, public)....
+ if other_projects_referencing:
+ # remove references but not delete
+ update_dict_pull = {"_admin.projects_read": session["project_id"],
+ "_admin.projects_write": session["project_id"]}
+ self.db.set_one(self.topic, filter_q, update_dict=None, pull_list=update_dict_pull)
return None
- else:
- self.db.del_one(self.topic, filter_q)
- self.delete_extra(session, _id, item_content)
- self._send_msg("deleted", {"_id": _id})
+ else:
+ can_write = next((p for p in item_content["_admin"]["projects_write"] if p == "ANY" or
+ p in session["project_id"]), None)
+ if not can_write:
+ raise EngineException("You have not write permission to delete it",
+ http_code=HTTPStatus.UNAUTHORIZED)
+
+ # delete
+ self.db.del_one(self.topic, filter_q)
+ self.delete_extra(session, _id, item_content, not_send_msg=not_send_msg)
+ self._send_msg("deleted", {"_id": _id}, not_send_msg=not_send_msg)
return None
def edit(self, session, _id, indata=None, kwargs=None, content=None):
if indata and session.get("set_project"):
raise EngineException("Cannot edit content and set to project (query string SET_PROJECT) at same time",
HTTPStatus.UNPROCESSABLE_ENTITY)
- indata = self._validate_input_edit(indata, force=session["force"])
-
+
# TODO self._check_edition(session, indata, _id, force)
if not content:
content = self.show(session, _id)
+
+ indata = self._validate_input_edit(indata, content, force=session["force"])
+
deep_update_rfc7396(content, indata)
# To allow project addressing by name AS WELL AS _id. Get the _id, just in case the provided one is a name