Source code for isomer.ui.objectmanager.crud

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

# Isomer - The distributed application framework
# ==============================================
# Copyright (C) 2011-2020 Heiko 'riot' Weinen <riot@c-base.org> and others.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""

Module: objectmanager.crud
==========================

CRUD operations for objects. CRUD stands for

* Create
* Read
* Update
* Delete


"""

from uuid import uuid4
from ast import literal_eval

from isomer.component import handler
from isomer.database import objectmodels, ValidationError
from isomer.schemastore import schemastore
from isomer.events.client import send
from isomer.events.objectmanager import (
    get,
    search,
    getlist,
    change,
    put,
    objectcreation,
    objectchange,
    delete,
    objectdeletion,
)
from isomer.logger import warn, verbose, error, debug, critical
from isomer.misc import nested_map_find, nested_map_update
from isomer.misc.std import std_uuid
from pymongo import ASCENDING, DESCENDING

from isomer.ui.objectmanager.cli import CliManager

WARN_SIZE = 500


[docs]class CrudOperations(CliManager): """Adds CRUD (create, read, update, delete) functionality"""
[docs] @handler(get) def get(self, event): """Get a specified object""" try: data, schema, user, client = self._get_args(event) except AttributeError: return object_filter = self._get_filter(event) if "subscribe" in data: do_subscribe = data["subscribe"] is True else: do_subscribe = False try: uuid = str(data["uuid"]) except (KeyError, TypeError): uuid = "" opts = schemastore[schema].get("options", {}) hidden = opts.get("hidden", []) if object_filter == {}: if uuid == "": self.log( "Object with no filter/uuid requested:", schema, data, lvl=warn ) return object_filter = {"uuid": uuid} storage_object = None storage_object = objectmodels[schema].find_one(object_filter) if not storage_object: self._cancel_by_error( event, uuid + "(" + str(object_filter) + ") of " + schema + " unavailable", ) return if storage_object: self.log("Object found, checking permissions: ", data, lvl=verbose) if not self._check_permissions(user, "read", storage_object): self._cancel_by_permission(schema, data, event) return for field in hidden: storage_object._fields.pop(field, None) if do_subscribe and uuid != "": self._add_subscription(uuid, event) result = { "component": "isomer.events.objectmanager", "action": "get", "data": { "schema": schema, "uuid": uuid, "object": storage_object.serializablefields(), }, } self._respond(None, result, event)
[docs] @handler(search) def search(self, event): """Search for an object""" try: data, schema, user, client = self._get_args(event) except AttributeError: return def get_filter(filter_request): # result['$text'] = {'$search': str(data['search'])} request_search = filter_request["search"] if filter_request.get("fulltext", False) is True: search_filter = { "name": {"$regex": str(request_search), "$options": "$i"} } else: search_filter = {} if isinstance(request_search, dict): search_filter = request_search elif isinstance(request_search, str) and len(request_search) > 0: if request_search != "*": self.log(request_search, lvl=warn) request_search = request_search.replace(r"\\\\", r"") search_filter = literal_eval(request_search) self.log("Final filter:", search_filter, lvl=debug) return search_filter object_filter = get_filter(data) if "fields" in data: fields = data["fields"] else: fields = [] skip = data.get("skip", 0) limit = data.get("limit", 0) sort = data.get("sort", None) # page = data.get('page', 0) # count = data.get('count', 0) # # if page > 0 and count > 0: # skip = page * count # limit = count if "subscribe" in data: self.log("Subscription:", data["subscribe"], lvl=verbose) do_subscribe = data["subscribe"] is True else: do_subscribe = False object_list = [] size = objectmodels[schema].count(object_filter) if size > WARN_SIZE and (limit > 0 and limit > WARN_SIZE): self.log( "Getting a very long (", size, ") list of items for ", schema, lvl=warn ) opts = schemastore[schema].get("options", {}) hidden = opts.get("hidden", []) self.log( "result: ", object_filter, " Schema: ", schema, "Fields: ", fields, lvl=verbose, ) def get_options(): options = {} if skip > 0: options["skip"] = skip if limit > 0: options["limit"] = limit if sort is not None: options["sort"] = [] for thing in sort: key = thing[0] direction = thing[1] direction = ASCENDING if direction == "asc" else DESCENDING options["sort"].append([key, direction]) return options cursor = objectmodels[schema].find(object_filter, **get_options()) for item in cursor: if not self._check_permissions(user, "list", item): continue self.log("Search found item: ", item, lvl=verbose) try: list_item = {"uuid": item.uuid} if fields in ("*", ["*"]): item_fields = item.serializablefields() for field in hidden: item_fields.pop(field, None) object_list.append(item_fields) else: if "name" in item._fields: list_item["name"] = item.name for field in fields: if field in item._fields and field not in hidden: list_item[field] = item._fields[field] else: list_item[field] = None object_list.append(list_item) if do_subscribe: self._add_subscription(item.uuid, event) except Exception as e: self.log( "Faulty object or field: ", e, type(e), item._fields, fields, lvl=error, exc=True, ) # self.log("Generated object search list: ", object_list) result = { "component": "isomer.events.objectmanager", "action": "search", "data": {"schema": schema, "list": object_list, "size": size}, } self._respond(None, result, event)
[docs] @handler(getlist) def objectlist(self, event): """Get a list of objects""" self.log("LEGACY LIST FUNCTION CALLED!", lvl=warn) try: data, schema, user, client = self._get_args(event) except AttributeError: return object_filter = self._get_filter(event) self.log( "Object list for", schema, "requested from", user.account.name, lvl=debug ) if "fields" in data: fields = data["fields"] else: fields = [] object_list = [] opts = schemastore[schema].get("options", {}) hidden = opts.get("hidden", []) if objectmodels[schema].count(object_filter) > WARN_SIZE: self.log("Getting a very long list of items for ", schema, lvl=warn) try: for item in objectmodels[schema].find(object_filter): try: if not self._check_permissions(user, "list", item): continue if fields in ("*", ["*"]): item_fields = item.serializablefields() for field in hidden: item_fields.pop(field, None) object_list.append(item_fields) else: list_item = {"uuid": item.uuid} if "name" in item._fields: list_item["name"] = item._fields["name"] for field in fields: if field in item._fields and field not in hidden: list_item[field] = item._fields[field] else: list_item[field] = None object_list.append(list_item) except Exception as e: self.log( "Faulty object or field: ", e, type(e), item._fields, fields, lvl=error, exc=True, ) except ValidationError as e: self.log("Invalid object in database encountered!", e, exc=True, lvl=warn) # self.log("Generated object list: ", object_list) result = { "component": "isomer.events.objectmanager", "action": "getlist", "data": {"schema": schema, "list": object_list}, } self._respond(None, result, event)
[docs] @handler(change) def change(self, event): """Change an existing object""" try: data, schema, user, client = self._get_args(event) except AttributeError: return try: uuid = data["uuid"] object_change = data["change"] field = object_change["field"] new_data = object_change["value"] except KeyError as e: self.log("Update request with missing arguments!", data, e, lvl=critical) self._cancel_by_error(event, "missing_args") return storage_object = None try: storage_object = objectmodels[schema].find_one({"uuid": uuid}) except Exception as e: self.log("Change for unknown object requested:", e, schema, data, lvl=warn) if storage_object is None: self._cancel_by_error(event, "not_found") return if not self._check_permissions(user, "write", storage_object): self._cancel_by_permission(schema, data, event) return self.log("Changing object:", storage_object._fields, lvl=debug) storage_object._fields[field] = new_data self.log("Storing object:", storage_object._fields, lvl=debug) try: storage_object.validate() except ValidationError: self.log("Validation of changed object failed!", storage_object, lvl=warn) self._cancel_by_error(event, "invalid_object") return storage_object.save() self.log("Object stored.") notification = objectchange(storage_object.uuid, schema, client) self._update_subscribers(schema, storage_object) result = { "component": "isomer.events.objectmanager", "action": "change", "data": {"schema": schema, "uuid": uuid}, } self._respond(notification, result, event)
def _validate(self, schema_name, model, client_data): """Validates and tries to fix up to 10 errors in client model data..""" # TODO: This should probably move to Formal. # Also i don't like artificially limiting this. # Alas, never giving it up is even worse :) give_up = 10 validated = False while give_up > 0 and validated is False: try: validated = model(client_data) except ValidationError as e: self.log("Validation Error:", e, e.__dict__, pretty=True) give_up -= 1 if e.validator == "type": schema_data = schemastore[schema_name]["schema"] if e.validator_value == "number": definition = nested_map_find( schema_data, list(e.schema_path)[:-1] ) if "default" in definition: client_data = nested_map_update( client_data, definition["default"], list(e.path) ) else: client_data = nested_map_update( client_data, None, list(e.path) ) if ( e.validator == "pattern" and "uuid" == e.path[0] and client_data["uuid"] == "create" ): client_data["uuid"] = std_uuid() if validated is False: raise ValidationError return client_data
[docs] @handler(put) def put(self, event): """Put an object""" try: data, schema, user, client = self._get_args(event) except AttributeError: return try: client_object = data["obj"] uuid = client_object["uuid"] except KeyError as e: self.log("Put request with missing arguments!", e, data, lvl=critical) return try: model = objectmodels[schema] created = False storage_object = None try: client_object = self._validate(schema, model, client_object) except ValidationError: self._cancel_by_error(event, "Invalid data") return if uuid != "create": storage_object = model.find_one({"uuid": uuid}) if uuid == "create" or model.count({"uuid": uuid}) == 0: if uuid == "create": uuid = str(uuid4()) created = True client_object["uuid"] = uuid client_object["owner"] = user.uuid storage_object = model(client_object) if not self._check_create_permission(user, schema): self._cancel_by_permission(schema, data, event) return if storage_object is not None: if not self._check_permissions(user, "write", storage_object): self._cancel_by_permission(schema, data, event) return self.log("Updating object:", storage_object._fields, lvl=debug) storage_object.update(client_object) else: storage_object = model(client_object) if not self._check_permissions(user, "write", storage_object): self._cancel_by_permission(schema, data, event) return self.log("Storing object:", storage_object._fields, lvl=debug) try: storage_object.validate() except ValidationError: self.log( "Validation of new object failed!", client_object, lvl=warn ) storage_object.save() self.log("Object %s stored." % schema) # Notify backend listeners if created: notification = objectcreation(storage_object.uuid, schema, client) else: notification = objectchange(storage_object.uuid, schema, client) self._update_subscribers(schema, storage_object) result = { "component": "isomer.events.objectmanager", "action": "put", "data": { "schema": schema, "object": storage_object.serializablefields(), "uuid": storage_object.uuid, }, } self._respond(notification, result, event) except Exception as e: self.log( "Error during object storage:", e, e.__dict__, type(e), data, lvl=error, exc=True, pretty=True, )
[docs] @handler(delete) def delete(self, event): """Delete an existing object""" try: data, schema, user, client = self._get_args(event) except AttributeError: return try: uuids = data["uuid"] if not isinstance(uuids, list): uuids = [uuids] if schema not in objectmodels.keys(): self.log("Unknown schema encountered: ", schema, lvl=warn) return for uuid in uuids: self.log("Looking for object to be deleted:", uuid, lvl=debug) storage_object = objectmodels[schema].find_one({"uuid": uuid}) if not storage_object: self._cancel_by_error(event, "not found") return self.log("Found object.", lvl=debug) if not self._check_permissions(user, "write", storage_object): self._cancel_by_permission(schema, data, event) return # self.log("Fields:", storage_object._fields, "\n\n\n", # storage_object.__dict__) storage_object.delete() self.log("Deleted. Preparing notification.", lvl=debug) notification = objectdeletion(uuid, schema, client) if uuid in self.subscriptions: deletion = { "component": "isomer.events.objectmanager", "action": "deletion", "data": {"schema": schema, "uuid": uuid}, } for recipient in self.subscriptions[uuid]: self.fireEvent(send(recipient, deletion)) del self.subscriptions[uuid] result = { "component": "isomer.events.objectmanager", "action": "delete", "data": {"schema": schema, "uuid": storage_object.uuid}, } self._respond(notification, result, event) except Exception as e: self.log("Error during delete request: ", e, type(e), lvl=error)