From c09b175c76bf87e164b1914c9968b7be84064efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Ta=C5=A1evski?= <36607228+BorisTasevski@users.noreply.github.com> Date: Thu, 8 Sep 2022 17:48:34 +0200 Subject: [PATCH] [E129-MG < T1006-MG] Expand C API with LBA checks (#527) * [T1006-MG < T1017-MG] Add LBA checks to all read procedures in C API (#515) * Initial Impl * NextPermittedEdge introduced * revert moving constructor to cpp * edge from and edge to methods expanded with lba check * minor fix * added check to path expand procedure * Added integration tests for read query procedures * additional check * changed iterator type to reference * comments from pr Co-authored-by: Josip Mrden * [T1006-MG < T1018-MG] Add LBA checks to all update procedures in C API (#516) * Initial Impl * NextPermittedEdge introduced * revert moving constructor to cpp * edge from and edge to methods expanded with lba check * minor fix * extended update methods * added check to path expand procedure * Added integration tests for read query procedures * Added integration tests for update query modules * additional check * changed iterator type to reference * fixed bug in Update property for node; fixed 2 e2e tests * replaced enum Co-authored-by: Josip Mrden * [T1006-MG < T1019-MG] Add LBA checks to all Create and Delete procedures in C API (#517) * Initial Impl * NextPermittedEdge introduced * revert moving constructor to cpp * edge from and edge to methods expanded with lba check * minor fix * extended update methods * initial implementation * added check to path expand procedure * Added integration tests for read query procedures * Added integration tests for update query modules * Added unit tests for creation of vertex, adding and removing vertex label * additional check * changed iterator type to reference * Added unit tests for create edge * Corrected query module in create edge * fixed bug in Update property for node; fixed 2 e2e tests * fixed merge errors * Expanded FineGrainedAuthChecker with HasGlobalPermissionOnVertices and HasGlobalPermissionOnEdges * Removed two wrong checks; Added two global checks * return null added * introduced new mgp_error value * fixed endless loop * replaced enum * intermediate * tests updated * PermissionDeniedError -> AuthorizationError rename * rename in enum permission_denied error -> authorization error * mgp_vertex_remove_label check improved * quotes changed; order of imports fixed * string constant introduced * import fixed * yaml format Co-authored-by: Josip Mrden Co-authored-by: Josip Mrden --- include/mg_procedure.h | 19 +- include/mgp.py | 361 +++++++++--------- release/mgp/_mgp.py | 4 + src/glue/auth_checker.cpp | 21 + src/glue/auth_checker.hpp | 7 +- src/query/auth_checker.hpp | 16 + src/query/procedure/mg_procedure_impl.cpp | 126 +++++- src/query/procedure/py_module.cpp | 6 + tests/e2e/lba_procedures/CMakeLists.txt | 5 +- tests/e2e/lba_procedures/common.py | 26 +- .../create_delete_query_modules.py | 299 +++++++++++++++ tests/e2e/lba_procedures/lba_procedures.py | 30 -- .../lba_procedures/procedures/CMakeLists.txt | 2 + .../procedures/create_delete.py | 63 +++ tests/e2e/lba_procedures/procedures/read.py | 10 + tests/e2e/lba_procedures/procedures/update.py | 21 + .../lba_procedures/read_permission_queries.py | 42 +- .../e2e/lba_procedures/read_query_modules.py | 225 +++++++++++ .../lba_procedures/update_query_modules.py | 184 +++++++++ tests/e2e/lba_procedures/workloads.yaml | 63 ++- 20 files changed, 1271 insertions(+), 259 deletions(-) create mode 100644 tests/e2e/lba_procedures/create_delete_query_modules.py delete mode 100644 tests/e2e/lba_procedures/lba_procedures.py create mode 100644 tests/e2e/lba_procedures/procedures/create_delete.py create mode 100644 tests/e2e/lba_procedures/procedures/update.py create mode 100644 tests/e2e/lba_procedures/read_query_modules.py create mode 100644 tests/e2e/lba_procedures/update_query_modules.py diff --git a/include/mg_procedure.h b/include/mg_procedure.h index e5c6faa3e..678ea5d9b 100644 --- a/include/mg_procedure.h +++ b/include/mg_procedure.h @@ -37,12 +37,19 @@ extern "C" { /// All functions return an error code that can be used to figure out whether the API call was successful or not. In /// case of failure, the specific error code can be used to identify the reason of the failure. MGP_ENUM_CLASS MGP_NODISCARD mgp_error{ - MGP_ERROR_NO_ERROR, MGP_ERROR_UNKNOWN_ERROR, - MGP_ERROR_UNABLE_TO_ALLOCATE, MGP_ERROR_INSUFFICIENT_BUFFER, - MGP_ERROR_OUT_OF_RANGE, MGP_ERROR_LOGIC_ERROR, - MGP_ERROR_DELETED_OBJECT, MGP_ERROR_INVALID_ARGUMENT, - MGP_ERROR_KEY_ALREADY_EXISTS, MGP_ERROR_IMMUTABLE_OBJECT, - MGP_ERROR_VALUE_CONVERSION, MGP_ERROR_SERIALIZATION_ERROR, + MGP_ERROR_NO_ERROR, + MGP_ERROR_UNKNOWN_ERROR, + MGP_ERROR_UNABLE_TO_ALLOCATE, + MGP_ERROR_INSUFFICIENT_BUFFER, + MGP_ERROR_OUT_OF_RANGE, + MGP_ERROR_LOGIC_ERROR, + MGP_ERROR_DELETED_OBJECT, + MGP_ERROR_INVALID_ARGUMENT, + MGP_ERROR_KEY_ALREADY_EXISTS, + MGP_ERROR_IMMUTABLE_OBJECT, + MGP_ERROR_VALUE_CONVERSION, + MGP_ERROR_SERIALIZATION_ERROR, + MGP_ERROR_AUTHORIZATION_ERROR, }; ///@} diff --git a/include/mgp.py b/include/mgp.py index 1e93a392b..6cef5a451 100644 --- a/include/mgp.py +++ b/include/mgp.py @@ -134,6 +134,15 @@ class SerializationError(_mgp.SerializationError): pass +class AuthorizationError(_mgp.AuthorizationError): + """ + Signals that the user doesn't have sufficient permissions to perform + procedure call. + """ + + pass + + class Label: """Label of a `Vertex`.""" @@ -146,7 +155,7 @@ class Label: def name(self) -> str: """ Get the name of the label. - + Returns: A string that represents the name of the label. @@ -195,20 +204,20 @@ class Properties: def get(self, property_name: str, default=None) -> object: """ Get the value of a property with the given name or return default value. - - Args: + + Args: property_name: String that represents property name. default: Default value return if there is no property. - - Returns: - Any object value that property under `property_name` has or default value otherwise. - - Raises: - InvalidContextError: If `edge` or `vertex` is out of context. + + Returns: + Any object value that property under `property_name` has or default value otherwise. + + Raises: + InvalidContextError: If `edge` or `vertex` is out of context. UnableToAllocateError: If unable to allocate a `mgp.Value`. DeletedObjectError: If the `object` has been deleted. - - Examples: + + Examples: ``` vertex.properties.get(property_name) edge.properties.get(property_name) @@ -227,23 +236,23 @@ class Properties: Set the value of the property. When the value is `None`, then the property is removed. - Args: - property_name: String that represents property name. + Args: + property_name: String that represents property name. value: Object that represents value to be set. - Raises: + Raises: UnableToAllocateError: If unable to allocate memory for storing the property. ImmutableObjectError: If the object is immutable. DeletedObjectError: If the object has been deleted. SerializationError: If the object has been modified by another transaction. ValueConversionError: If `value` is vertex, edge or path. - + Examples: ``` vertex.properties.set(property_name, value) edge.properties.set(property_name, value) ``` - + """ self[property_name] = value @@ -252,15 +261,15 @@ class Properties: Iterate over the properties. Doesn’t return a dynamic view of the properties but copies the current properties. - Returns: + Returns: Iterable `Property` of names and values. - Raises: - InvalidContextError: If edge or vertex is out of context. + Raises: + InvalidContextError: If edge or vertex is out of context. UnableToAllocateError: If unable to allocate an iterator. DeletedObjectError: If the object has been deleted. - Examples: + Examples: ``` items = vertex.properties.items() for it in items: @@ -290,15 +299,15 @@ class Properties: Iterate over property names. Doesn’t return a dynamic view of the property names but copies the name of the current properties. - Returns: + Returns: Iterable list of strings that represent names/keys of properties. - Raises: + Raises: InvalidContextError: If edge or vertex is out of context. UnableToAllocateError: If unable to allocate an iterator. DeletedObjectError: If the object has been deleted. - Examples: + Examples: ``` graph.vertex.properties.keys() graph.edge.properties.keys() @@ -314,20 +323,20 @@ class Properties: Iterate over property values. Doesn’t return a dynamic view of the property values but copies the value of the current properties. - Returns: + Returns: Iterable list of property values. - Raises: + Raises: InvalidContextError: If edge or vertex is out of context. UnableToAllocateError: If unable to allocate an iterator. DeletedObjectError: If the object has been deleted. - Examples: + Examples: ``` vertex.properties.values() edge.properties.values() ``` - + """ if not self._vertex_or_edge.is_valid(): raise InvalidContextError() @@ -338,15 +347,15 @@ class Properties: """ Get the number of properties. - Returns: + Returns: A number of properties on vertex or edge. - Raises: + Raises: InvalidContextError: If edge or vertex is out of context. UnableToAllocateError: If unable to allocate an iterator. DeletedObjectError: If the object has been deleted. - - Examples: + + Examples: ``` len(vertex.properties) len(edge.properties) @@ -363,15 +372,15 @@ class Properties: """ Iterate over property names. - Returns: + Returns: Iterable list of strings that represent names of properties. - - Raises: + + Raises: InvalidContextError: If edge or vertex is out of context. UnableToAllocateError: If unable to allocate an iterator. DeletedObjectError: If the object has been deleted. - - Examples: + + Examples: ``` iter(vertex.properties) iter(edge.properties) @@ -386,24 +395,24 @@ class Properties: def __getitem__(self, property_name: str) -> object: """ Get the value of a property with the given name or raise KeyError. - - Args: + + Args: property_name: String that represents property name. - Returns: + Returns: Any value that property under property_name have. - Raises: + Raises: InvalidContextError: If edge or vertex is out of context. UnableToAllocateError: If unable to allocate a mgp.Value. DeletedObjectError: If the object has been deleted. - Examples: + Examples: ``` vertex.properties[property_name] edge.properties[property_name] ``` - + """ if not self._vertex_or_edge.is_valid(): raise InvalidContextError() @@ -417,18 +426,18 @@ class Properties: Set the value of the property. When the value is `None`, then the property is removed. - Args: + Args: property_name: String that represents property name. value: Object that represents value to be set. - + Raises: UnableToAllocateError: If unable to allocate memory for storing the property. ImmutableObjectError: If the object is immutable. DeletedObjectError: If the object has been deleted. SerializationError: If the object has been modified by another transaction. ValueConversionError: If `value` is vertex, edge or path. - - Examples: + + Examples: ``` vertex.properties[property_name] = value edge.properties[property_name] = value @@ -443,18 +452,18 @@ class Properties: """ Check if there is a property with the given name. - Args: + Args: property_name: String that represents property name - + Returns: - Bool value that depends if there is with a given name. - - Raises: + Bool value that depends if there is with a given name. + + Raises: InvalidContextError: If edge or vertex is out of context. UnableToAllocateError: If unable to allocate a mgp.Value. DeletedObjectError: If the object has been deleted. - - Examples: + + Examples: ``` if property_name in vertex.properties: ``` @@ -483,7 +492,7 @@ class EdgeType: def name(self) -> str: """ Get the name of EdgeType. - + Returns: A string that represents the name of EdgeType. @@ -512,7 +521,7 @@ class Edge: Access to an Edge is only valid during a single execution of a procedure in a query. You should not globally store an instance of an Edge. Using an invalid Edge instance will raise InvalidContextError. - + """ __slots__ = ("_edge",) @@ -532,10 +541,10 @@ class Edge: def is_valid(self) -> bool: """ Check if `edge` is in a valid context and may be used. - + Returns: - A `bool` value depends on if the `edge` is in a valid context. - + A `bool` value depends on if the `edge` is in a valid context. + Examples: ```edge.is_valid()``` @@ -543,15 +552,15 @@ class Edge: return self._edge.is_valid() def underlying_graph_is_mutable(self) -> bool: - """ - Check if the `graph` can be modified. + """ + Check if the `graph` can be modified. - Returns: + Returns: A `bool` value depends on if the `graph` is mutable. - - Examples: + + Examples: ```edge.underlying_graph_is_mutable()``` - + """ if not self.is_valid(): raise InvalidContextError() @@ -564,10 +573,10 @@ class Edge: Returns: `EdgeId` represents ID of the edge. - - Raises: + + Raises: InvalidContextError: If edge is out of context. - + Examples: ```edge.id``` """ @@ -581,12 +590,12 @@ class Edge: Get the type of edge. Returns: - `EdgeType` describing the type of edge. + `EdgeType` describing the type of edge. Raises: InvalidContextError: If edge is out of context. - - Examples: + + Examples: ```edge.type``` """ if not self.is_valid(): @@ -598,10 +607,10 @@ class Edge: """ Get the source vertex. - Returns: + Returns: `Vertex` from where the edge is directed. - Raises: + Raises: InvalidContextError: If edge is out of context. Examples: @@ -615,14 +624,14 @@ class Edge: def to_vertex(self) -> "Vertex": """ Get the destination vertex. - - Returns: + + Returns: `Vertex` to where the edge is directed. - - Raises: + + Raises: InvalidContextError: If edge is out of context. - Examples: + Examples: ```edge.to_vertex``` """ if not self.is_valid(): @@ -635,7 +644,7 @@ class Edge: Get the properties of the edge. Returns: - All `Properties` of edge. + All `Properties` of edge. Raises: InvalidContextError: If edge is out of context. @@ -692,9 +701,9 @@ class Vertex: Checks if `Vertex` is in valid context and may be used. Returns: - A `bool` value depends on if the `Vertex` is in a valid context. - - Examples: + A `bool` value depends on if the `Vertex` is in a valid context. + + Examples: ```vertex.is_valid()``` """ @@ -702,14 +711,14 @@ class Vertex: def underlying_graph_is_mutable(self) -> bool: """ - Check if the `graph` is mutable. + Check if the `graph` is mutable. - Returns: + Returns: A `bool` value depends on if the `graph` is mutable. - - Examples: + + Examples: ```vertex.underlying_graph_is_mutable()``` - + """ if not self.is_valid(): raise InvalidContextError() @@ -722,10 +731,10 @@ class Vertex: Returns: `VertexId` represents ID of the vertex. - - Raises: + + Raises: InvalidContextError: If vertex is out of context. - + Examples: ```vertex.id``` """ @@ -738,15 +747,15 @@ class Vertex: """ Get the labels of the vertex. - Returns: + Returns: A tuple of `Label` representing vertex Labels - Raises: + Raises: InvalidContextError: If vertex is out of context. OutOfRangeError: If some of the labels are removed while collecting the labels. DeletedObjectError: If `Vertex` has been deleted. - - Examples: + + Examples: ```vertex.labels``` """ if not self.is_valid(): @@ -757,17 +766,17 @@ class Vertex: """ Add the label to the vertex. - Args: - label: String label to be added. - - Raises: + Args: + label: String label to be added. + + Raises: InvalidContextError: If `Vertex` is out of context. UnableToAllocateError: If unable to allocate memory for storing the label. ImmutableObjectError: If `Vertex` is immutable. DeletedObjectError: If `Vertex` has been deleted. SerializationError: If `Vertex` has been modified by another transaction. - - Examples: + + Examples: ```vertex.add_label(label)``` """ if not self.is_valid(): @@ -778,15 +787,15 @@ class Vertex: """ Remove the label from the vertex. - Args: - label: String label to be deleted - Raises: + Args: + label: String label to be deleted + Raises: InvalidContextError: If `Vertex` is out of context. ImmutableObjectError: If `Vertex` is immutable. DeletedObjectError: If `Vertex` has been deleted. SerializationError: If `Vertex` has been modified by another transaction. - - Examples: + + Examples: ```vertex.remove_label(label)``` """ if not self.is_valid(): @@ -798,13 +807,13 @@ class Vertex: """ Get the properties of the vertex. - Returns: + Returns: `Properties` on a current vertex. - Raises: + Raises: InvalidContextError: If `Vertex` is out of context. - Examples: + Examples: ```vertex.properties``` """ if not self.is_valid(): @@ -820,13 +829,13 @@ class Vertex: Returns: Iterable list of `Edge` objects that are directed in towards the current vertex. - - Raises: + + Raises: InvalidContextError: If `Vertex` is out of context. UnableToAllocateError: If unable to allocate an iterator. DeletedObjectError: If `Vertex` has been deleted. - Examples: + Examples: ```for edge in vertex.in_edges:``` """ if not self.is_valid(): @@ -850,12 +859,12 @@ class Vertex: Returns: Iterable list of `Edge` objects that are directed out of the current vertex. - Raises: + Raises: InvalidContextError: If `Vertex` is out of context. UnableToAllocateError: If unable to allocate an iterator. DeletedObjectError: If `Vertex` has been deleted. - Examples: + Examples: ```for edge in vertex.out_edges:``` """ if not self.is_valid(): @@ -888,7 +897,7 @@ class Path: def __init__(self, starting_vertex_or_path: typing.Union[_mgp.Path, Vertex]): """Initialize with a starting Vertex. - Raises: + Raises: InvalidContextError: If passed in Vertex is invalid. UnableToAllocateError: If cannot allocate a path. """ @@ -932,10 +941,10 @@ class Path: def is_valid(self) -> bool: """ Check if `Path` is in valid context and may be used. - + Returns: - A `bool` value depends on if the `Path` is in a valid context. - + A `bool` value depends on if the `Path` is in a valid context. + Examples: ```path.is_valid()``` """ @@ -948,15 +957,15 @@ class Path: The last vertex on the path will become the other endpoint of the given edge, as continued from the current last vertex. - Args: + Args: edge: `Edge` that is added to the path - Raises: + Raises: InvalidContextError: If using an invalid `Path` instance or if passed in `Edge` is invalid. LogicErrorError: If the current last vertex in the path is not part of the given edge. UnableToAllocateError: If unable to allocate memory for path extension. - Examples: + Examples: ```path.expand(edge)``` """ if not isinstance(edge, Edge): @@ -973,14 +982,14 @@ class Path: """ Vertices are ordered from the start to the end of the path. - Returns: - A tuple of `Vertex` objects order from start to end of the path. + Returns: + A tuple of `Vertex` objects order from start to end of the path. Raises: InvalidContextError: If using an invalid Path instance. Examples: - ```path.vertices``` + ```path.vertices``` """ if not self.is_valid(): raise InvalidContextError() @@ -994,11 +1003,11 @@ class Path: """ Edges are ordered from the start to the end of the path. - Returns: + Returns: A tuple of `Edge` objects order from start to end of the path - Raises: + Raises: InvalidContextError: If using an invalid `Path` instance. - Examples: + Examples: ```path.edges``` """ if not self.is_valid(): @@ -1039,10 +1048,10 @@ class Vertices: def is_valid(self) -> bool: """ Check if `Vertices` is in valid context and may be used. - + Returns: - A `bool` value depends on if the `Vertices` is in valid context. - + A `bool` value depends on if the `Vertices` is in valid context. + Examples: ```vertices.is_valid()``` """ @@ -1052,14 +1061,14 @@ class Vertices: """ Iterate over vertices. - Returns: - Iterable list of `Vertex` objects. + Returns: + Iterable list of `Vertex` objects. - Raises: + Raises: InvalidContextError: If context is invalid. UnableToAllocateError: If unable to allocate an iterator or a vertex. - Examples: + Examples: ``` for vertex in graph.vertices: ``` @@ -1080,18 +1089,18 @@ class Vertices: def __contains__(self, vertex): """ - Check if Vertices contain the given vertex. + Check if Vertices contain the given vertex. - Args: + Args: vertex: `Vertex` to be checked if it is a part of graph `Vertices`. Returns: - Bool value depends if there is `Vertex` in graph `Vertices`. + Bool value depends if there is `Vertex` in graph `Vertices`. Raises: UnableToAllocateError: If unable to allocate the vertex. - Examples: + Examples: ```if vertex in graph.vertices:``` """ try: @@ -1104,14 +1113,14 @@ class Vertices: """ Get the number of vertices. - Returns: + Returns: A number of vertices in the graph. - - Raises: + + Raises: InvalidContextError: If context is invalid. UnableToAllocateError: If unable to allocate an iterator or a vertex. - - Examples: + + Examples: ```len(graph.vertices)``` """ if not self._len: @@ -1140,9 +1149,9 @@ class Graph: Check if `graph` is in a valid context and may be used. Returns: - A `bool` value depends on if the `graph` is in a valid context. - - Examples: + A `bool` value depends on if the `graph` is in a valid context. + + Examples: ```graph.is_valid()``` """ @@ -1169,7 +1178,7 @@ class Graph: Examples: ```graph.get_vertex_by_id(vertex_id)``` - + """ if not self.is_valid(): raise InvalidContextError() @@ -1207,11 +1216,11 @@ class Graph: def is_mutable(self) -> bool: """ Check if the graph is mutable. Thus it can be used to modify vertices and edges. - - Returns: - A `bool` value that depends if the graph is mutable or not. - Examples: + Returns: + A `bool` value that depends if the graph is mutable or not. + + Examples: ```graph.is_mutable()``` """ if not self.is_valid(): @@ -1222,14 +1231,14 @@ class Graph: """ Create an empty vertex. - Returns: - Created `Vertex`. + Returns: + Created `Vertex`. - Raises: + Raises: ImmutableObjectError: If `graph` is immutable. UnableToAllocateError: If unable to allocate a vertex. - - Examples: + + Examples: Creating an empty vertex. ```vertex = graph.create_vertex()``` @@ -1249,7 +1258,7 @@ class Graph: LogicErrorError: If `vertex` has edges. SerializationError: If `vertex` has been modified by another transaction. - Examples: + Examples: ```graph.delete_vertex(vertex)``` """ @@ -1260,14 +1269,14 @@ class Graph: def detach_delete_vertex(self, vertex: Vertex) -> None: """ Delete a vertex and all of its edges. - - Args: + + Args: vertex: `Vertex` to be deleted with all of its edges - - Raises: + + Raises: ImmutableObjectError: If `graph` is immutable. SerializationError: If `vertex` has been modified by another transaction. - Examples: + Examples: ```graph.detach_delete_vertex(vertex)``` """ if not self.is_valid(): @@ -1277,18 +1286,18 @@ class Graph: def create_edge(self, from_vertex: Vertex, to_vertex: Vertex, edge_type: EdgeType) -> None: """ Create an edge. - - Args: - from_vertex: `Vertex` from where edge is directed. - to_vertex: `Vertex' to where edge is directed. - edge_type: `EdgeType` defines the type of edge. + + Args: + from_vertex: `Vertex` from where edge is directed. + to_vertex: `Vertex' to where edge is directed. + edge_type: `EdgeType` defines the type of edge. Raises: ImmutableObjectError: If `graph` is immutable. UnableToAllocateError: If unable to allocate an edge. DeletedObjectError: If `from_vertex` or `to_vertex` has been deleted. SerializationError: If `from_vertex` or `to_vertex` has been modified by another transaction. - Examples: + Examples: ```graph.create_edge(from_vertex, vertex, edge_type)``` """ if not self.is_valid(): @@ -1301,8 +1310,8 @@ class Graph: Args: edge: `Edge` to be deleted - - Raises: + + Raises: ImmutableObjectError if `graph` is immutable. Raise SerializationError if `edge`, its source or destination vertex has been modified by another transaction. """ @@ -1337,15 +1346,15 @@ class ProcCtx: @property def graph(self) -> Graph: """ - Access to `Graph` object. - - Returns: - Graph object. + Access to `Graph` object. - Raises: + Returns: + Graph object. + + Raises: InvalidContextError: If context is invalid. - - Examples: + + Examples: ```context.graph``` """ if not self.is_valid(): @@ -1969,6 +1978,8 @@ def _wrap_exceptions(): raise ValueConversionError(e) except _mgp.SerializationError as e: raise SerializationError(e) + except _mgp.AuthorizationError as e: + raise AuthorizationError(e) return wrapped_func diff --git a/release/mgp/_mgp.py b/release/mgp/_mgp.py index 435fac9fd..34cf96263 100644 --- a/release/mgp/_mgp.py +++ b/release/mgp/_mgp.py @@ -173,6 +173,10 @@ class SerializationError(Exception): pass +class AuthorizationError(Exception): + pass + + def type_nullable(elem: Any): pass diff --git a/src/glue/auth_checker.cpp b/src/glue/auth_checker.cpp index 0d56c2f3c..cd66f6146 100644 --- a/src/glue/auth_checker.cpp +++ b/src/glue/auth_checker.cpp @@ -28,6 +28,18 @@ bool IsUserAuthorizedLabels(const memgraph::auth::User &user, const memgraph::qu }); } +bool IsUserAuthorizedGloballyLabels(const memgraph::auth::User &user, + const memgraph::auth::FineGrainedPermission fine_grained_permission) { + return user.GetFineGrainedAccessLabelPermissions().Has(memgraph::auth::kAsterisk, fine_grained_permission) == + memgraph::auth::PermissionLevel::GRANT; +} + +bool IsUserAuthorizedGloballyEdges(const memgraph::auth::User &user, + const memgraph::auth::FineGrainedPermission fine_grained_permission) { + return user.GetFineGrainedAccessEdgeTypePermissions().Has(memgraph::auth::kAsterisk, fine_grained_permission) == + memgraph::auth::PermissionLevel::GRANT; +} + bool IsUserAuthorizedEdgeType(const memgraph::auth::User &user, const memgraph::query::DbAccessor &dba, const memgraph::storage::EdgeTypeId &edgeType, const memgraph::query::AuthQuery::FineGrainedPrivilege fine_grained_permission) { @@ -125,4 +137,13 @@ bool FineGrainedAuthChecker::Accept( return IsUserAuthorizedEdgeType(user_, dba, edge_type, fine_grained_permission); } +bool FineGrainedAuthChecker::HasGlobalPermissionOnVertices( + const memgraph::query::AuthQuery::FineGrainedPrivilege fine_grained_privilege) const { + return IsUserAuthorizedGloballyLabels(user_, FineGrainedPrivilegeToFineGrainedPermission(fine_grained_privilege)); +} + +bool FineGrainedAuthChecker::HasGlobalPermissionOnEdges( + const memgraph::query::AuthQuery::FineGrainedPrivilege fine_grained_privilege) const { + return IsUserAuthorizedGloballyEdges(user_, FineGrainedPrivilegeToFineGrainedPermission(fine_grained_privilege)); +}; } // namespace memgraph::glue diff --git a/src/glue/auth_checker.hpp b/src/glue/auth_checker.hpp index e0be005ba..f80d0f441 100644 --- a/src/glue/auth_checker.hpp +++ b/src/glue/auth_checker.hpp @@ -12,7 +12,6 @@ #pragma once #include "auth/auth.hpp" -#include "auth/models.hpp" #include "glue/auth.hpp" #include "query/auth_checker.hpp" #include "query/db_accessor.hpp" @@ -54,6 +53,12 @@ class FineGrainedAuthChecker : public query::FineGrainedAuthChecker { bool Accept(const memgraph::query::DbAccessor &dba, const memgraph::storage::EdgeTypeId &edge_type, query::AuthQuery::FineGrainedPrivilege fine_grained_permission) const override; + bool HasGlobalPermissionOnVertices( + memgraph::query::AuthQuery::FineGrainedPrivilege fine_grained_privilege) const override; + + bool HasGlobalPermissionOnEdges( + memgraph::query::AuthQuery::FineGrainedPrivilege fine_grained_privilege) const override; + private: auth::User user_; }; diff --git a/src/query/auth_checker.hpp b/src/query/auth_checker.hpp index b7c463482..703406569 100644 --- a/src/query/auth_checker.hpp +++ b/src/query/auth_checker.hpp @@ -48,6 +48,12 @@ class FineGrainedAuthChecker { [[nodiscard]] virtual bool Accept(const memgraph::query::DbAccessor &dba, const memgraph::storage::EdgeTypeId &edge_type, query::AuthQuery::FineGrainedPrivilege fine_grained_permission) const = 0; + + [[nodiscard]] virtual bool HasGlobalPermissionOnVertices( + memgraph::query::AuthQuery::FineGrainedPrivilege fine_grained_privilege) const = 0; + + [[nodiscard]] virtual bool HasGlobalPermissionOnEdges( + memgraph::query::AuthQuery::FineGrainedPrivilege fine_grained_privilege) const = 0; }; class AllowEverythingFineGrainedAuthChecker final : public query::FineGrainedAuthChecker { @@ -71,6 +77,16 @@ class AllowEverythingFineGrainedAuthChecker final : public query::FineGrainedAut const query::AuthQuery::FineGrainedPrivilege fine_grained_permission) const override { return true; } + + bool HasGlobalPermissionOnVertices( + const memgraph::query::AuthQuery::FineGrainedPrivilege /*fine_grained_privilege*/) const override { + return true; + } + + bool HasGlobalPermissionOnEdges( + const memgraph::query::AuthQuery::FineGrainedPrivilege /*fine_grained_privilege*/) const override { + return true; + } }; // namespace memgraph::query class AllowEverythingAuthChecker final : public query::AuthChecker { diff --git a/src/query/procedure/mg_procedure_impl.cpp b/src/query/procedure/mg_procedure_impl.cpp index e1c0fa611..25c809e5b 100644 --- a/src/query/procedure/mg_procedure_impl.cpp +++ b/src/query/procedure/mg_procedure_impl.cpp @@ -120,6 +120,10 @@ struct SerializationException : public memgraph::utils::BasicException { using memgraph::utils::BasicException::BasicException; }; +struct AuthorizationException : public memgraph::utils::BasicException { + using memgraph::utils::BasicException::BasicException; +}; + template concept ReturnsType = std::same_as, TReturn>; @@ -160,6 +164,9 @@ template } catch (const SerializationException &se) { spdlog::error("Serialization error during mg API call: {}", se.what()); return mgp_error::MGP_ERROR_SERIALIZATION_ERROR; + } catch (const AuthorizationException &ae) { + spdlog::error("Authorization error during mg API call: {}", ae.what()); + return mgp_error::MGP_ERROR_AUTHORIZATION_ERROR; } catch (const std::bad_alloc &bae) { spdlog::error("Memory allocation error during mg API call: {}", bae.what()); return mgp_error::MGP_ERROR_UNABLE_TO_ALLOCATE; @@ -1066,6 +1073,7 @@ mgp_error mgp_path_expand(mgp_path *path, mgp_edge *edge) { // the given edge. auto *src_vertex = &path->vertices.back(); mgp_vertex *dst_vertex{nullptr}; + if (edge->to == *src_vertex) { dst_vertex = &edge->from; } else if (edge->from == *src_vertex) { @@ -1579,9 +1587,16 @@ memgraph::storage::PropertyValue ToPropertyValue(const mgp_value &value) { mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *property_name, mgp_value *property_value) { return WrapExceptions([=] { + if (v->graph->ctx && v->graph->ctx->auth_checker && + !v->graph->ctx->auth_checker->Accept(*v->graph->ctx->db_accessor, v->impl, v->graph->view, + memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE)) { + throw AuthorizationException{"Insufficient permissions for setting a property on vertex!"}; + } + if (!MgpVertexIsMutable(*v)) { throw ImmutableObjectException{"Cannot set a property on an immutable vertex!"}; } + const auto prop_key = v->graph->impl->NameToProperty(property_name); const auto result = v->impl.SetProperty(prop_key, ToPropertyValue(*property_value)); if (result.HasError()) { @@ -1619,6 +1634,14 @@ mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *property_nam mgp_error mgp_vertex_add_label(struct mgp_vertex *v, mgp_label label) { return WrapExceptions([=] { + if (v->graph->ctx && v->graph->ctx->auth_checker && + !(v->graph->ctx->auth_checker->Accept(*v->graph->ctx->db_accessor, v->impl, v->graph->view, + memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE) && + v->graph->ctx->auth_checker->Accept(*v->graph->ctx->db_accessor, {v->graph->impl->NameToLabel(label.name)}, + memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE))) { + throw AuthorizationException{"Insufficient permissions for adding a label to vertex!"}; + } + if (!MgpVertexIsMutable(*v)) { throw ImmutableObjectException{"Cannot add a label to an immutable vertex!"}; } @@ -1651,6 +1674,14 @@ mgp_error mgp_vertex_add_label(struct mgp_vertex *v, mgp_label label) { mgp_error mgp_vertex_remove_label(struct mgp_vertex *v, mgp_label label) { return WrapExceptions([=] { + if (v->graph->ctx && v->graph->ctx->auth_checker && + !(v->graph->ctx->auth_checker->Accept(*v->graph->ctx->db_accessor, v->impl, v->graph->view, + memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE) && + v->graph->ctx->auth_checker->Accept(*v->graph->ctx->db_accessor, {v->graph->impl->NameToLabel(label.name)}, + memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE))) { + throw AuthorizationException{"Insufficient permissions for removing a label from vertex!"}; + } + if (!MgpVertexIsMutable(*v)) { throw ImmutableObjectException{"Cannot remove a label from an immutable vertex!"}; } @@ -1828,6 +1859,32 @@ mgp_error mgp_vertex_iter_properties(mgp_vertex *v, mgp_memory *memory, mgp_prop void mgp_edges_iterator_destroy(mgp_edges_iterator *it) { DeleteRawMgpObject(it); } +namespace { +void NextPermittedEdge(mgp_edges_iterator &it, const bool for_in) { + if (!it.source_vertex.graph->ctx || !it.source_vertex.graph->ctx->auth_checker) return; + + auto &impl_it = for_in ? it.in_it : it.out_it; + const auto end = for_in ? it.in->end() : it.out->end(); + + if (impl_it) { + const auto *auth_checker = it.source_vertex.graph->ctx->auth_checker.get(); + const auto db_accessor = *it.source_vertex.graph->ctx->db_accessor; + const auto view = it.source_vertex.graph->view; + while (*impl_it != end) { + if (auth_checker->Accept(db_accessor, **impl_it, memgraph::query::AuthQuery::FineGrainedPrivilege::READ)) { + const auto &check_vertex = it.source_vertex.impl == (*impl_it)->From() ? (*impl_it)->To() : (*impl_it)->From(); + if (auth_checker->Accept(db_accessor, check_vertex, view, + memgraph::query::AuthQuery::FineGrainedPrivilege::READ)) { + break; + } + } + + ++*impl_it; + } + } +}; +} // namespace + mgp_error mgp_vertex_iter_in_edges(mgp_vertex *v, mgp_memory *memory, mgp_edges_iterator **result) { return WrapExceptions( [v, memory] { @@ -1851,6 +1908,9 @@ mgp_error mgp_vertex_iter_in_edges(mgp_vertex *v, mgp_memory *memory, mgp_edges_ } it->in.emplace(std::move(*maybe_edges)); it->in_it.emplace(it->in->begin()); + + NextPermittedEdge(*it, true); + if (*it->in_it != it->in->end()) { it->current_e.emplace(**it->in_it, v->graph, it->GetMemoryResource()); } @@ -1883,6 +1943,9 @@ mgp_error mgp_vertex_iter_out_edges(mgp_vertex *v, mgp_memory *memory, mgp_edges } it->out.emplace(std::move(*maybe_edges)); it->out_it.emplace(it->out->begin()); + + NextPermittedEdge(*it, false); + if (*it->out_it != it->out->end()) { it->current_e.emplace(**it->out_it, v->graph, it->GetMemoryResource()); } @@ -1911,24 +1974,35 @@ mgp_error mgp_edges_iterator_next(mgp_edges_iterator *it, mgp_edge **result) { return WrapExceptions( [it] { MG_ASSERT(it->in || it->out); - auto next = [&](auto *impl_it, const auto &end) -> mgp_edge * { + auto next = [it](const bool for_in) -> mgp_edge * { + auto &impl_it = for_in ? it->in_it : it->out_it; + const auto end = for_in ? it->in->end() : it->out->end(); if (*impl_it == end) { MG_ASSERT(!it->current_e, "Iteration is already done, so it->current_e " "should have been set to std::nullopt"); return nullptr; } - if (++(*impl_it) == end) { + + ++*impl_it; + + NextPermittedEdge(*it, for_in); + + if (*impl_it == end) { it->current_e = std::nullopt; return nullptr; } + it->current_e.emplace(**impl_it, it->source_vertex.graph, it->GetMemoryResource()); return &*it->current_e; }; if (it->in_it) { - return next(&*it->in_it, it->in->end()); + auto *result = next(true); + if (result != nullptr) { + return result; + } } - return next(&*it->out_it, it->out->end()); + return next(false); }, result); } @@ -2002,6 +2076,12 @@ mgp_error mgp_edge_get_property(mgp_edge *e, const char *name, mgp_memory *memor mgp_error mgp_edge_set_property(struct mgp_edge *e, const char *property_name, mgp_value *property_value) { return WrapExceptions([=] { + if (e->from.graph->ctx && e->from.graph->ctx->auth_checker && + !e->from.graph->ctx->auth_checker->Accept(*e->from.graph->ctx->db_accessor, e->impl, + memgraph::query::AuthQuery::FineGrainedPrivilege::UPDATE)) { + throw AuthorizationException{"Insufficient permissions for setting a property on edge!"}; + } + if (!MgpEdgeIsMutable(*e)) { throw ImmutableObjectException{"Cannot set a property on an immutable edge!"}; } @@ -2057,7 +2137,8 @@ mgp_error mgp_edge_iter_properties(mgp_edge *e, mgp_memory *memory, mgp_properti throw DeletedObjectException{"Cannot get the properties of a deleted edge!"}; case memgraph::storage::Error::NONEXISTENT_OBJECT: LOG_FATAL( - "Query modules shouldn't have access to nonexistent objects when getting the properties of an edge."); + "Query modules shouldn't have access to nonexistent objects when getting the properties of an " + "edge."); case memgraph::storage::Error::PROPERTIES_DISABLED: case memgraph::storage::Error::VERTEX_HAS_EDGES: case memgraph::storage::Error::SERIALIZATION_ERROR: @@ -2088,7 +2169,13 @@ mgp_error mgp_graph_is_mutable(mgp_graph *graph, int *result) { mgp_error mgp_graph_create_vertex(struct mgp_graph *graph, mgp_memory *memory, mgp_vertex **result) { return WrapExceptions( - [=] { + [=]() -> mgp_vertex * { + if (graph->ctx && graph->ctx->auth_checker && + !graph->ctx->auth_checker->HasGlobalPermissionOnVertices( + memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) { + throw AuthorizationException{"Insufficient permissions for creating vertices!"}; + } + if (!MgpGraphIsMutable(*graph)) { throw ImmutableObjectException{"Cannot create a vertex in an immutable graph!"}; } @@ -2107,6 +2194,12 @@ mgp_error mgp_graph_create_vertex(struct mgp_graph *graph, mgp_memory *memory, m mgp_error mgp_graph_delete_vertex(struct mgp_graph *graph, mgp_vertex *vertex) { return WrapExceptions([=] { + if (graph->ctx && graph->ctx->auth_checker && + !graph->ctx->auth_checker->Accept(*graph->ctx->db_accessor, vertex->impl, graph->view, + memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) { + throw AuthorizationException{"Insufficient permissions for deleting a vertex!"}; + } + if (!MgpGraphIsMutable(*graph)) { throw ImmutableObjectException{"Cannot remove a vertex from an immutable graph!"}; } @@ -2142,6 +2235,12 @@ mgp_error mgp_graph_delete_vertex(struct mgp_graph *graph, mgp_vertex *vertex) { mgp_error mgp_graph_detach_delete_vertex(struct mgp_graph *graph, mgp_vertex *vertex) { return WrapExceptions([=] { + if (graph->ctx && graph->ctx->auth_checker && + !graph->ctx->auth_checker->Accept(*graph->ctx->db_accessor, vertex->impl, graph->view, + memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) { + throw AuthorizationException{"Insufficient permissions for deleting a vertex!"}; + } + if (!MgpGraphIsMutable(*graph)) { throw ImmutableObjectException{"Cannot remove a vertex from an immutable graph!"}; } @@ -2188,7 +2287,13 @@ mgp_error mgp_graph_detach_delete_vertex(struct mgp_graph *graph, mgp_vertex *ve mgp_error mgp_graph_create_edge(mgp_graph *graph, mgp_vertex *from, mgp_vertex *to, mgp_edge_type type, mgp_memory *memory, mgp_edge **result) { return WrapExceptions( - [=] { + [=]() -> mgp_edge * { + if (graph->ctx && graph->ctx->auth_checker && + !graph->ctx->auth_checker->Accept(*graph->ctx->db_accessor, from->graph->impl->NameToEdgeType(type.name), + memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) { + throw AuthorizationException{"Insufficient permissions for creating edges!"}; + } + if (!MgpGraphIsMutable(*graph)) { throw ImmutableObjectException{"Cannot create an edge in an immutable graph!"}; } @@ -2221,6 +2326,11 @@ mgp_error mgp_graph_create_edge(mgp_graph *graph, mgp_vertex *from, mgp_vertex * mgp_error mgp_graph_delete_edge(struct mgp_graph *graph, mgp_edge *edge) { return WrapExceptions([=] { + if (graph->ctx && graph->ctx->auth_checker && + !graph->ctx->auth_checker->Accept(*graph->ctx->db_accessor, edge->impl, + memgraph::query::AuthQuery::FineGrainedPrivilege::CREATE_DELETE)) { + throw AuthorizationException{"Insufficient permissions for deleting an edge!"}; + } if (!MgpGraphIsMutable(*graph)) { throw ImmutableObjectException{"Cannot remove an edge from an immutable graph!"}; } @@ -2253,7 +2363,7 @@ mgp_error mgp_graph_delete_edge(struct mgp_graph *graph, mgp_edge *edge) { namespace { void NextPermitted(mgp_vertices_iterator &it) { - if (!it.graph->ctx->auth_checker) { + if (!it.graph->ctx || !it.graph->ctx->auth_checker) { return; } diff --git a/src/query/procedure/py_module.cpp b/src/query/procedure/py_module.cpp index 981608703..f1959649f 100644 --- a/src/query/procedure/py_module.cpp +++ b/src/query/procedure/py_module.cpp @@ -51,6 +51,7 @@ PyObject *gMgpKeyAlreadyExistsError{nullptr}; // NOLINT(cppcoreguidelines-avo PyObject *gMgpImmutableObjectError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) PyObject *gMgpValueConversionError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) PyObject *gMgpSerializationError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +PyObject *gMgpAuthorizationError{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // Returns true if an exception is raised bool RaiseExceptionFromErrorCode(const mgp_error error) { @@ -101,6 +102,10 @@ bool RaiseExceptionFromErrorCode(const mgp_error error) { PyErr_SetString(gMgpSerializationError, "Operation cannot be serialized."); return true; } + case mgp_error::MGP_ERROR_AUTHORIZATION_ERROR: { + PyErr_SetString(gMgpAuthorizationError, "Authorization Error. Permission Denied."); + return true; + } } } @@ -2192,6 +2197,7 @@ PyObject *PyInitMgpModule() { PyMgpError{"_mgp.ImmutableObjectError", gMgpImmutableObjectError, PyExc_RuntimeError, nullptr}, PyMgpError{"_mgp.ValueConversionError", gMgpValueConversionError, PyExc_RuntimeError, nullptr}, PyMgpError{"_mgp.SerializationError", gMgpSerializationError, PyExc_RuntimeError, nullptr}, + PyMgpError{"_mgp.AuthorizationError", gMgpAuthorizationError, PyExc_RuntimeError, nullptr}, }; Py_INCREF(Py_None); diff --git a/tests/e2e/lba_procedures/CMakeLists.txt b/tests/e2e/lba_procedures/CMakeLists.txt index 5c604d245..8e1ebb41b 100644 --- a/tests/e2e/lba_procedures/CMakeLists.txt +++ b/tests/e2e/lba_procedures/CMakeLists.txt @@ -3,10 +3,11 @@ function(copy_lba_procedures_e2e_python_files FILE_NAME) endfunction() copy_lba_procedures_e2e_python_files(common.py) -copy_lba_procedures_e2e_python_files(lba_procedures.py) copy_lba_procedures_e2e_python_files(show_privileges.py) +copy_lba_procedures_e2e_python_files(read_query_modules.py) +copy_lba_procedures_e2e_python_files(update_query_modules.py) +copy_lba_procedures_e2e_python_files(create_delete_query_modules.py) copy_lba_procedures_e2e_python_files(read_permission_queries.py) copy_lba_procedures_e2e_python_files(update_permission_queries.py) - add_subdirectory(procedures) diff --git a/tests/e2e/lba_procedures/common.py b/tests/e2e/lba_procedures/common.py index 1d1a40737..553307b4c 100644 --- a/tests/e2e/lba_procedures/common.py +++ b/tests/e2e/lba_procedures/common.py @@ -24,7 +24,7 @@ def connect(**kwargs) -> mgclient.Connection: return connection -def reset_permissions(admin_cursor: mgclient.Cursor, create_index: bool): +def reset_permissions(admin_cursor: mgclient.Cursor, create_index: bool = False): execute_and_fetch_all(admin_cursor, "REVOKE LABELS * FROM user;") execute_and_fetch_all(admin_cursor, "REVOKE EDGE_TYPES * FROM user;") execute_and_fetch_all(admin_cursor, "MATCH(n) DETACH DELETE n;") @@ -32,6 +32,9 @@ def reset_permissions(admin_cursor: mgclient.Cursor, create_index: bool): execute_and_fetch_all(admin_cursor, "DROP INDEX ON :read_label;") execute_and_fetch_all(admin_cursor, "CREATE (n:read_label {prop: 5});") + execute_and_fetch_all( + admin_cursor, "CREATE (n:read_label_1 {prop: 5})-[r:read_edge_type]->(m:read_label_2 {prop: 5});" + ) if create_index: execute_and_fetch_all(admin_cursor, "CREATE INDEX ON :read_label;") @@ -41,11 +44,26 @@ def reset_permissions(admin_cursor: mgclient.Cursor, create_index: bool): def reset_update_permissions(admin_cursor: mgclient.Cursor): execute_and_fetch_all(admin_cursor, "REVOKE LABELS * FROM user;") execute_and_fetch_all(admin_cursor, "REVOKE EDGE_TYPES * FROM user;") - - execute_and_fetch_all(admin_cursor, "MATCH(n) DETACH DELETE n;") + execute_and_fetch_all(admin_cursor, "MATCH (n) DETACH DELETE n;") execute_and_fetch_all(admin_cursor, "CREATE (n:update_label {prop: 1});") execute_and_fetch_all( admin_cursor, - "CREATE (n:update_label_1)-[r:update_edge_type]->(m:update_label_2);", + "CREATE (n:update_label_1)-[r:update_edge_type {prop: 1}]->(m:update_label_2);", + ) + + +def reset_create_delete_permissions(admin_cursor: mgclient.Cursor): + execute_and_fetch_all(admin_cursor, "REVOKE LABELS * FROM user;") + execute_and_fetch_all(admin_cursor, "REVOKE EDGE_TYPES * FROM user;") + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS * TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON EDGE_TYPES * TO user;") + + execute_and_fetch_all(admin_cursor, "MATCH (n) DETACH DELETE n;") + + execute_and_fetch_all(admin_cursor, "CREATE (n:create_delete_label);") + execute_and_fetch_all( + admin_cursor, + "CREATE (n:create_delete_label_1)-[r:create_delete_edge_type]->(m:create_delete_label_2);", ) diff --git a/tests/e2e/lba_procedures/create_delete_query_modules.py b/tests/e2e/lba_procedures/create_delete_query_modules.py new file mode 100644 index 000000000..e63105bdb --- /dev/null +++ b/tests/e2e/lba_procedures/create_delete_query_modules.py @@ -0,0 +1,299 @@ +# Copyright 2022 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +import pytest +import sys + +from common import ( + connect, + execute_and_fetch_all, + mgclient, + reset_create_delete_permissions, +) + +AUTHORIZATION_ERROR_IDENTIFIER = "AuthorizationError" + +create_vertex_query = "CALL create_delete.create_vertex() YIELD created_node RETURN labels(created_node);" +remove_label_vertex_query = "CALL create_delete.remove_label('create_delete_label') YIELD node RETURN labels(node);" +set_label_vertex_query = "CALL create_delete.set_label('new_create_delete_label') YIELD node RETURN labels(node);" +create_edge_query = "MATCH (n:create_delete_label_1), (m:create_delete_label_2) CALL create_delete.create_edge(n, m) YIELD nr_of_edges RETURN nr_of_edges;" +delete_edge_query = "CALL create_delete.delete_edge() YIELD * RETURN *;" + + +def test_can_not_create_vertex_when_given_nothing(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, create_vertex_query) + + +def test_can_create_vertex_when_given_global_create_delete(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + + result = execute_and_fetch_all(test_cursor, create_vertex_query) + + len(result[0][0]) == 1 + + +def test_can_not_create_vertex_when_given_global_read(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, create_vertex_query) + + +def test_can_not_create_vertex_when_given_global_update(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON LABELS :create_delete_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, create_vertex_query) + + +def test_can_add_vertex_label_when_given_create_delete(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all( + admin_cursor, + "GRANT CREATE_DELETE ON LABELS :new_create_delete_label, UPDATE ON LABELS :create_delete_label TO user;", + ) + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_label_vertex_query) + + assert "create_delete_label" in result[0][0] + assert "new_create_delete_label" in result[0][0] + + +def test_can_not_add_vertex_label_when_given_update(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all( + admin_cursor, "GRANT UPDATE ON LABELS :new_create_delete_label, :create_delete_label TO user;" + ) + + test_cursor = connect(username="user", password="test").cursor() + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, set_label_vertex_query) + + +def test_can_not_add_vertex_label_when_given_read(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all( + admin_cursor, "GRANT READ ON LABELS :new_create_delete_label, UPDATE ON LABELS :create_delete_label TO user;" + ) + + test_cursor = connect(username="user", password="test").cursor() + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, set_label_vertex_query) + + +def test_can_remove_vertex_label_when_given_create_delete(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON LABELS :create_delete_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, remove_label_vertex_query) + + assert result[0][0] != ":create_delete_label" + + +def test_can_remove_vertex_label_when_given_global_create_delete(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, remove_label_vertex_query) + + assert result[0][0] != ":create_delete_label" + + +def test_can_not_remove_vertex_label_when_given_update(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON LABELS :create_delete_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, remove_label_vertex_query) + + +def test_can_not_remove_vertex_label_when_given_global_update(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, remove_label_vertex_query) + + +def test_can_not_remove_vertex_label_when_given_read(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :create_delete_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, remove_label_vertex_query) + + +def test_can_not_remove_vertex_label_when_given_global_read(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, remove_label_vertex_query) + + +def test_can_not_create_edge_when_given_nothing(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, create_edge_query) + + +def test_can_not_create_edge_when_given_read(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON EDGE_TYPES :new_create_delete_edge_type TO user") + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, create_edge_query) + + +def test_can_not_create_edge_when_given_update(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON EDGE_TYPES :new_create_delete_edge_type TO user") + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, create_edge_query) + + +def test_can_create_edge_when_given_create_delete(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all( + admin_cursor, + "GRANT CREATE_DELETE ON EDGE_TYPES :new_create_delete_edge_type TO user", + ) + + test_cursor = connect(username="user", password="test").cursor() + + no_of_edges = execute_and_fetch_all(test_cursor, create_edge_query) + + assert no_of_edges[0][0] == 2 + + +def test_can_not_delete_edge_when_given_nothing(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, delete_edge_query) + + +def test_can_not_delete_edge_when_given_read(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all( + admin_cursor, + "GRANT READ ON EDGE_TYPES :create_delete_edge_type TO user", + ) + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, delete_edge_query) + + +def test_can_not_delete_edge_when_given_update(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all( + admin_cursor, + "GRANT UPDATE ON EDGE_TYPES :create_delete_edge_type TO user", + ) + + test_cursor = connect(username="user", password="test").cursor() + + with pytest.raises(mgclient.DatabaseError, match=AUTHORIZATION_ERROR_IDENTIFIER): + execute_and_fetch_all(test_cursor, delete_edge_query) + + +def test_can_delete_edge_when_given_create_delete(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_create_delete_permissions(admin_cursor) + + execute_and_fetch_all( + admin_cursor, + "GRANT CREATE_DELETE ON EDGE_TYPES :create_delete_edge_type TO user", + ) + + test_cursor = connect(username="user", password="test").cursor() + + no_of_edges = execute_and_fetch_all(test_cursor, delete_edge_query) + + assert no_of_edges[0][0] == 0 + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/lba_procedures/lba_procedures.py b/tests/e2e/lba_procedures/lba_procedures.py deleted file mode 100644 index 641ca8277..000000000 --- a/tests/e2e/lba_procedures/lba_procedures.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2022 Memgraph Ltd. -# -# Use of this software is governed by the Business Source License -# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source -# License, and you may not use this file except in compliance with the Business Source License. -# -# As of the Change Date specified in that file, in accordance with -# the Business Source License, use of this software will be governed -# by the Apache License, Version 2.0, included in the file -# licenses/APL.txt. - -import sys -import pytest -from common import connect, execute_and_fetch_all - - -def test_lba_procedures_vertices_iterator_count_only_permitted_vertices(): - cursor = connect(username="Josip", password="").cursor() - result = execute_and_fetch_all(cursor, "CALL read.number_of_visible_nodes() YIELD nr_of_nodes RETURN nr_of_nodes ;") - - assert result[0][0] == 10 - - cursor = connect(username="Boris", password="").cursor() - result = execute_and_fetch_all(cursor, "CALL read.number_of_visible_nodes() YIELD nr_of_nodes RETURN nr_of_nodes ;") - - assert result[0][0] == 6 - - -if __name__ == "__main__": - sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/lba_procedures/procedures/CMakeLists.txt b/tests/e2e/lba_procedures/procedures/CMakeLists.txt index db09a16a0..0c3a1e498 100644 --- a/tests/e2e/lba_procedures/procedures/CMakeLists.txt +++ b/tests/e2e/lba_procedures/procedures/CMakeLists.txt @@ -1 +1,3 @@ copy_lba_procedures_e2e_python_files(read.py) +copy_lba_procedures_e2e_python_files(update.py) +copy_lba_procedures_e2e_python_files(create_delete.py) diff --git a/tests/e2e/lba_procedures/procedures/create_delete.py b/tests/e2e/lba_procedures/procedures/create_delete.py new file mode 100644 index 000000000..9cf5e654b --- /dev/null +++ b/tests/e2e/lba_procedures/procedures/create_delete.py @@ -0,0 +1,63 @@ +# Copyright 2021 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +import mgp + + +@mgp.write_proc +def create_vertex(ctx: mgp.ProcCtx) -> mgp.Record(created_node=mgp.Vertex): + vertex = ctx.graph.create_vertex() + return mgp.Record(created_node=vertex) + + +@mgp.write_proc +def remove_label(ctx: mgp.ProcCtx, label: str) -> mgp.Record(node=mgp.Vertex): + for vertex in ctx.graph.vertices: + if "create_delete_label" in vertex.labels: + break + + vertex.remove_label(label) + return mgp.Record(node=vertex) + + +@mgp.write_proc +def set_label(ctx: mgp.ProcCtx, new_label: str) -> mgp.Record(node=mgp.Vertex): + for vertex in ctx.graph.vertices: + if "create_delete_label" in vertex.labels: + break + + vertex.add_label(new_label) + return mgp.Record(node=vertex) + + +@mgp.write_proc +def create_edge(ctx: mgp.ProcCtx, v1: mgp.Vertex, v2: mgp.Vertex) -> mgp.Record(nr_of_edges=int): + ctx.graph.create_edge(v1, v2, mgp.EdgeType("new_create_delete_edge_type")) + + count = 0 + for vertex in ctx.graph.vertices: + for _ in vertex.out_edges: + count += 1 + + return mgp.Record(nr_of_edges=count) + + +@mgp.write_proc +def delete_edge(ctx: mgp.ProcCtx) -> mgp.Record(edge_count=int): + count = 0 + for vertex in ctx.graph.vertices: + for edge in vertex.out_edges: + if edge.type.name == "create_delete_edge_type": + ctx.graph.delete_edge(edge) + else: + count += 1 + + return mgp.Record(edge_count=count) diff --git a/tests/e2e/lba_procedures/procedures/read.py b/tests/e2e/lba_procedures/procedures/read.py index d5d3c1fd0..21aa22aed 100644 --- a/tests/e2e/lba_procedures/procedures/read.py +++ b/tests/e2e/lba_procedures/procedures/read.py @@ -15,3 +15,13 @@ import mgp @mgp.read_proc def number_of_visible_nodes(ctx: mgp.ProcCtx) -> mgp.Record(nr_of_nodes=int): return mgp.Record(nr_of_nodes=len(mgp.Vertices(ctx.graph._graph))) + + +@mgp.read_proc +def number_of_visible_edges(ctx: mgp.ProcCtx) -> mgp.Record(nr_of_edges=int): + count = 0 + for vertex in ctx.graph.vertices: + for _ in vertex.out_edges: + count += 1 + + return mgp.Record(nr_of_edges=count) diff --git a/tests/e2e/lba_procedures/procedures/update.py b/tests/e2e/lba_procedures/procedures/update.py new file mode 100644 index 000000000..8d89a89d0 --- /dev/null +++ b/tests/e2e/lba_procedures/procedures/update.py @@ -0,0 +1,21 @@ +# Copyright 2021 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +import mgp + + +@mgp.write_proc +def set_property(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record(): + try: + object.properties.set("prop", 2) + except mgp.AuthorizationError: + pass + return mgp.Record() diff --git a/tests/e2e/lba_procedures/read_permission_queries.py b/tests/e2e/lba_procedures/read_permission_queries.py index 061b43b75..9b31a290c 100644 --- a/tests/e2e/lba_procedures/read_permission_queries.py +++ b/tests/e2e/lba_procedures/read_permission_queries.py @@ -19,10 +19,10 @@ from common import connect, execute_and_fetch_all, reset_permissions match_query = "MATCH (n) RETURN n;" match_by_id_query = "MATCH (n) WHERE ID(n) >= 0 RETURN n;" -match_by_label_query = "MATCH (n:read_label) RETURN n;" -match_by_label_property_range_query = "MATCH (n:read_label) WHERE n.prop < 7 RETURN n;" -match_by_label_property_value_query = "MATCH (n:read_label {prop: 5}) RETURN n;" -match_by_label_property_query = "MATCH (n:read_label) WHERE n.prop IS NOT NULL RETURN n;" +match_by_label_query = "MATCH (n) RETURN n;" +match_by_label_property_range_query = "MATCH (n) WHERE n.prop < 7 RETURN n;" +match_by_label_property_value_query = "MATCH (n {prop: 5}) RETURN n;" +match_by_label_property_query = "MATCH (n) WHERE n.prop IS NOT NULL RETURN n;" read_node_without_index_operation_cases = [ @@ -34,6 +34,7 @@ read_node_without_index_operation_cases = [ ["GRANT CREATE_DELETE ON LABELS * TO user;"], ] +read_node_without_index_operation_cases_expected_size = [1, 3, 1, 3, 1, 3] read_node_with_index_operation_cases = [ ["GRANT READ ON LABELS :read_label TO user;"], @@ -44,6 +45,7 @@ read_node_with_index_operation_cases = [ ["GRANT CREATE_DELETE ON LABELS * TO user;"], ] +read_node_with_index_operation_cases_expected_sizes = [1, 3, 1, 3, 1, 3] not_read_node_without_index_operation_cases = [ [], @@ -67,6 +69,7 @@ not_read_node_without_index_operation_cases = [ ], ] +not_read_node_without_index_operation_cases_expected_sizes = [0, 0, 0, 0, 2, 0, 2] not_read_node_with_index_operation_cases = [ [], @@ -90,6 +93,8 @@ not_read_node_with_index_operation_cases = [ ], ] +not_read_node_with_index_operation_cases_expexted_sizes = [0, 0, 0, 0, 2, 0, 2] + def get_admin_cursor(): return connect(username="admin", password="test").cursor() @@ -100,7 +105,7 @@ def get_user_cursor(): def execute_read_node_assertion( - operation_case: List[str], queries: List[str], create_index: bool, can_read: bool + operation_case: List[str], queries: List[str], create_index: bool, expected_size: int ) -> None: admin_cursor = get_admin_cursor() user_cursor = get_user_cursor() @@ -110,10 +115,9 @@ def execute_read_node_assertion( for operation in operation_case: execute_and_fetch_all(admin_cursor, operation) - read_size = 1 if can_read else 0 for mq in queries: results = execute_and_fetch_all(user_cursor, mq) - assert len(results) == read_size + assert len(results) == expected_size def test_can_read_node_when_authorized(): @@ -125,10 +129,14 @@ def test_can_read_node_when_authorized(): match_by_label_property_value_query, ] - for operation_case in read_node_without_index_operation_cases: - execute_read_node_assertion(operation_case, match_queries_without_index, False, True) - for operation_case in read_node_with_index_operation_cases: - execute_read_node_assertion(operation_case, match_queries_with_index, True, True) + for expected_size, operation_case in zip( + read_node_without_index_operation_cases_expected_size, read_node_without_index_operation_cases + ): + execute_read_node_assertion(operation_case, match_queries_without_index, False, expected_size) + for expected_size, operation_case in zip( + read_node_with_index_operation_cases_expected_sizes, read_node_with_index_operation_cases + ): + execute_read_node_assertion(operation_case, match_queries_with_index, True, expected_size) def test_can_not_read_node_when_authorized(): @@ -140,10 +148,14 @@ def test_can_not_read_node_when_authorized(): match_by_label_property_value_query, ] - for operation_case in not_read_node_without_index_operation_cases: - execute_read_node_assertion(operation_case, match_queries_without_index, False, False) - for operation_case in not_read_node_with_index_operation_cases: - execute_read_node_assertion(operation_case, match_queries_with_index, True, False) + for expected_size, operation_case in zip( + not_read_node_without_index_operation_cases_expected_sizes, not_read_node_without_index_operation_cases + ): + execute_read_node_assertion(operation_case, match_queries_without_index, False, expected_size) + for expected_size, operation_case in zip( + not_read_node_with_index_operation_cases_expexted_sizes, not_read_node_with_index_operation_cases + ): + execute_read_node_assertion(operation_case, match_queries_with_index, True, expected_size) if __name__ == "__main__": diff --git a/tests/e2e/lba_procedures/read_query_modules.py b/tests/e2e/lba_procedures/read_query_modules.py new file mode 100644 index 000000000..7882ef9d2 --- /dev/null +++ b/tests/e2e/lba_procedures/read_query_modules.py @@ -0,0 +1,225 @@ +# Copyright 2022 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +import sys +import pytest +from common import connect, execute_and_fetch_all, reset_permissions + +get_number_of_vertices_query = "CALL read.number_of_visible_nodes() YIELD nr_of_nodes RETURN nr_of_nodes;" +get_number_of_edges_query = "CALL read.number_of_visible_edges() YIELD nr_of_edges RETURN nr_of_edges;" + + +def test_can_read_vertex_through_c_api_when_given_grant_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :read_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_vertices_query) + + assert result[0][0] == 1 + + +def test_can_read_vertex_through_c_api_when_given_update_grant_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON LABELS :read_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_vertices_query) + + assert result[0][0] == 1 + + +def test_can_read_vertex_through_c_api_when_given_create_delete_grant_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON LABELS :read_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_vertices_query) + + assert result[0][0] == 1 + + +def test_can_not_read_vertex_through_c_api_when_given_nothing(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_vertices_query) + + assert result[0][0] == 0 + + +def test_can_not_read_vertex_through_c_api_when_given_deny_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "DENY READ ON LABELS :read_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_vertices_query) + + assert result[0][0] == 0 + + +def test_can_read_partial_vertices_through_c_api_when_given_global_read_but_deny_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "DENY READ ON LABELS :read_label TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_vertices_query) + + assert result[0][0] == 2 + + +def test_can_read_partial_vertices_through_c_api_when_given_global_update_but_deny_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "DENY READ ON LABELS :read_label TO user;") + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_vertices_query) + + assert result[0][0] == 2 + + +def test_can_read_partial_vertices_through_c_api_when_given_global_create_delete_but_deny_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "DENY READ ON LABELS :read_label TO user;") + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_vertices_query) + + assert result[0][0] == 2 + + +def test_can_read_edge_through_c_api_when_given_grant_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :read_label_1, :read_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON EDGE_TYPES :read_edge_type TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_edges_query) + + assert result[0][0] == 1 + + +def test_can_not_read_edge_through_c_api_when_given_deny_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :read_label_1, :read_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "DENY READ ON EDGE_TYPES :read_edge_type TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_edges_query) + + assert result[0][0] == 0 + + +def test_can_read_edge_through_c_api_when_given_grant_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :read_label_1, :read_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON EDGE_TYPES :read_edge_type TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_edges_query) + + assert result[0][0] == 1 + + +def test_can_read_edge_through_c_api_when_given_update_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :read_label_1, :read_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON EDGE_TYPES :read_edge_type TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_edges_query) + + assert result[0][0] == 1 + + +def test_can_read_edge_through_c_api_when_given_create_delete_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :read_label_1, :read_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON EDGE_TYPES :read_edge_type TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_edges_query) + + assert result[0][0] == 1 + + +def test_can_not_read_edge_through_c_api_when_given_read_global_but_deny_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :read_label_1, :read_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "DENY READ ON EDGE_TYPES :read_edge_type TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON EDGE_TYPES * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_edges_query) + + assert result[0][0] == 0 + + +def test_can_not_read_edge_through_c_api_when_given_update_global_but_deny_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :read_label_1, :read_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "DENY READ ON EDGE_TYPES :read_edge_type TO user;") + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON EDGE_TYPES * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_edges_query) + + assert result[0][0] == 0 + + +def test_can_not_read_edge_through_c_api_when_given_create_delete_global_but_deny_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :read_label_1, :read_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "DENY READ ON EDGE_TYPES :read_edge_type TO user;") + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON EDGE_TYPES * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, get_number_of_edges_query) + + assert result[0][0] == 0 + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/lba_procedures/update_query_modules.py b/tests/e2e/lba_procedures/update_query_modules.py new file mode 100644 index 000000000..b62286e7a --- /dev/null +++ b/tests/e2e/lba_procedures/update_query_modules.py @@ -0,0 +1,184 @@ +# Copyright 2022 Memgraph Ltd. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source +# License, and you may not use this file except in compliance with the Business Source License. +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0, included in the file +# licenses/APL.txt. + +import pytest +import sys + +from common import ( + connect, + execute_and_fetch_all, + reset_update_permissions, +) + +set_vertex_property_query = "MATCH (n:update_label) CALL update.set_property(n) YIELD * RETURN n.prop;" +set_edge_property_query = "MATCH (n:update_label_1)-[r:update_edge_type]->(m:update_label_2) CALL update.set_property(r) YIELD * RETURN r.prop;" + + +def test_can_not_update_vertex_when_given_read(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_vertex_property_query) + + assert result[0][0] == 1 + + +def test_can_update_vertex_when_given_update_grant_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON LABELS :update_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_vertex_property_query) + + assert result[0][0] == 2 + + +def test_can_update_vertex_when_given_create_delete_grant_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON LABELS :update_label TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_vertex_property_query) + + assert result[0][0] == 2 + + +def test_can_update_vertex_when_given_update_global_grant_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_vertex_property_query) + + assert result[0][0] == 2 + + +def test_can_update_vertex_when_given_create_delete_global_grant_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_vertex_property_query) + + assert result[0][0] == 2 + + +def test_can_not_update_vertex_when_denied_update_and_granted_global_update_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "DENY UPDATE ON LABELS :update_label TO user;") + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_vertex_property_query) + + assert result[0][0] == 1 + + +def test_can_not_update_vertex_when_denied_update_and_granted_global_create_delete_on_label(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "DENY UPDATE ON LABELS :update_label TO user;") + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON LABELS * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_vertex_property_query) + + assert result[0][0] == 1 + + +def test_can_update_edge_when_given_update_grant_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label_1 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT UPDATE ON EDGE_TYPES :update_edge_type TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_edge_property_query) + + assert result[0][0] == 2 + + +def test_can_not_update_edge_when_given_read_grant_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label_1 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON EDGE_TYPES :update_edge_type TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_edge_property_query) + + assert result[0][0] == 1 + + +def test_can_update_edge_when_given_create_delete_grant_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label_1 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT CREATE_DELETE ON EDGE_TYPES :update_edge_type TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_edge_property_query) + + assert result[0][0] == 2 + + +def test_can_not_update_edge_when_denied_update_edge_type_but_granted_global_update_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label_1 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "DENY UPDATE ON EDGE_TYPES :update_edge_type TO user;") + execute_and_fetch_all(admin_cursor, "DENY UPDATE ON EDGE_TYPES * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_edge_property_query) + + assert result[0][0] == 1 + + +def test_can_not_update_edge_when_denied_update_edge_type_but_granted_global_create_delete_on_edge_type(): + admin_cursor = connect(username="admin", password="test").cursor() + reset_update_permissions(admin_cursor) + + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label_1 TO user;") + execute_and_fetch_all(admin_cursor, "GRANT READ ON LABELS :update_label_2 TO user;") + execute_and_fetch_all(admin_cursor, "DENY UPDATE ON EDGE_TYPES :update_edge_type TO user;") + execute_and_fetch_all(admin_cursor, "DENY CREATE_DELETE ON EDGE_TYPES * TO user;") + + test_cursor = connect(username="user", password="test").cursor() + result = execute_and_fetch_all(test_cursor, set_edge_property_query) + + assert result[0][0] == 1 + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/lba_procedures/workloads.yaml b/tests/e2e/lba_procedures/workloads.yaml index 8fb6eb826..8b849721d 100644 --- a/tests/e2e/lba_procedures/workloads.yaml +++ b/tests/e2e/lba_procedures/workloads.yaml @@ -1,22 +1,25 @@ -template_cluster: &template_cluster +read_query_modules_cluster: &read_query_modules_cluster cluster: main: args: ["--bolt-port", "7687", "--log-level=TRACE"] log_file: "lba-e2e.log" setup_queries: - - "Create (:Label1 {id: 1}) ;" - - "Create (:Label1 {id: 2}) ;" - - "Create (:Label1 {id: 3}) ;" - - "Create (:Label1 {id: 4}) ;" - - "Create (:Label1 {id: 5}) ;" - - "Create (:Label1 {id: 6}) ;" - - "Create (:Label2 {id: 1}) ;" - - "Create (:Label2 {id: 2}) ;" - - "Create (:Label2 {id: 3}) ;" - - "Create (:Label2 {id: 4}) ;" - - "Create User Josip ;" - - "Create User Boris ;" - - "Grant Read On Labels :Label1 to Boris;" + - "CREATE USER admin IDENTIFIED BY 'test';" + - "GRANT ALL PRIVILEGES TO admin" + - "CREATE USER user IDENTIFIED BY 'test';" + - "GRANT ALL PRIVILEGES TO user" + validation_queries: [] + +update_query_modules_cluster: &update_query_modules_cluster + cluster: + main: + args: ["--bolt-port", "7687", "--log-level=TRACE"] + log_file: "lba-e2e.log" + setup_queries: + - "CREATE USER admin IDENTIFIED BY 'test';" + - "GRANT ALL PRIVILEGES TO admin" + - "CREATE USER user IDENTIFIED BY 'test';" + - "GRANT ALL PRIVILEGES TO user" validation_queries: [] show_privileges_cluster: &show_privileges_cluster @@ -54,8 +57,21 @@ show_privileges_cluster: &show_privileges_cluster - "Create User Bruno;" - "Grant Auth to Bruno;" - "Deny Create_Delete On Labels * to Bruno" + validation_queries: [] read_permission_queries: &read_permission_queries + cluster: + main: + args: ["--bolt-port", "7687", "--log-level=TRACE"] + log_file: "lba-e2e.log" + setup_queries: + - "CREATE USER admin IDENTIFIED BY 'test';" + - "GRANT ALL PRIVILEGES TO admin" + - "CREATE USER user IDENTIFIED BY 'test';" + - "GRANT ALL PRIVILEGES TO user" + validation_queries: [] + +create_delete_query_modules_cluster: &create_delete_query_modules_cluster cluster: main: args: ["--bolt-port", "7687", "--log-level=TRACE"] @@ -77,15 +93,26 @@ update_permission_queries_cluster: &update_permission_queries_cluster - "GRANT ALL PRIVILEGES TO admin;" - "CREATE USER user IDENTIFIED BY 'test'" - "GRANT ALL PRIVILEGES TO user;" - validation_queries: [] workloads: - - name: "Label-based auth" + - name: "read-query-modules" binary: "tests/e2e/pytest_runner.sh" proc: "tests/e2e/lba_procedures/procedures/" - args: ["lba_procedures/lba_procedures.py"] - <<: *template_cluster + args: ["lba_procedures/read_query_modules.py"] + <<: *read_query_modules_cluster + + - name: "update-query-modules" + binary: "tests/e2e/pytest_runner.sh" + proc: "tests/e2e/lba_procedures/procedures/" + args: ["lba_procedures/update_query_modules.py"] + <<: *update_query_modules_cluster + + - name: "create-delete-query-modules" + binary: "tests/e2e/pytest_runner.sh" + proc: "tests/e2e/lba_procedures/procedures/" + args: ["lba_procedures/create_delete_query_modules.py"] + <<: *create_delete_query_modules_cluster - name: "show-privileges" binary: "tests/e2e/pytest_runner.sh"