gecos-team/gecoscc-ui

View on GitHub
gecoscc/api/__init__.py

Summary

Maintainability
F
4 days
Test Coverage
from __future__ import division
#
# Copyright 2013, Junta de Andalucia
# http://www.juntadeandalucia.es/
#
# Authors:
#   Antonio Perez-Aranda <ant30tx@gmail.com>
#   Pablo Martin <goinnn@gmail.com>
#
# All rights reserved - EUPL License V 1.1
# https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
#

from builtins import str
from past.utils import old_div
from builtins import object
import cgi
import os

from bson import ObjectId
from copy import deepcopy

from pymongo.errors import DuplicateKeyError
from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPForbidden
from webob.multidict import MultiDict

from gecoscc.models import Node
from gecoscc.permissions import (can_access_to_this_path, nodes_path_filter,
                                 is_gecos_master_or_403,
                                 master_policy_no_updated_or_403)
from gecoscc.socks import invalidate_change, invalidate_delete
from gecoscc.tasks import (object_created, object_changed, object_deleted, 
                           object_moved, object_refresh_policies)
from gecoscc.utils import (get_computer_of_user, get_filter_nodes_parents_ou,
                           oids_filter, check_unique_node_name_by_type_at_domain,
                           visibility_object_related, visibility_group,
                           RESOURCES_EMITTERS_TYPES, 
                           get_object_related_list_count,
                           is_domain, get_domain, is_root)

import gettext
import logging
logger = logging.getLogger(__name__)

SAFE_METHODS = ('GET', 'OPTIONS', 'HEAD',)
UNSAFE_METHODS = ('POST', 'PUT', 'PATCH', 'DELETE', )
SCHEMA_METHODS = ('POST', 'PUT', )


class BaseAPI(object):

    order_field = '_id'

    def __init__(self, request, context=None):
        self.request = request
        self.collection = self.get_collection()
        localedir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'locale')
        gettext.bindtextdomain('gecoscc', localedir)
        gettext.textdomain('gecoscc')
        self._ = gettext.gettext                                

    def parse_item(self, item):
        return self.schema_detail().serialize(item)

    def parse_collection(self, collection):
        return self.schema_collection().serialize(collection)

    def get_collection(self, collection=None):
        if collection is None:
            collection = self.collection_name
        return self.request.db[collection]

    def set_variables(self, method):
        request = self.request
        fs = cgi.FieldStorage(fp=request.body_file,
                              environ=request.environ.copy(),
                              keep_blank_values=True)
        setattr(self.request, method, MultiDict.from_fieldstorage(fs))


