Implement LDAP authentication

Reviewers: teon.banek

Reviewed By: teon.banek

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D1888
This commit is contained in:
Matej Ferencevic 2019-02-27 15:22:54 +01:00
parent 54b23ba5b6
commit d9bc4ec476
16 changed files with 885 additions and 6 deletions

35
cmake/FindLdap.cmake Normal file
View File

@ -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()

1
init
View File

@ -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
)

View File

@ -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)

View File

@ -1,8 +1,39 @@
#include "auth/auth.hpp"
#include <cstring>
#include <limits>
#include <utility>
#include <fmt/format.h>
#include <glog/logging.h>
#include <ldap.h>
#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<uint16_t>::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:<username>", value="<rolename>"
*/
#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<std::unique_ptr<char[]>, struct berval> LdapConvertString(
const std::string &s) {
std::unique_ptr<char[]> 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<std::string> 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<User> Auth::Authenticate(
const std::string &username, const std::string &password) {
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<std::string> 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<User> Auth::GetUser(
@ -185,7 +443,8 @@ std::vector<auth::Role> Auth::AllRoles() {
return ret;
}
std::vector<auth::User> Auth::AllUsersForRole(const std::string &rolename_orig) {
std::vector<auth::User> Auth::AllUsersForRole(
const std::string &rolename_orig) {
auto rolename = utils::ToLowerCase(rolename_orig);
std::vector<auth::User> ret;
for (auto it = storage_.begin(kLinkPrefix); it != storage_.end(kLinkPrefix);

View File

@ -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.

View File

@ -70,6 +70,7 @@ void SingleNodeMain() {
std::experimental::filesystem::path(FLAGS_durability_directory);
// Auth
auth::Init();
auth::Auth auth{durability_directory / "auth"};
// Audit log

View File

@ -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,

View File

@ -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",

View File

@ -24,3 +24,6 @@ add_subdirectory(ha/index)
# audit test binaries
add_subdirectory(audit)
# ldap test binaries
add_subdirectory(ldap)

View File

@ -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

1
tests/integration/ldap/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
openldap-2.4.47

View File

@ -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)

View File

@ -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!"

336
tests/integration/ldap/runner.py Executable file
View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,70 @@
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <json/json.hpp>
#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;
}