def test_update_raw_node_relationship(self): group1, group2 = Group.objects.create( name='group1'), Group.objects.create(name='group2') node1, node2 = get_node_for_object(group1), get_node_for_object(group2) # Create a custom relationship between group1 and group2 results, _ = db.cypher_query( 'MATCH (a), (b) WHERE ID(a) = %d AND ID(b) = %d ' 'CREATE (a)-[r1:RELATION]->(b), (b)-[r2:RELATION]->(a) ' 'RETURN r1, r2' % (node1.id, node2.id)) results = list(flatten(results)) self.assertEqual(len(results), 2) self.assertTrue(all([r.type == 'RELATION' for r in results])) node1.__update_raw_node__() # Make sure custom relationship is deleted results, _ = db.cypher_query( 'MATCH (n)-[r]->() WHERE ID(n) = %d RETURN r' % node1.id) self.assertEqual(len(results), 0) # The other relationship should still be intact results, _ = db.cypher_query( 'MATCH (n)-[r]->() WHERE ID(n) = %d RETURN r' % node2.id) results = list(flatten(results)) self.assertEqual(len(results), 1)
def test_inflate_node_class_raises_inflate_error(self): # Make sure the InflateError is raised if trying # to inflate nodes with wrong model author = AuthorFixture(Author).create_one() deflated, _ = list( flatten( db.cypher_query('MATCH (u: UserNode {pk: %d}) RETURN u;' % author.user.pk))) node_class = get_node_class_for_model(Author) self.assertRaises(InflateError, node_class.inflate, deflated)
def _get_id_from_database(self, params): """ Query for node and return id. :param params: Parameters to use in query. :returns: Node id if found, else None """ query = ' '.join( ('MATCH (n:{label}) WHERE'.format(label=self.__label__), ' AND '.join([ 'n.{} = {{{}}}'.format(key, key) for key in params.keys() ]), 'RETURN id(n) LIMIT 1')) result, _ = db.cypher_query(query, params) return list(flatten(result))[0] if result else None
def test_update_raw_node_property(self): group = Group.objects.create(name='group') node = get_node_for_object(group) # Set a custom attribute on the node and make sure it's saved on the node result, _ = list( flatten( db.cypher_query('MATCH (n) WHERE ID(n) = %d ' 'SET n.foo = "bar", n.baz = "qux" RETURN n' % node.id))) self.assertTrue(all(i in result.properties for i in ('foo', 'baz'))) self.assertEqual(result.properties['foo'], 'bar') self.assertEqual(result.properties['baz'], 'qux') self.assertFalse(all(hasattr(node, i) for i in ('foo', 'baz'))) node.__update_raw_node__() # Make sure the custom attribute has been deleted result, _ = list( flatten( db.cypher_query('MATCH (n) WHERE ID(n) = %d RETURN n' % node.id))) self.assertFalse(all(i in result.properties for i in ('foo', 'baz')))
def list(self, request, format=None): """ Returns the meta graph serialized as JSON. """ result, _ = db.cypher_query( 'MATCH (n {type: "MetaNode", is_intermediary: False}) RETURN n') response = [] for item in list(flatten(result)): try: if hasattr(item, 'properties') and all( value in item.properties for value in ('app_label', 'model_name')): model = apps.get_model( app_label=item.properties['app_label'], model_name=item.properties['model_name']) klass = get_meta_node_class_for_model(model) serializer = NodeSerializer(instance=klass.inflate(item), many=False) response.append(serializer.data) except LookupError: # Might trigger if getting data originating from a different source. continue return Response(data=response, content_type='application/json')
def get_objects_for_user(user, permissions, klass=None, use_groups=True, extra_perms=None, any_perm=False, with_superuser=True): """ Returns a queryset of objects for which there can be calculated a path between the ``user`` using one or more access rules with *all* permissions present at ``permissions``. :param user: ``User`` instance for which objects should be returned. :param permissions: Single permission string, or sequence of permission strings that should be checked. If ``klass`` parameter is not given, those should be full permission strings rather than only codenames (ie. ``auth.change_user``). If more than one permission is present in the sequence, their content type **must** be the same or ``MixedContentTypeError`` would be raised. :param klass: May be a ``Model``, ``Manager`` or ``QuerySet`` object. If not given, this will be calculated based on passed ``permissions`` strings. :param use_groups: If ``True``, include users groups permissions. Defaults to ``True``. :param extra_perms: Single permission string, or sequence of permission strings that should be used as ``global_perms`` base. These permissions will be treated as if the user possesses them. :param any_perm: If ``True``, any permission in sequence is accepted. Defaults to ``False``. :param with_superuser: If ``True`` and ``user.is_superuser`` is set, returns the entire queryset. Otherwise will only return the objects the user has explicit permissions to. Defaults to ``True``. :raises MixedContentTypeError: If computed content type for ``permissions`` and/or ``klass`` clashes. :raises ValueError: If unable to compute content type for ``permissions``. :returns: QuerySet containing objects ``user`` has ``permissions`` to. """ # Make sure all permissions checks out! ctype, codenames = check_permissions_app_label(permissions) if extra_perms: extra_ctype, extra_perms = check_permissions_app_label(extra_perms) if extra_ctype != ctype: raise MixedContentTypeError( 'Calculated content type from keyword argument `extra_perms` ' '%s does not match %r.' % (extra_ctype, ctype)) extra_perms = extra_perms or set() if ctype is None and klass is not None: queryset = _get_queryset(klass) ctype = get_content_type(queryset.model) elif ctype is not None and klass is None: queryset = _get_queryset(ctype.model_class()) elif klass is None: raise ValueError('Could not determine the content type.') else: queryset = _get_queryset(klass) if ctype.model_class() != queryset.model: raise MixedContentTypeError( 'ContentType for given permissions and klass differs.') # Superusers have access to all objects. if with_superuser and user.is_superuser: return queryset # We don't support anonymous users. if user.is_anonymous: return queryset.none() # If there is no node in the graph for the user object, return empty queryset. source_node = get_node_class_for_model(user).nodes.get_or_none( **{'pk': user.pk}) if not source_node: return queryset.none() # Next, get all permissions the user has, either directly set through user permissions # or if ``use_groups`` are set, derived from a group membership. global_perms = extra_perms | set( get_perms(user, queryset.model ) if use_groups else get_user_perms(user, queryset.model)) # Check if we requires the user to have *all* permissions or if it is # sufficient with any provided. if not any_perm and not all((code in global_perms for code in codenames)): return queryset.none() elif any_perm: for code in codenames.copy(): if code not in global_perms: codenames.remove(code) # Calculate a PATH query for each rule queries = [] for access_rule in get_access_rules(get_content_type(user), ctype, codenames): manager = source_node.paths for n, rule_definition in enumerate(access_rule.relation_types_obj): relation_type, target_props = zip(*rule_definition.items()) relation_type, target_props = relation_type[0], target_props[0] source_props = {} if n == 0 and access_rule.requires_staff: source_props.update({'is_staff': True}) manager = manager.add(relation_type, source_props=source_props, target_props=target_props) if manager.statement: queries.append(manager.get_path()) q_values = Q() start_node_class = get_node_class_for_model(user) end_node_class = get_node_class_for_model(queryset.model) for query in queries: # FIXME: https://github.com/inonit/libcypher-parser-python/issues/1 # validate_cypher(query, raise_exception=True) result, _ = db.cypher_query(query) if result: values = set() for item in flatten(result): if not isinstance(item, Path): # pragma: no cover continue elif (start_node_class.__label__ not in item.start.labels or end_node_class.__label__ not in item.end.labels): continue try: start, end = (start_node_class(user).inflate(item.start), end_node_class.inflate(item.end)) if start == source_node and isinstance( end, end_node_class): values.add(item.end.properties['pk']) except (KeyError, InflateError): # pragma: no cover continue q_values |= Q(pk__in=values) # If no values in the Q filter, it means we couldn't get a path from the # user node to given object in queryset by any evaluated rule. # Return an empty queryset. if not q_values: return queryset.none() return queryset.filter(q_values)
def get_users_with_perms(obj, permissions, with_superusers=False, with_group_users=True): """ Returns a queryset of all ``User`` objects which there can be calculated a path from the given ``obj``. :param obj: model instance. :param permissions: Single permission string, or sequence of permissions strings that user requires to have. :param with_superusers: Default: ``False``. If set to ``True`` result would include all superusers. :param with_group_users: Default: ``True``. If set to ``False`` result would **not** include users which has only group permissions for given ``obj``. :raises MixedContentTypeError: If computed content type for ``permissions`` and/or ``obj`` clashes. :returns: Queryset containing ``User`` objects which has ``permissions`` for ``obj``. """ ctype, codenames = check_permissions_app_label(permissions) if ctype is None: ctype = get_content_type(obj) if codenames: # Make sure permissions are valid. _codenames = set( ctype.permission_set.filter( codename__in=codenames).values_list('codename', flat=True)) if not codenames == _codenames: message = ngettext_lazy( 'Calculated content type from permission "%s" does not match %r.' % (next(iter(codenames)), ctype), 'One or more permissions "%s" from calculated content type does not match %r.' % (', '.join(sorted(codenames)), ctype), len(codenames)) raise MixedContentTypeError(message) elif not ctype == get_content_type(obj): raise MixedContentTypeError( 'Calculated content type %r does not match %r.' % (ctype, get_content_type(obj))) queryset = _get_queryset(User) # If there is no node in the graph for ``obj``, return empty queryset. target_node = get_node_class_for_model(obj).nodes.get_or_none( **{'pk': obj.pk}) if not codenames or not target_node: if with_superusers is True: return queryset.filter(is_superuser=True) return queryset.none() ctype_source = get_content_type(User) # We need a fake source content type model to use as origin. fake_model = ctype_source.model_class()() source_node = get_node_for_object(fake_model, bind=False) queries = [] for access_rule in get_access_rules(ctype_source, ctype, codenames): manager = source_node.paths if access_rule.direction is not None: manager.direction = access_rule.direction for n, rule_definition in enumerate(access_rule.relation_types_obj): relation_type, target_props = zip(*rule_definition.items()) relation_type, target_props = relation_type[0], target_props[0] source_props = {} target_props = target_props or {} if n == 0 and access_rule.requires_staff: source_props.update({'is_staff': True}) # Make sure the last object in the query is matched to ``obj``. if n == len(access_rule.relation_types_obj) - 1: target_props['pk'] = target_node.pk # FIXME: Workaround for https://github.com/inonit/django-chemtrails/issues/46 # If using "{source}.<attr>" filters, ignore them! target_props = { key: value for key, value in target_props.items() if isinstance(value, str) and not value.startswith('{source}.') } manager = manager.add(relation_type, source_props=source_props, target_props=target_props) if manager.statement: queries.append(manager.get_path()) q_values = Q() if with_superusers is True: q_values |= Q(is_superuser=True) start_node_class = get_node_class_for_model(queryset.model) end_node_class = get_node_class_for_model(obj) for query in queries: # FIXME: https://github.com/inonit/libcypher-parser-python/issues/1 # validate_cypher(query, raise_exception=True) result, _ = db.cypher_query(query) if result: values = set() for item in flatten(result): if not isinstance(item, Path): # pragma: no cover continue elif (start_node_class.__label__ not in item.start.labels or end_node_class.__label__ not in item.end.labels): continue try: start, end = (start_node_class.inflate(item.start), end_node_class.inflate(item.end)) if isinstance(start, start_node_class) and end == target_node: # Make sure the user object has correct permissions instance = start.get_object() global_perms = set( get_perms(instance, obj) if with_group_users else get_user_perms(instance, obj)) if all((code in global_perms for code in codenames)): values.add(item.start.properties['pk']) except (KeyError, InflateError, ObjectDoesNotExist): continue q_values |= Q(pk__in=values) if not q_values: return queryset.none() return queryset.filter(q_values)