class ResourcePaginatedReadOnly(BaseAPI):

    schema_collection = None
    schema_detail = None
    mongo_filter = {
        'type': 'anytype',
    }
    collection_name = 'nodes'
    objtype = None
    key = '_id'

    def __init__(self, request, context=None):
        super(ResourcePaginatedReadOnly, self).__init__(request,
            context=context)
        self.default_pagesize = request.registry.settings.get(
            'default_pagesize', 30)
        if self.objtype is None:
            raise self.BadResourceDefinition('objtype is not defined')

    class BadResourceDefinition(Exception):
        pass

    def set_name_filter(self, query, key_name='name'):
        if 'name' in self.request.GET:
            query.append({
                key_name: self.request.GET.get('name')
            })

        if 'iname' in self.request.GET:
            query.append({
                key_name: {
                    '$regex': u'.*{0}.*'.format(self.request.GET.get('iname')),
                    '$options': '-i'
                }
            })

    def set_username_filter(self, query):
        if 'iname' in self.request.GET:
            # Look for users with that username (or similar)
            users = self.request.db.nodes.find({"type": "user", "name": {
                    '$regex': u'.*{0}.*'.format(self.request.GET.get('iname')),
                    '$options': '-i'
                }})
            
            # Merge the list of computers
            computer_ids = []
            for user in users:
                computer_ids = computer_ids + user['computers']

            # Append the list of computers to the query
            query.append({
                "_id": {
                    "$in": computer_ids
                }
            })    
            
    def get_objects_filter(self):
        query = []
        if not self.request.method == 'GET':
            return []

        if 'search_by' in self.request.GET:
            search_by = self.request.GET.get('search_by')
            if search_by == 'ip':
                # Search by IPv4 address
                self.set_name_filter(query, 'ipaddress')

            elif search_by == 'username':
                # Search by username
                self.set_username_filter(query)

            else:
                # Default: search by node name
                self.set_name_filter(query)
            
        else:
            # Default: search by node name
            self.set_name_filter(query)

        if 'oids' in self.request.GET:
            oid_filters = oids_filter(self.request)
            if oid_filters:
                query.append(oid_filters)

        if issubclass(self.schema_detail, Node):
            path_filter = nodes_path_filter(self.request, ['ou_managed','ou_readonly'])
            if path_filter:
                query.append(path_filter)

        return query

    def get_object_filter(self):
        return {}

    def get_distinct_filter(self, objects):
        return objects

    def get_oid_filter(self, oid):
        return {self.key: ObjectId(oid)}

    def collection_get(self):
        page = int(self.request.GET.get('page', 1))
        pagesize = int(self.request.GET.get('pagesize', self.default_pagesize))
        if pagesize <= 0 or page <= 0:
            raise HTTPBadRequest()
        extraargs = {
            'skip': (page - 1) * pagesize,
            'limit': pagesize,
        }

        objects_filter = self.get_objects_filter()
        if self.mongo_filter:
            objects_filter.append(self.mongo_filter)

        if objects_filter:
            mongo_query = {
                '$and': objects_filter,
            }
        else:
            mongo_query = {}

        nodes_count = self.collection.count_documents(mongo_query)

        objects = self.collection.find(mongo_query, **extraargs).sort(self.order_field)
        objects = self.get_distinct_filter(objects)
        pages = int(old_div(nodes_count, pagesize))
        if nodes_count % pagesize > 0:
            pages += 1
        parsed_objects = self.parse_collection(list(objects))
        return {
            'pagesize': pagesize,
            'pages': pages,
            'page': page,
            self.collection_name: parsed_objects,
            'total': nodes_count,
        }

    def get(self):
        oid = self.request.matchdict['oid']
        if issubclass(self.schema_detail, Node):
            try:
                can_access_to_this_path(self.request, self.collection, oid, ou_type='ou_readonly')
            except HTTPForbidden:
                can_access_to_this_path(self.request, self.collection, oid)
        collection_filter = self.get_oid_filter(oid)
        collection_filter.update(self.get_object_filter())
        collection_filter.update(self.mongo_filter)
        node = self.collection.find_one(collection_filter)
        if not node:
            raise HTTPNotFound()
        node = self.parse_item(node)

        if node.get('type', None) in RESOURCES_EMITTERS_TYPES:
            node['is_assigned'] = self.is_assigned(node)
            return node
        return node
        
    def is_assigned(self, related_object):
        node_with_related_object_count = get_object_related_list_count(
            self.request.db, related_object)
        return bool(node_with_related_object_count)


