diff --git a/cmake/FindLdap.cmake b/cmake/FindLdap.cmake new file mode 100644 index 000000000..922db7461 --- /dev/null +++ b/cmake/FindLdap.cmake @@ -0,0 +1,35 @@ +# Find the OpenLDAP library. +# This module plugs into CMake's `find_package` so the example usage is: +# `find_package(Ldap REQUIRED)` +# Options to `find_package` are as documented in CMake documentation. +# LDAP_LIBRARY will be a path to the library. +# LDAP_INCLUDE_DIR will be a path to the include directory. +# LDAP_FOUND will be TRUE if the library is found. +# +# If the library is found, an imported target `ldap` will be provided. This +# can be used for linking via `target_link_libraries`, without the need to +# explicitly include LDAP_INCLUDE_DIR and link with LDAP_LIBRARY. For +# example: `target_link_libraries(my_executable ldap)`. +if (LDAP_LIBRARY AND LDAP_INCLUDE_DIR) + set(LDAP_FOUND TRUE) +else() + find_library(LDAP_LIBRARY ldap) + find_path(LDAP_INCLUDE_DIR ldap.h) + if (LDAP_LIBRARY AND LDAP_INCLUDE_DIR) + set(LDAP_FOUND TRUE) + if (NOT LDAP_FIND_QUIETLY) + message(STATUS "Found LDAP: ${LDAP_LIBRARY} ${LDAP_INCLUDE_DIR}") + endif() + else() + set(LDAP_FOUND FALSE) + if (LDAP_FIND_REQUIRED) + message(FATAL_ERROR "Could not find LDAP") + elseif (NOT LDAP_FIND_QUIETLY) + message(STATUS "Could not find LDAP") + endif() + endif() + mark_as_advanced(LDAP_LIBRARY LDAP_INCLUDE_DIR) + add_library(ldap SHARED IMPORTED) + set_property(TARGET ldap PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${LDAP_INCLUDE_DIR}) + set_property(TARGET ldap PROPERTY IMPORTED_LOCATION ${LDAP_LIBRARY}) +endif() diff --git a/init b/init index 822ae948b..273cd33ab 100755 --- a/init +++ b/init @@ -10,6 +10,7 @@ required_pkgs=(git arcanist # source code control python3 python-virtualenv python3-pip # for qa, macro_benchmark and stress tests uuid-dev # mg-utils libcurl4-openssl-dev # mg-requests + libldap2-dev # mg-auth sbcl # for custom Lisp C++ preprocessing ) diff --git a/src/auth/CMakeLists.txt b/src/auth/CMakeLists.txt index 102bd4f79..4bd867a02 100644 --- a/src/auth/CMakeLists.txt +++ b/src/auth/CMakeLists.txt @@ -3,6 +3,8 @@ set(auth_src_files crypto.cpp models.cpp) +find_package(Ldap REQUIRED) + add_library(mg-auth STATIC ${auth_src_files}) -target_link_libraries(mg-auth json libbcrypt) +target_link_libraries(mg-auth json libbcrypt glog gflags fmt ldap) target_link_libraries(mg-auth mg-utils) diff --git a/src/auth/auth.cpp b/src/auth/auth.cpp index e492daa4a..330ebbb20 100644 --- a/src/auth/auth.cpp +++ b/src/auth/auth.cpp @@ -1,8 +1,39 @@ #include "auth/auth.hpp" +#include +#include +#include + +#include +#include + +#include + #include "auth/exceptions.hpp" +#include "utils/flag_validation.hpp" +#include "utils/on_scope_exit.hpp" #include "utils/string.hpp" +DEFINE_bool(auth_ldap_enabled, false, + "Set to true to enable LDAP authentication."); +DEFINE_bool( + auth_ldap_issue_starttls, false, + "Set to true to enable issuing of STARTTLS on LDAP server connections."); +DEFINE_string(auth_ldap_prefix, "cn=", + "The prefix used when forming the DN for LDAP authentication."); +DEFINE_string(auth_ldap_suffix, "", + "The suffix used when forming the DN for LDAP authentication."); +DEFINE_string(auth_ldap_host, "", "Host used for LDAP authentication."); +DEFINE_VALIDATED_int32(auth_ldap_port, LDAP_PORT, + "Port used for LDAP authentication.", + FLAG_IN_RANGE(1, std::numeric_limits::max())); +DEFINE_bool(auth_ldap_create_user, true, + "Set to false to disable creation of missing users."); +DEFINE_bool(auth_ldap_create_role, true, + "Set to false to disable creation of missing roles."); +DEFINE_string(auth_ldap_role_mapping_root_dn, "", + "Set this value to the DN that contains all role mappings."); + namespace auth { const std::string kUserPrefix = "user:"; @@ -26,15 +57,242 @@ const std::string kLinkPrefix = "link:"; * key="link:", value="" */ +#define INIT_ABORT_ON_ERROR(expr) \ + CHECK(expr == LDAP_SUCCESS) << "Couldn't initialize auth stack!"; + +void Init() { + // The OpenLDAP manual states that we should call either `ldap_set_option` or + // `ldap_get_option` once from a single thread so that the internal state of + // the library is initialized. This is noted in the manual for + // `ldap_initialize` under the 'Note:' + // ``` + // Note: the first call into the LDAP library also initializes the global + // options for the library. As such the first call should be single- + // threaded or otherwise protected to insure that only one call is active. + // It is recommended that ldap_get_option() or ldap_set_option() be used + // in the program's main thread before any additional threads are created. + // See ldap_get_option(3). + // ``` + // https://www.openldap.org/software/man.cgi?query=ldap_initialize&sektion=3&apropos=0&manpath=OpenLDAP+2.4-Release + LDAP *ld = nullptr; + INIT_ABORT_ON_ERROR(ldap_initialize(&ld, "")); + int ldap_version = LDAP_VERSION3; + INIT_ABORT_ON_ERROR( + ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ldap_version)); + INIT_ABORT_ON_ERROR(ldap_unbind_ext(ld, NULL, NULL)); +} + Auth::Auth(const std::string &storage_directory) : storage_(storage_directory) {} +/// Converts a `std::string` to a `struct berval`. +std::pair, struct berval> LdapConvertString( + const std::string &s) { + std::unique_ptr data(new char[s.size() + 1]); + char *ptr = data.get(); + memcpy(ptr, s.c_str(), s.size()); + ptr[s.size()] = '\0'; + return {std::move(data), {s.size(), ptr}}; +} + +/// Escapes a string so that it can't be used for LDAP DN injection. +/// https://ldapwiki.com/wiki/DN%20Escape%20Values +std::string LdapEscapeString(const std::string &src) { + std::string ret; + ret.reserve(src.size() * 2); + + int spaces_leading = 0, spaces_trailing = 0; + for (int i = 0; i < src.size(); ++i) { + if (src[i] == ' ') { + ++spaces_leading; + } else { + break; + } + } + for (int i = src.size() - 1; i >= 0; --i) { + if (src[i] == ' ') { + ++spaces_trailing; + } else { + break; + } + } + + for (int i = 0; i < spaces_leading; ++i) { + ret.append("\\ "); + } + for (int i = spaces_leading; i < src.size() - spaces_trailing; ++i) { + char c = src[i]; + if (c == ',' || c == '\\' || c == '#' || c == '+' || c == '<' || c == '>' || + c == ';' || c == '"' || c == '=') { + ret.append(1, '\\'); + } + ret.append(1, c); + } + for (int i = 0; i < spaces_trailing; ++i) { + ret.append("\\ "); + } + + return ret; +} + +/// This function searches for a role mapping for the given `user_dn` by +/// searching all first level children of the `role_base_dn` and finding that +/// item that has a `mapping` attribute to the given `user_dn`. The found item's +/// `cn` is used as the role name. +std::experimental::optional LdapFindRole( + LDAP *ld, const std::string &role_base_dn, const std::string &user_dn, + const std::string &username) { + auto ldap_user_dn = LdapConvertString(user_dn); + + char *attrs[1] = {nullptr}; + LDAPMessage *msg = nullptr; + + int ret = + ldap_search_ext_s(ld, role_base_dn.c_str(), LDAP_SCOPE_ONELEVEL, NULL, + attrs, 0, NULL, NULL, NULL, LDAP_NO_LIMIT, &msg); + utils::OnScopeExit cleanup([&msg] { ldap_msgfree(msg); }); + + if (ret != LDAP_SUCCESS) { + LOG(WARNING) << "Couldn't find role for user '" << username + << "' using LDAP due to error: " << ldap_err2string(ret); + return std::experimental::nullopt; + } + + if (ret == LDAP_SUCCESS && msg != nullptr) { + for (LDAPMessage *entry = ldap_first_entry(ld, msg); entry != nullptr; + entry = ldap_next_entry(ld, entry)) { + char *entry_dn = ldap_get_dn(ld, entry); + ret = ldap_compare_ext_s(ld, entry_dn, "member", &ldap_user_dn.second, + NULL, NULL); + ldap_memfree(entry_dn); + if (ret == LDAP_COMPARE_TRUE) { + auto values = ldap_get_values_len(ld, entry, "cn"); + if (ldap_count_values_len(values) != 1) { + LOG(WARNING) << "Couldn't find role for user '" << username + << "' using LDAP because to the role object doesn't " + "have a unique CN attribute!"; + return std::experimental::nullopt; + } + return std::string(values[0]->bv_val, values[0]->bv_len); + } else if (ret != LDAP_COMPARE_FALSE) { + LOG(WARNING) << "Couldn't find role for user '" << username + << "' using LDAP due to error: " << ldap_err2string(ret); + return std::experimental::nullopt; + } + } + } + return std::experimental::nullopt; +} + +#define LDAP_EXIT_ON_ERROR(expr, username) \ + { \ + int r = expr; \ + if (r != LDAP_SUCCESS) { \ + LOG(WARNING) << "Couldn't authenticate user '" << username \ + << "' using LDAP due to error: " << ldap_err2string(r); \ + return std::experimental::nullopt; \ + } \ + } + std::experimental::optional Auth::Authenticate( const std::string &username, const std::string &password) { - auto user = GetUser(username); - if (!user) return std::experimental::nullopt; - if (!user->CheckPassword(password)) return std::experimental::nullopt; - return user; + if (FLAGS_auth_ldap_enabled) { + LDAP *ld = nullptr; + + // Initialize the LDAP struct. + std::string uri = + fmt::format("ldap://{}:{}", FLAGS_auth_ldap_host, FLAGS_auth_ldap_port); + LDAP_EXIT_ON_ERROR(ldap_initialize(&ld, uri.c_str()), username); + + // After this point the struct is valid and we need to clean it up on exit. + utils::OnScopeExit cleanup([&ld] { ldap_unbind_ext(ld, NULL, NULL); }); + + // Set protocol version used. + int ldap_version = LDAP_VERSION3; + LDAP_EXIT_ON_ERROR( + ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ldap_version), + username); + + // Create DN used for authentication. + std::string distinguished_name = FLAGS_auth_ldap_prefix + + LdapEscapeString(username) + + FLAGS_auth_ldap_suffix; + + // Issue STARTTLS if we are using TLS. + if (FLAGS_auth_ldap_issue_starttls) { + LDAP_EXIT_ON_ERROR(ldap_start_tls_s(ld, NULL, NULL), username); + } + + // Try to authenticate. + // Since `ldap_simple_bind_s` is now deprecated, we use `ldap_sasl_bind_s` + // to emulate the simple bind behavior. This is inspired by the following + // link. They use the async version, we use the sync version. + // https://github.com/openldap/openldap/blob/b45a6a7dc728d9df18aa1ca7a9aa43dabb1d4037/clients/tools/common.c#L1618 + { + auto cred = LdapConvertString(password); + LDAP_EXIT_ON_ERROR( + ldap_sasl_bind_s(ld, distinguished_name.c_str(), LDAP_SASL_SIMPLE, + &cred.second, NULL, NULL, NULL), + username); + } + + // Find role name. + std::experimental::optional rolename; + if (!FLAGS_auth_ldap_role_mapping_root_dn.empty()) { + rolename = LdapFindRole(ld, FLAGS_auth_ldap_role_mapping_root_dn, + distinguished_name, username); + } + + // Find or create the user and return it. + auto user = GetUser(username); + if (!user) { + if (FLAGS_auth_ldap_create_user) { + user = AddUser(username, password); + if (!user) { + LOG(WARNING) + << "Couldn't authenticate user '" << username + << "' using LDAP because the user already exists as a role!"; + return std::experimental::nullopt; + } + } else { + LOG(WARNING) << "Couldn't authenticate user '" << username + << "' using LDAP because the user doesn't exist!"; + return std::experimental::nullopt; + } + } else { + user->UpdatePassword(password); + } + if (rolename) { + auto role = GetRole(*rolename); + if (!role) { + if (FLAGS_auth_ldap_create_role) { + role = AddRole(*rolename); + if (!role) { + LOG(WARNING) << "Couldn't authenticate user '" << username + << "' using LDAP because the user's role '" + << *rolename << "' already exists as a user!"; + return std::experimental::nullopt; + } + SaveRole(*role); + } else { + LOG(WARNING) << "Couldn't authenticate user '" << username + << "' using LDAP because the user's role '" << *rolename + << "' doesn't exist!"; + return std::experimental::nullopt; + } + } + user->SetRole(*role); + } else { + user->ClearRole(); + } + SaveUser(*user); + return user; + } else { + auto user = GetUser(username); + if (!user) return std::experimental::nullopt; + if (!user->CheckPassword(password)) return std::experimental::nullopt; + return user; + } } std::experimental::optional Auth::GetUser( @@ -185,7 +443,8 @@ std::vector Auth::AllRoles() { return ret; } -std::vector Auth::AllUsersForRole(const std::string &rolename_orig) { +std::vector Auth::AllUsersForRole( + const std::string &rolename_orig) { auto rolename = utils::ToLowerCase(rolename_orig); std::vector ret; for (auto it = storage_.begin(kLinkPrefix); it != storage_.end(kLinkPrefix); diff --git a/src/auth/auth.hpp b/src/auth/auth.hpp index ef070fe36..40b41f757 100644 --- a/src/auth/auth.hpp +++ b/src/auth/auth.hpp @@ -10,6 +10,14 @@ namespace auth { +/** + * Call this function in each `main` file that uses the Auth stack. It is used + * to initialize all libraries (primarily OpenLDAP). + * + * NOTE: This function must be called **exactly** once. + */ +void Init(); + /** * This class serves as the main Authentication/Authorization storage. * It provides functions for managing Users, Roles and Permissions. diff --git a/src/memgraph.cpp b/src/memgraph.cpp index 7ec5a5bd4..b1f7ce7a2 100644 --- a/src/memgraph.cpp +++ b/src/memgraph.cpp @@ -70,6 +70,7 @@ void SingleNodeMain() { std::experimental::filesystem::path(FLAGS_durability_directory); // Auth + auth::Init(); auth::Auth auth{durability_directory / "auth"}; // Audit log diff --git a/src/memgraph_distributed.cpp b/src/memgraph_distributed.cpp index eaecec94c..996eb109a 100644 --- a/src/memgraph_distributed.cpp +++ b/src/memgraph_distributed.cpp @@ -68,6 +68,7 @@ void MasterMain() { auto durability_directory = std::experimental::filesystem::path(FLAGS_durability_directory); + auth::Init(); auth::Auth auth{durability_directory / "auth"}; audit::Log audit_log{durability_directory / "audit", FLAGS_audit_buffer_size, diff --git a/tests/feature_benchmark/kafka/benchmark.cpp b/tests/feature_benchmark/kafka/benchmark.cpp index a4c5e3fa3..42a583e8f 100644 --- a/tests/feature_benchmark/kafka/benchmark.cpp +++ b/tests/feature_benchmark/kafka/benchmark.cpp @@ -33,6 +33,7 @@ void KafkaBenchmarkMain() { auto durability_directory = std::experimental::filesystem::path(FLAGS_durability_directory); + auth::Init(); auth::Auth auth{durability_directory / "auth"}; audit::Log audit_log{durability_directory / "audit", diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt index 7b2368758..e786c5806 100644 --- a/tests/integration/CMakeLists.txt +++ b/tests/integration/CMakeLists.txt @@ -24,3 +24,6 @@ add_subdirectory(ha/index) # audit test binaries add_subdirectory(audit) + +# ldap test binaries +add_subdirectory(ldap) diff --git a/tests/integration/apollo_runs.yaml b/tests/integration/apollo_runs.yaml index 3e595d0cc..6a8e47aea 100644 --- a/tests/integration/apollo_runs.yaml +++ b/tests/integration/apollo_runs.yaml @@ -51,6 +51,19 @@ - ../../../build_debug/memgraph # memgraph binary - ../../../build_debug/tests/integration/audit/tester # tester binary +- name: integration__ldap + cd: ldap + commands: | + ./prepare.sh + ./runner.py + infiles: + - prepare.sh # preparation script + - runner.py # runner script + - schema.ldif # schema file + - ../../../build_debug/memgraph # memgraph binary + - ../../../build_debug/tests/integration/ldap/tester # tester binary + enable_network: true + - name: integration__distributed cd: distributed commands: TIMEOUT=480 ./runner.py diff --git a/tests/integration/ldap/.gitignore b/tests/integration/ldap/.gitignore new file mode 100644 index 000000000..dd5fce91b --- /dev/null +++ b/tests/integration/ldap/.gitignore @@ -0,0 +1 @@ +openldap-2.4.47 diff --git a/tests/integration/ldap/CMakeLists.txt b/tests/integration/ldap/CMakeLists.txt new file mode 100644 index 000000000..8d33f02bd --- /dev/null +++ b/tests/integration/ldap/CMakeLists.txt @@ -0,0 +1,6 @@ +set(target_name memgraph__integration__ldap) +set(tester_target_name ${target_name}__tester) + +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 json) diff --git a/tests/integration/ldap/prepare.sh b/tests/integration/ldap/prepare.sh new file mode 100755 index 000000000..4a7704812 --- /dev/null +++ b/tests/integration/ldap/prepare.sh @@ -0,0 +1,50 @@ +#!/bin/bash -e + +function echo_info { printf "\n\033[1;36m~~ $1 ~~\033[0m\n"; } +function echo_success { printf "\n\n\033[1;32m~~ $1 ~~\033[0m\n"; } + +CPUS=$( cat /proc/cpuinfo | grep processor | wc -l ) +NPROC=$(( $CPUS / 2 )) + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "$DIR" + +version="2.4.47" +name="openldap-$version" + +if [ -d "$name" ]; then + rm -rf "$name" +fi + +echo_info "Downloading and unpacking OpenLDAP" +wget -nv -O $name.tgz http://deps.memgraph.io/$name.tgz +tar -xf $name.tgz +rm $name.tgz + +cd "$name" + +echo_info "Configuring OpenLDAP build" +sed 's/include\ libraries\ clients\ servers\ tests\ doc/include libraries servers/g' -i Makefile.in +./configure --prefix="$DIR/$name/exe" \ + --disable-bdb \ + --disable-hdb \ + --disable-relay \ + --disable-monitor \ + --disable-dependency-tracking + +echo_info "Building dependencies" +make depend -j$NPROC + +echo_info "Building OpenLDAP" +make -j$NPROC + +echo_info "Installing OpenLDAP" +make install -j$NPROC + +echo_info "Configuring and importing schema" +sed 's/my-domain/memgraph/g' -i exe/etc/openldap/slapd.conf +sed 's/Manager/admin/g' -i exe/etc/openldap/slapd.conf +mkdir exe/var/openldap-data +./exe/sbin/slapadd -l ../schema.ldif + +echo_success "Done!" diff --git a/tests/integration/ldap/runner.py b/tests/integration/ldap/runner.py new file mode 100755 index 000000000..6dbbb2412 --- /dev/null +++ b/tests/integration/ldap/runner.py @@ -0,0 +1,336 @@ +#!/usr/bin/python3 -u +import argparse +import atexit +import os +import subprocess +import sys +import tempfile +import time + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "..")) + + +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, username="", password="", + auth_should_fail=False, query_should_fail=False): + if password == "": + password = username + args = [binary, "--username", username, "--password", password] + if auth_should_fail: + args.append("--auth-should-fail") + if query_should_fail: + args.append("--query-should-fail") + args.extend(queries) + subprocess.run(args).check_returncode() + + +class Memgraph: + def __init__(self, binary): + self._binary = binary + self._storage_directory = None + self._process = None + + def start(self, args=[]): + self.stop() + self._storage_directory = tempfile.TemporaryDirectory() + self.restart(args) + + def restart(self, args=[]): + self.stop() + args = [self._binary, "--durability-directory", + self._storage_directory.name] + list(map(str, args)) + self._process = subprocess.Popen(args) + time.sleep(0.1) + assert self._process.poll() is None, "Memgraph process died " \ + "prematurely!" + wait_for_server(7687) + + def stop(self, check=True): + if self._process is None: + return 0 + self._process.terminate() + exitcode = self._process.wait() + self._process = None + if check: + assert exitcode == 0, "Memgraph process didn't exit cleanly!" + return exitcode + + +def restart_memgraph(memgraph, tester_binary, **kwargs): + args = ["--auth-ldap-enabled", "--auth-ldap-host", "127.0.0.1", + "--auth-ldap-port", "1389"] + if "prefix" not in kwargs: + kwargs["prefix"] = "cn=" + if "suffix" not in kwargs: + kwargs["suffix"] = ",ou=people,dc=memgraph,dc=com" + for key, value in kwargs.items(): + ldap_key = "--auth-ldap-" + key.replace("_", "-") + if type(value) == bool: + args.append(ldap_key + "=" + str(value).lower()) + else: + args.append(ldap_key) + args.append(value) + memgraph.restart(args) + + +def initialize_test(memgraph, tester_binary, **kwargs): + memgraph.start() + execute_tester(tester_binary, + ["CREATE USER root", "GRANT ALL PRIVILEGES TO root"]) + check_login = kwargs.pop("check_login", True) + restart_memgraph(memgraph, tester_binary, **kwargs) + if check_login: + execute_tester(tester_binary, [], "root") + + +# Tests + + +def test_basic(memgraph, tester_binary): + initialize_test(memgraph, tester_binary) + execute_tester(tester_binary, [], "alice") + execute_tester(tester_binary, ["GRANT MATCH TO alice"], "root") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") + memgraph.stop() + + +def test_only_existing_users(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, create_user=False) + execute_tester(tester_binary, [], "alice", auth_should_fail=True) + execute_tester(tester_binary, ["CREATE USER alice"], "root") + execute_tester(tester_binary, [], "alice") + execute_tester(tester_binary, ["GRANT MATCH TO alice"], "root") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") + memgraph.stop() + + +def test_role_mapping(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, + role_mapping_root_dn="ou=roles,dc=memgraph,dc=com") + + execute_tester(tester_binary, [], "alice") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", + query_should_fail=True) + execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") + + execute_tester(tester_binary, [], "bob") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "bob", + query_should_fail=True) + + execute_tester(tester_binary, [], "carol") + execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol", + query_should_fail=True) + execute_tester(tester_binary, ["GRANT CREATE TO admin"], "root") + execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol") + execute_tester(tester_binary, ["CREATE (n) RETURN n"], "dave") + + memgraph.stop() + + +def test_role_removal(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, + role_mapping_root_dn="ou=roles,dc=memgraph,dc=com") + execute_tester(tester_binary, [], "alice") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", + query_should_fail=True) + execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") + restart_memgraph(memgraph, tester_binary) + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", + query_should_fail=True) + memgraph.stop() + + +def test_only_existing_roles(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, + role_mapping_root_dn="ou=roles,dc=memgraph,dc=com", + create_role=False) + execute_tester(tester_binary, [], "bob") + execute_tester(tester_binary, [], "alice", auth_should_fail=True) + execute_tester(tester_binary, ["CREATE ROLE moderator"], "root") + execute_tester(tester_binary, [], "alice") + memgraph.stop() + + +def test_role_is_user(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, + role_mapping_root_dn="ou=roles,dc=memgraph,dc=com") + execute_tester(tester_binary, [], "admin") + execute_tester(tester_binary, [], "carol", auth_should_fail=True) + memgraph.stop() + + +def test_user_is_role(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, + role_mapping_root_dn="ou=roles,dc=memgraph,dc=com") + execute_tester(tester_binary, [], "carol") + execute_tester(tester_binary, [], "admin", auth_should_fail=True) + memgraph.stop() + + +def test_user_permissions_persistancy(memgraph, tester_binary): + initialize_test(memgraph, tester_binary) + execute_tester(tester_binary, + ["CREATE USER alice", "GRANT MATCH TO alice"], "root") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") + memgraph.stop() + + +def test_role_permissions_persistancy(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, + role_mapping_root_dn="ou=roles,dc=memgraph,dc=com") + execute_tester(tester_binary, + ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], + "root") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") + memgraph.stop() + + +def test_only_authentication(memgraph, tester_binary): + initialize_test(memgraph, tester_binary) + execute_tester(tester_binary, + ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], + "root") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", + query_should_fail=True) + memgraph.stop() + + +def test_wrong_prefix(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, prefix="eve", check_login=False) + execute_tester(tester_binary, [], "root", auth_should_fail=True) + memgraph.stop() + + +def test_wrong_suffix(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, suffix="", check_login=False) + execute_tester(tester_binary, [], "root", auth_should_fail=True) + memgraph.stop() + + +def test_suffix_with_spaces(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, + suffix=", ou= people, dc = memgraph, dc = com") + execute_tester(tester_binary, + ["CREATE USER alice", "GRANT MATCH TO alice"], "root") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") + memgraph.stop() + + +def test_role_mapping_wrong_root_dn(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, + role_mapping_root_dn="ou=invalid,dc=memgraph,dc=com") + execute_tester(tester_binary, + ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], + "root") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", + query_should_fail=True) + restart_memgraph(memgraph, tester_binary, + role_mapping_root_dn="ou=roles,dc=memgraph,dc=com") + execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice") + memgraph.stop() + + +def test_wrong_password(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, + role_mapping_root_dn="ou=roles,dc=memgraph,dc=com") + execute_tester(tester_binary, [], "root", password="sudo", + auth_should_fail=True) + execute_tester(tester_binary, ["SHOW USERS"], "root", password="root") + memgraph.stop() + + +def test_password_persistancy(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, check_login=False) + memgraph.restart() + execute_tester(tester_binary, ["SHOW USERS"], "root", password="sudo") + execute_tester(tester_binary, ["SHOW USERS"], "root", password="root") + restart_memgraph(memgraph, tester_binary) + execute_tester(tester_binary, [], "root", password="sudo", + auth_should_fail=True) + execute_tester(tester_binary, ["SHOW USERS"], "root", password="root") + memgraph.restart() + execute_tester(tester_binary, [], "root", password="sudo", + auth_should_fail=True) + execute_tester(tester_binary, ["SHOW USERS"], "root", password="root") + memgraph.stop() + + +def test_starttls_failure(memgraph, tester_binary): + initialize_test(memgraph, tester_binary, issue_starttls=True, + check_login=False) + execute_tester(tester_binary, [], "root", auth_should_fail=True) + memgraph.stop() + + +# Startup logic + + +if __name__ == "__main__": + memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph") + if not os.path.exists(memgraph_binary): + memgraph_binary = os.path.join(PROJECT_DIR, "build_debug", "memgraph") + tester_binary = os.path.join(PROJECT_DIR, "build", "tests", + "integration", "ldap", "tester") + if not os.path.exists(tester_binary): + tester_binary = os.path.join(PROJECT_DIR, "build_debug", "tests", + "integration", "ldap", "tester") + + parser = argparse.ArgumentParser() + parser.add_argument("--memgraph", default=memgraph_binary) + parser.add_argument("--tester", default=tester_binary) + parser.add_argument("--openldap-dir", + default=os.path.join(SCRIPT_DIR, "openldap-2.4.47")) + args = parser.parse_args() + + # Setup Memgraph handler + memgraph = Memgraph(args.memgraph) + + # Start the slapd binary + slapd_args = [os.path.join(args.openldap_dir, "exe", "libexec", "slapd"), + "-h", "ldap://127.0.0.1:1389/", "-d", "0"] + slapd = subprocess.Popen(slapd_args) + time.sleep(0.1) + assert slapd.poll() is None, "slapd process died prematurely!" + wait_for_server(1389) + + # Register cleanup function + @atexit.register + def cleanup(): + mg_stat = memgraph.stop(check=False) + if mg_stat != 0: + print("Memgraph process didn't exit cleanly!") + + if slapd.poll() is None: + slapd.terminate() + slapd_stat = slapd.wait() + if slapd_stat != 0: + print("slapd process didn't exit cleanly!") + + assert mg_stat == 0 and slapd_stat == 0, "Some of the processes " \ + "(memgraph, slapd) crashed!" + + # Execute tests + names = sorted(globals().keys()) + for name in names: + if not name.startswith("test_"): + continue + test = " ".join(name[5:].split("_")) + func = globals()[name] + print("\033[1;36m~~ Running", test, "test ~~\033[0m") + func(memgraph, args.tester) + print("\033[1;36m~~ Finished", test, "test ~~\033[0m\n") + + # Shutdown the slapd binary + slapd.terminate() + assert slapd.wait() == 0, "slapd process didn't exit cleanly!" + + sys.exit(0) diff --git a/tests/integration/ldap/schema.ldif b/tests/integration/ldap/schema.ldif new file mode 100644 index 000000000..cbadd563e --- /dev/null +++ b/tests/integration/ldap/schema.ldif @@ -0,0 +1,92 @@ +# LDAP integration test schema + +# Root object +dn: dc=memgraph,dc=com +dc: memgraph +o: example +objectclass: top +objectclass: dcObject +objectclass: organization + +# Admin user for LDAP +dn: cn=admin,dc=memgraph,dc=com +cn: admin +description: LDAP administrator +objectclass: simpleSecurityObject +objectclass: organizationalRole +userpassword: secret + +# Users root object +dn: ou=people,dc=memgraph,dc=com +objectclass: organizationalUnit +objectclass: top +ou: people + +# User root +dn: cn=root,ou=people,dc=memgraph,dc=com +cn: root +objectclass: person +objectclass: top +sn: user +userpassword: root + +# User alice +dn: cn=alice,ou=people,dc=memgraph,dc=com +cn: alice +objectclass: person +objectclass: top +sn: user +userpassword: alice + +# User bob +dn: cn=bob,ou=people,dc=memgraph,dc=com +cn: bob +objectclass: person +objectclass: top +sn: user +userpassword: bob + +# User carol +dn: cn=carol,ou=people,dc=memgraph,dc=com +cn: carol +objectclass: person +objectclass: top +sn: user +userpassword: carol + +# User dave +dn: cn=dave,ou=people,dc=memgraph,dc=com +cn: dave +objectclass: person +objectclass: top +sn: user +userpassword: dave + +# User admin +dn: cn=admin,ou=people,dc=memgraph,dc=com +cn: admin +objectclass: person +objectclass: top +sn: user +userpassword: admin + +# Roles root object +dn: ou=roles,dc=memgraph,dc=com +objectclass: organizationalUnit +objectclass: top +ou: roles + +# Role moderator +dn: cn=moderator,ou=roles,dc=memgraph,dc=com +cn: moderator +member: cn=alice,ou=people,dc=memgraph,dc=com +objectclass: groupOfNames +objectclass: top + +# Role admin +dn: cn=admin,ou=roles,dc=memgraph,dc=com +cn: admin +member: cn=carol,ou=people,dc=memgraph,dc=com +member: cn=dave,ou=people,dc=memgraph,dc=com +objectclass: groupOfNames +objectclass: top diff --git a/tests/integration/ldap/tester.cpp b/tests/integration/ldap/tester.cpp new file mode 100644 index 000000000..3e735300d --- /dev/null +++ b/tests/integration/ldap/tester.cpp @@ -0,0 +1,70 @@ +#include +#include +#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(auth_should_fail, false, + "Set to true to expect authentication failure."); +DEFINE_bool(query_should_fail, false, + "Set to true to expect query execution failure."); + +/** + * Logs in to the server and executes the queries specified as arguments. On any + * errors it exits with a non-zero exit code. + */ +int main(int argc, char **argv) { + gflags::ParseCommandLineFlags(&argc, &argv, true); + google::InitGoogleLogging(argv[0]); + + communication::Init(); + + io::network::Endpoint endpoint(io::network::ResolveHostname(FLAGS_address), + FLAGS_port); + + communication::ClientContext context(FLAGS_use_ssl); + communication::bolt::Client client(&context); + + { + std::string what; + try { + client.Connect(endpoint, FLAGS_username, FLAGS_password); + } catch (const communication::bolt::ClientFatalException &e) { + what = e.what(); + } + if (FLAGS_auth_should_fail) { + CHECK(!what.empty()) << "The authentication should have failed!"; + } else { + CHECK(what.empty()) << "The authentication should have succeeded, but " + "failed with message: " + << what; + } + } + + for (int i = 1; i < argc; ++i) { + std::string query(argv[i]); + std::string what; + try { + client.Execute(query, {}); + } catch (const communication::bolt::ClientQueryException &e) { + what = e.what(); + } + if (FLAGS_query_should_fail) { + CHECK(!what.empty()) << "The query execution should have failed!"; + } else { + CHECK(what.empty()) << "The query execution should have succeeded, but " + "failed with message: " + << what; + } + } + + return 0; +}