diff --git a/src/auth/auth.hpp b/src/auth/auth.hpp index a143b96ce..11590e21a 100644 --- a/src/auth/auth.hpp +++ b/src/auth/auth.hpp @@ -21,7 +21,7 @@ namespace memgraph::auth { /** * This class serves as the main Authentication/Authorization storage. - * It provides functions for managing Users, Roles and Permissions. + * It provides functions for managing Users, Roles, Permissions and FineGrainedAccessPermissions. * NOTE: The non-const functions in this class aren't thread safe. * TODO (mferencevic): Disable user/role modification functions when they are * being managed by the auth module. diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index 32c67eee7..389e193cd 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -38,6 +38,7 @@ #include "query/procedure/mg_procedure_impl.hpp" #include "query/procedure/module.hpp" #include "storage/v2/property_value.hpp" +#include "storage/v2/view.hpp" #include "utils/algorithm.hpp" #include "utils/csv_parsing.hpp" #include "utils/event_counter.hpp" @@ -684,7 +685,11 @@ bool Expand::ExpandCursor::Pull(Frame &frame, ExecutionContext &context) { if (in_edges_ && *in_edges_it_ != in_edges_->end()) { auto edge = *(*in_edges_it_)++; if (context.auth_checker && - !context.auth_checker->IsUserAuthorizedEdgeType(context.user, context.db_accessor, edge.EdgeType())) + (!context.auth_checker->IsUserAuthorizedEdgeType(context.user, context.db_accessor, edge.EdgeType()) || + !context.auth_checker->IsUserAuthorizedLabels(context.user, context.db_accessor, + edge.To().Labels(storage::View::OLD).GetValue()) || + !context.auth_checker->IsUserAuthorizedLabels(context.user, context.db_accessor, + edge.From().Labels(storage::View::OLD).GetValue()))) continue; frame[self_.common_.edge_symbol] = edge; pull_node(edge, EdgeAtom::Direction::IN); @@ -699,7 +704,11 @@ bool Expand::ExpandCursor::Pull(Frame &frame, ExecutionContext &context) { // already done in the block above if (self_.common_.direction == EdgeAtom::Direction::BOTH && edge.IsCycle()) continue; if (context.auth_checker && - !context.auth_checker->IsUserAuthorizedEdgeType(context.user, context.db_accessor, edge.EdgeType())) + (!context.auth_checker->IsUserAuthorizedEdgeType(context.user, context.db_accessor, edge.EdgeType()) || + !context.auth_checker->IsUserAuthorizedLabels(context.user, context.db_accessor, + edge.To().Labels(storage::View::OLD).GetValue()) || + !context.auth_checker->IsUserAuthorizedLabels(context.user, context.db_accessor, + edge.From().Labels(storage::View::OLD).GetValue()))) continue; frame[self_.common_.edge_symbol] = edge; pull_node(edge, EdgeAtom::Direction::OUT); diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt index 3ef0f4e3d..41a0d033a 100644 --- a/tests/integration/CMakeLists.txt +++ b/tests/integration/CMakeLists.txt @@ -10,6 +10,9 @@ add_subdirectory(transactions) # auth test binaries add_subdirectory(auth) +# lba test binaries +add_subdirectory(lba) + ## distributed ha/basic binaries #add_subdirectory(ha/basic) # diff --git a/tests/integration/lba/CMakeLists.txt b/tests/integration/lba/CMakeLists.txt new file mode 100644 index 000000000..9207bb13a --- /dev/null +++ b/tests/integration/lba/CMakeLists.txt @@ -0,0 +1,13 @@ + + +set(target_name memgraph__integration__lba) +set(tester_target_name ${target_name}__tester) +set(filtering_target_name ${target_name}__filtering) + +add_executable(${tester_target_name} tester.cpp) +set_target_properties(${tester_target_name} PROPERTIES OUTPUT_NAME tester) +target_link_libraries(${tester_target_name} mg-communication) + +add_executable(${filtering_target_name} filtering.cpp) +set_target_properties(${filtering_target_name} PROPERTIES OUTPUT_NAME filtering) +target_link_libraries(${filtering_target_name} mg-communication) diff --git a/tests/integration/lba/filtering.cpp b/tests/integration/lba/filtering.cpp new file mode 100644 index 000000000..3a9db74f5 --- /dev/null +++ b/tests/integration/lba/filtering.cpp @@ -0,0 +1,59 @@ +// 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. + +#include +#include + +#include "communication/bolt/client.hpp" +#include "io/network/endpoint.hpp" +#include "io/network/utils.hpp" +#include "utils/logging.hpp" + +DEFINE_string(address, "127.0.0.1", "Server address"); +DEFINE_int32(port, 7687, "Server port"); +DEFINE_string(username, "admin", "Username for the database"); +DEFINE_string(password, "admin", "Password for the database"); +DEFINE_bool(use_ssl, false, "Set to true to connect with SSL to the server."); + +/** + * Verifies that user 'user' has privileges that are given as positional + * arguments. + */ +int main(int argc, char **argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + + memgraph::communication::SSLInit sslInit; + + memgraph::io::network::Endpoint endpoint(memgraph::io::network::ResolveHostname(FLAGS_address), FLAGS_port); + + memgraph::communication::ClientContext context(FLAGS_use_ssl); + memgraph::communication::bolt::Client client(&context); + + client.Connect(endpoint, FLAGS_username, FLAGS_password); + + try { + std::string query(argv[1]); + auto ret = client.Execute(query, {}); + uint64_t count_got = ret.records.size(); + + if (count_got != std::atoi(argv[2])) { + LOG_FATAL("Expected the record to have {} entries but they had {} entries!", argv[2], count_got); + } + + } catch (const memgraph::communication::bolt::ClientQueryException &e) { + LOG_FATAL( + "The query shoudn't have failed but it failed with an " + "error message '{}', {}", + e.what(), argv[0]); + } + + return 0; +} diff --git a/tests/integration/lba/runner.py b/tests/integration/lba/runner.py new file mode 100644 index 000000000..a8696984e --- /dev/null +++ b/tests/integration/lba/runner.py @@ -0,0 +1,129 @@ +#!/usr/bin/python3 -u + +# 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 argparse +import atexit +import os +import subprocess +import sys +import tempfile +import time +from typing import List + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "..")) + +UNAUTHORIZED_ERROR = "You are not authorized to execute this query! Please " "contact your database administrator." + + +def wait_for_server(port, delay=0.1): + cmd = ["nc", "-z", "-w", "1", "127.0.0.1", str(port)] + while subprocess.call(cmd) != 0: + time.sleep(0.01) + time.sleep(delay) + + +def execute_tester( + binary, queries, should_fail=False, failure_message="", username="", password="", check_failure=True +): + args = [binary, "--username", username, "--password", password] + if should_fail: + args.append("--should-fail") + if failure_message: + args.extend(["--failure-message", failure_message]) + if check_failure: + args.append("--check-failure") + args.extend(queries) + subprocess.run(args).check_returncode() + + +def execute_filtering(binary: str, queries: List[str], expected: int, username: str = "", password: str = "") -> None: + args = [binary, "--username", username, "--password", password] + + args.extend(queries) + args.append(str(expected)) + + subprocess.run(args).check_returncode() + + +def execute_test(memgraph_binary: str, tester_binary: str, filtering_binary: str) -> None: + storage_directory = tempfile.TemporaryDirectory() + memgraph_args = [memgraph_binary, "--data-directory", storage_directory.name] + + def execute_admin_queries(queries): + return execute_tester( + tester_binary, queries, should_fail=False, check_failure=True, username="admin", password="admin" + ) + + def execute_user_queries(queries, should_fail=False, failure_message="", check_failure=True): + return execute_tester(tester_binary, queries, should_fail, failure_message, "user", "user", check_failure) + + # Start the memgraph binary + memgraph = subprocess.Popen(list(map(str, memgraph_args))) + time.sleep(0.1) + assert memgraph.poll() is None, "Memgraph process died prematurely!" + wait_for_server(7687) + + # Register cleanup function + @atexit.register + def cleanup(): + if memgraph.poll() is None: + memgraph.terminate() + assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!" + + # Prepare all users + execute_admin_queries( + [ + "CREATE USER admin IDENTIFIED BY 'admin'", + "GRANT ALL PRIVILEGES TO admin", + "CREATE USER user IDENTIFIED BY 'user'", + "GRANT LABELS :label1, :label2, :label3 TO user", + "GRANT EDGE_TYPES :edgeType1, :edgeType2 TO user", + "MERGE (l1:label1 {name: 'test1'})", + "MERGE (l2:label2 {name: 'test2'})", + "MATCH (l1:label1),(l2:label2) WHERE l1.name = 'test1' AND l2.name = 'test2' CREATE (l1)-[r:edgeType1]->(l2)", + "MERGE (l3:label3 {name: 'test3'})", + "MATCH (l1:label1),(l3:label3) WHERE l1.name = 'test1' AND l3.name = 'test3' CREATE (l1)-[r:edgeType2]->(l3)", + ] + ) + + # Run the test with all combinations of permissions + print("\033[1;36m~~ Starting edge filtering test ~~\033[0m") + execute_filtering(filtering_binary, ["MATCH (n)-[r]->(m) RETURN n,r,m"], 2, "user", "user") + execute_admin_queries(["DENY EDGE_TYPES :edgeType1 TO user"]) + execute_filtering(filtering_binary, ["MATCH (n)-[r]->(m) RETURN n,r,m"], 1, "user", "user") + execute_admin_queries(["GRANT EDGE_TYPES :edgeType1 TO user", "DENY LABELS :label3 TO user"]) + execute_filtering(filtering_binary, ["MATCH (n)-[r]->(m) RETURN n,r,m"], 1, "user", "user") + execute_admin_queries(["REVOKE LABELS * FROM user", "REVOKE EDGE_TYPES * FROM user"]) + execute_filtering(filtering_binary, ["MATCH (n)-[r]->(m) RETURN n,r,m"], 0, "user", "user") + print("\033[1;36m~~ Finished edge filtering test ~~\033[0m\n") + + # Shutdown the memgraph binary + memgraph.terminate() + assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!" + + +if __name__ == "__main__": + memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph") + tester_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "lba", "tester") + filtering_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "lba", "filtering") + + parser = argparse.ArgumentParser() + parser.add_argument("--memgraph", default=memgraph_binary) + parser.add_argument("--tester", default=tester_binary) + parser.add_argument("--filtering", default=filtering_binary) + args = parser.parse_args() + + execute_test(args.memgraph, args.tester, args.filtering) + + sys.exit(0) diff --git a/tests/integration/lba/tester.cpp b/tests/integration/lba/tester.cpp new file mode 100644 index 000000000..904ab14a5 --- /dev/null +++ b/tests/integration/lba/tester.cpp @@ -0,0 +1,84 @@ +// 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. + +#include + +#include "communication/bolt/client.hpp" +#include "io/network/endpoint.hpp" +#include "io/network/utils.hpp" + +DEFINE_string(address, "127.0.0.1", "Server address"); +DEFINE_int32(port, 7687, "Server port"); +DEFINE_string(username, "", "Username for the database"); +DEFINE_string(password, "", "Password for the database"); +DEFINE_bool(use_ssl, false, "Set to true to connect with SSL to the server."); + +DEFINE_bool(check_failure, false, "Set to true to enable failure checking."); +DEFINE_bool(should_fail, false, "Set to true to expect a failure."); +DEFINE_string(failure_message, "", "Set to the expected failure message."); + +/** + * Executes queries passed as positional arguments and verifies whether they + * succeeded, failed, failed with a specific error message or executed without a + * specific error occurring. + */ +int main(int argc, char **argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + + memgraph::communication::SSLInit sslInit; + + memgraph::io::network::Endpoint endpoint(memgraph::io::network::ResolveHostname(FLAGS_address), FLAGS_port); + + memgraph::communication::ClientContext context(FLAGS_use_ssl); + memgraph::communication::bolt::Client client(&context); + + client.Connect(endpoint, FLAGS_username, FLAGS_password); + + for (int i = 1; i < argc; ++i) { + std::string query(argv[i]); + try { + client.Execute(query, {}); + } catch (const memgraph::communication::bolt::ClientQueryException &e) { + if (!FLAGS_check_failure) { + if (!FLAGS_failure_message.empty() && e.what() == FLAGS_failure_message) { + LOG_FATAL( + "The query should have succeeded or failed with an error " + "message that isn't equal to '{}' but it failed with that error " + "message", + FLAGS_failure_message); + } + continue; + } + if (FLAGS_should_fail) { + if (!FLAGS_failure_message.empty() && e.what() != FLAGS_failure_message) { + LOG_FATAL( + "The query should have failed with an error message of '{}'' but " + "instead it failed with '{}'", + FLAGS_failure_message, e.what()); + } + return 0; + } else { + LOG_FATAL( + "The query shoudn't have failed but it failed with an " + "error message '{}'", + e.what()); + } + } + if (!FLAGS_check_failure) continue; + if (FLAGS_should_fail) { + LOG_FATAL( + "The query should have failed but instead it executed " + "successfully!"); + } + } + + return 0; +} diff --git a/tests/unit/auth_checker.cpp b/tests/unit/auth_checker.cpp deleted file mode 100644 index 5bea001d3..000000000 --- a/tests/unit/auth_checker.cpp +++ /dev/null @@ -1,50 +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. - -#include -#include - -#include -#include - -#include "auth/auth.hpp" -#include "auth/crypto.hpp" -#include "auth/models.hpp" -#include "utils/cast.hpp" -#include "utils/file.hpp" -#include "utils/license.hpp" - -using namespace memgraph::auth; -namespace fs = std::filesystem; - -DECLARE_bool(auth_password_permit_null); -DECLARE_string(auth_password_strength_regex); - -class AuthWithStorage : public ::testing::Test { - protected: - virtual void SetUp() { - memgraph::utils::EnsureDir(test_folder_); - FLAGS_auth_password_permit_null = true; - FLAGS_auth_password_strength_regex = ".+"; - - memgraph::utils::license::global_license_checker.EnableTesting(); - } - - virtual void TearDown() { fs::remove_all(test_folder_); } - - fs::path test_folder_{fs::temp_directory_path() / "MG_tests_unit_auth"}; - - Auth auth{test_folder_ / ("unit_auth_test_" + std::to_string(static_cast(getpid())))}; -}; - -TEST_F(AuthWithStorage, IsUserAuthorizedLabels) { ASSERT_TRUE(true); } - -TEST_F(AuthWithStorage, IsUserAuthorizedEdgeType) { ASSERT_TRUE(true); }