class ResourcePaginated(ResourcePaginatedReadOnly):

    def __init__(self, request, context=None):
        super(ResourcePaginated, self).__init__(request, context=context)
        if request.method == 'POST':
            schema = self.schema_detail()
            del schema['_id']
            self.schema = schema

        elif request.method == 'PUT':
            self.schema = self.schema_detail
            # Implement write permissions

    def integrity_validation(self, obj, real_obj=None):
        return True

    def pre_save(self, obj, old_obj=None):
        if old_obj and 'name' in old_obj:
            obj['name'] = old_obj['name']

        # Check he policies "object_related_list" attribute
        if 'policies' in obj:
            policies = obj['policies']
            for policy in policies:
                # Get the policy
                policyobj = self.request.db.policies.find_one({"_id": ObjectId(str(policy))})
                if policyobj is None:
                    logger.warning("Unknown policy: %s" % (str(policy)))
                else:
                    # Get the related object collection
                    ro_collection = None
                    if policyobj['slug'] == 'printer_can_view':
                        ro_collection = self.request.db.nodes
                    elif policyobj['slug'] == 'repository_can_view':
                        ro_collection = None
                    elif policyobj['slug'] == 'storage_can_view':
                        ro_collection = self.request.db.nodes
                    elif policyobj['slug'] == 'local_users_res':
                        ro_collection = None
                    else:
                        logger.info("Policy without related objects: %s" % (str(policyobj['slug'])))

                    # Check the related objects
                    if ro_collection is not None:
                        ro_list = policies[str(policy)]['object_related_list']
                        for ro_id in ro_list:
                            ro_obj = ro_collection.find_one({"_id": ObjectId(str(ro_id))})
                            if ro_obj is None:
                                logger.error("Can't find related object: %s:%s" % (str(policyobj['slug']), str(ro_id)))
                                self.request.errors.add('body', 'object', "Can't find related object: %s:%s" % (str(policyobj['slug']), str(ro_id)))
                                return None

        else:
            logger.debug("No policies in this object")

        return obj

    def post_save(self, obj, old_obj=None):
        return obj

    def pre_delete(self, obj, old_obj=None):
        return obj

    def post_delete(self, obj, old_obj=None):
        return obj

    def collection_post(self):
        obj = self.request.validated

        if issubclass(self.schema_detail, Node):
            can_access_to_this_path(self.request, self.collection, obj)
            is_gecos_master_or_403(self.request, self.collection, obj, self.schema_detail)
            master_policy_no_updated_or_403(self.request, self.collection, obj)

        if not self.integrity_validation(obj):
            if len(self.request.errors) < 1:
                self.request.errors.add('body', 'object', 'Integrity error')
            return

        # Remove '_id' for security reasons
        if self.key in obj:
            del obj[self.key]

        obj = self.pre_save(obj)
        if obj is None:
            return

        try:
            obj_id = self.collection.insert_one(obj).inserted_id
        except DuplicateKeyError as e:
            raise HTTPBadRequest('The Object already exists: '
                                 '{0}'.format(e.message))

        obj = self.post_save(obj)

        obj.update({self.key: obj_id})
        self.notify_created(obj)
        return self.parse_item(obj)

    def notify_created(self, obj):
        object_created.delay(self.request.user, self.objtype, obj)

    def notify_changed(self, obj, old_obj):
        if obj['path'] != old_obj['path']:
            object_moved.delay(self.request.user, self.objtype, obj, old_obj)
        else:
            object_changed.delay(self.request.user, self.objtype, obj, old_obj)
            invalidate_change(self.request, obj)

    def notify_deleted(self, obj):
        object_deleted.delay(self.request.user, self.objtype, obj)
        invalidate_delete(self.request, obj)

    def notify_refresh_policies(self, obj):
        object_refresh_policies.delay(self.request.user, self.objtype, obj)
        invalidate_change(self.request, obj)



    def put(self):
        obj = self.request.validated
        oid = self.request.matchdict['oid']

        if oid != str(obj[self.key]):
            raise HTTPBadRequest('The object id is not the same that the id in'
                                 ' the url')

        if issubclass(self.schema_detail, Node):
            can_access_to_this_path(self.request, self.collection, oid)
            is_gecos_master_or_403(self.request, self.collection, obj, self.schema_detail)
            master_policy_no_updated_or_403(self.request, self.collection, obj)

        obj_filter = self.get_oid_filter(oid)
        obj_filter.update(self.mongo_filter)

        real_obj = self.collection.find_one(obj_filter)
        if not real_obj:
            raise HTTPNotFound()
        old_obj = deepcopy(real_obj)

        if not self.integrity_validation(obj, real_obj=real_obj):
            if len(self.request.errors) < 1:
                self.request.errors.add('body', 'object', 'Integrity error')
                logger.error("Integrity error in object: {0}".format(obj))
            else:
                logger.error("Integrity error {0} in object: {1}".format(
                    self.request.errors, obj))
                
            return

        obj = self.pre_save(obj, old_obj=old_obj)
        if obj is None:
            return

        inheritance_backup = None
        if 'inheritance' in obj:
            # Do not save the inheritance field
            inheritance_backup = obj['inheritance']
            del obj['inheritance']
            
        real_obj.update(obj)
        
        try:
            self.collection.replace_one(obj_filter, real_obj)
        except DuplicateKeyError as e:
            raise HTTPBadRequest('Duplicated object {0}'.format(
                e.message))

        obj = self.post_save(obj, old_obj=old_obj)
        self.notify_changed(obj, old_obj)
        obj = self.parse_item(obj)
        
        if inheritance_backup is not None:
            obj['inheritance'] = inheritance_backup
        
        return obj



    def refresh_policies(self):
        obj = self.request.validated
        oid = self.request.matchdict['oid']

        if oid != str(obj[self.key]):
            raise HTTPBadRequest('The object id is not the same that the id in'
                                 ' the url')

        if issubclass(self.schema_detail, Node):
            can_access_to_this_path(self.request, self.collection, oid)
            is_gecos_master_or_403(self.request, self.collection, obj, self.schema_detail)
            master_policy_no_updated_or_403(self.request, self.collection, obj)

        obj_filter = self.get_oid_filter(oid)
        obj_filter.update(self.mongo_filter)

        real_obj = self.collection.find_one(obj_filter)
        if not real_obj:
            raise HTTPNotFound()
            
        self.notify_refresh_policies(real_obj)
        
        obj = self.parse_item(real_obj)
        
        return obj


    def delete(self):

        oid = self.request.matchdict['oid']

        if issubclass(self.schema_detail, Node):
            obj = self.collection.find_one({'_id': ObjectId(oid)})
            can_access_to_this_path(self.request, self.collection, oid)
            is_gecos_master_or_403(self.request, self.collection, obj, self.schema_detail)
            master_policy_no_updated_or_403(self.request, self.collection, obj)

        filters = self.get_oid_filter(oid)
        filters.update(self.mongo_filter)

        obj = self.collection.find_one(filters)
        if not obj:
            raise HTTPNotFound()
        old_obj = deepcopy(obj)

        obj = self.pre_save(obj)
        if obj is None:
            return
        obj = self.pre_delete(obj)

        status = self.collection.delete_one(filters).raw_result

        if status['ok']:
            obj = self.post_save(obj, old_obj)
            obj = self.post_delete(obj)

            self.notify_deleted(obj)
            return {
                'status': 'The object was deleted successfully',
                'ok': 1
            }
        else:
            self.request.errors.add('body', '%s: db status'%(obj[self.key]),
                                    status)
            return


class TreeResourcePaginated(ResourcePaginated):

    def check_unique_node_name_by_type_at_domain(self, obj):
        unique = check_unique_node_name_by_type_at_domain(self.request.db.nodes, obj)
        if not unique:
            self.request.errors.add('body', 'name',
                                    "Name must be unique in domain.")
        if unique and not is_domain(obj) and not is_root(obj):
            # Check that the node name is not the same as the domain name
            domain = get_domain(obj, self.request.db.nodes)
            if domain['name'].lower() == obj['name'].lower():
                self.request.errors.add('body', 'name',
                                    "Name already used as domain name.")                
                return False
            
        
        return unique

    def integrity_validation(self, obj, real_obj=None):
        """ Test that the object path already exist """

        if real_obj is not None and obj['path'] == real_obj['path']:
            # This path was already verified before
            return True

        parents = obj['path'].split(',')

        parent_id = parents[-1]

        if parent_id == 'root':
            return True

        parent = self.collection.find_one({self.key: ObjectId(parent_id)})
        if not parent:
            self.request.errors.add('body', '%s: path'%(str(obj[self.key])),
                "parent doesn't exist {0}".format(parent_id))
            return False

        candidate_path_parent = ','.join(parents[:-1])

        if parent['path'] != candidate_path_parent:
            self.request.errors.add('body',
                '%s: path'%(str(obj[self.key])),
                "the parent object {0} has a different path".format(parent_id))
            return False

        return self.check_unique_node_name_by_type_at_domain(obj)

# TODO: Only have to extends this class the ComputerResource and UserResource
# Now there are another class that extends it. I don't make it, because this
# could break another things


class TreeLeafResourcePaginated(TreeResourcePaginated):

    def check_memberof_integrity(self, obj):
        """ Check if memberof ids already exists or if the group is out of scope"""
        if 'memberof' not in obj:
            return True
        obj_validated = visibility_group(self.request.db, obj)
        if obj != obj_validated:
            self.request.errors.add('body', '%s: memberof'%(str(obj[self.key])),
                                    "There is a group out of scope.")
            return False
        for group_id in obj['memberof']:
            group = self.request.db.nodes.find_one({'_id': group_id})
            if not group:
                self.request.errors.add(
                    'body', '%s: memberof'%(str(obj[self.key])),
                    "The group {0} doesn't exist".format(str(group_id)))
                return False
        return True

    def check_policies_integrity(self, obj, is_moved=False):
        """
        Check if the policy is out of scope
        """
        obj_original = deepcopy(obj)
        visibility_object_related(self.request.db, obj)
        if not is_moved:
            if obj != obj_original:
                self.request.errors.add('body',
                                        '%s - policies'%(str(obj[self.key])),
                                        "The related object is out of scope")
                return False
        return True

    def integrity_validation(self, obj, real_obj=None):
        result = super(TreeLeafResourcePaginated, self).integrity_validation(
            obj, real_obj)
        result = result and self.check_memberof_integrity(obj)
        result = result and self.check_unique_node_name_by_type_at_domain(obj)
        if real_obj is not None and real_obj['path'] == obj['path']:
            result = result and self.check_policies_integrity(obj)
        else:
            result = result and self.check_policies_integrity(obj, is_moved=True)
        return result

    def computers_to_group(self, obj):
        if obj['type'] == 'computer':
            return [obj]
        elif obj['type'] == 'user':
            return get_computer_of_user(self.collection, obj)
        raise ValueError("The object type should be computer or user")

    def post_save(self, obj, old_obj=None):
        if self.request.method == 'DELETE':
            newmemberof = []
        else:
            newmemberof = obj.get('memberof', [])
        if old_obj is not None:
            oldmemberof = old_obj.get('memberof', [])
        else:
            oldmemberof = []

        adds = [n for n in newmemberof if n not in oldmemberof]
        removes = [n for n in oldmemberof if n not in newmemberof]

        for group_id in removes:
            group = self.request.db.nodes.find_one({'_id': group_id})
            self.request.db.nodes.update_one({
                '_id': group_id
            }, {
                '$pull': {
                    'members': obj['_id']
                }
            })
            group_without_policies = self.request.db.nodes.find_one({'_id': group_id})
            group_without_policies['policies'] = {}
            computers = self.computers_to_group(obj)
            object_changed.delay(self.request.user, 'group', group_without_policies, group, 'changed', computers)

        for group_id in adds:

            # Add newmember to new group
            self.request.db.nodes.update_one({
                '_id': group_id
            }, {
                '$push': {
                    'members': obj['_id']
                }
            })
            group = self.request.db.nodes.find_one({'_id': group_id})
            computers = self.computers_to_group(obj)
            object_changed.delay(self.request.user, 'group', group, {}, 'changed', computers)

        return super(TreeLeafResourcePaginated, self).post_save(obj, old_obj)


class PassiveResourcePaginated(TreeLeafResourcePaginated):

    def get_objects_filter(self):
        filters = super(PassiveResourcePaginated, self).get_objects_filter()
        ou_id = self.request.GET.get('ou_id', None)
        item_id = self.request.GET.get('item_id', None)
        if ou_id and item_id:
            filters.append({'path': get_filter_nodes_parents_ou(self.request.db,
                                                                ou_id, item_id)})
        return filters

    def check_obj_is_related(self, obj):
        '''
        Check if the emitter object is related with any object
        '''
        if obj.get('_id'):
            if obj['type'] == 'printer':
                slug = 'printer_can_view'
            elif obj['type'] == 'repository':
                slug = 'repository_can_view'
            elif obj['type'] == 'storage':
                slug = 'storage_can_view'
            elif obj['type'] == 'group':
                members_group = obj['members']
                if not members_group:
                    return True
                return False

            policy_id = self.request.db.policies.find_one({'slug': slug}).get(
                '_id')
            nodes_related_with_obj = self.request.db.nodes.count_documents(
                {"policies.%s.object_related_list" % str(policy_id): {
                    '$in': [str(obj['_id'])]}})
            if nodes_related_with_obj == 0:
                return True

            return False
        return True

    def integrity_validation(self, obj, real_obj=None):
        result = super(PassiveResourcePaginated, self).integrity_validation(
            obj, real_obj)
        result = result and (self.request.user.get('is_superuser', False) or self.check_obj_is_related(obj))

        return result