Implement LDAP authentication
Reviewers: teon.banek Reviewed By: teon.banek Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D1888
This commit is contained in:
parent
54b23ba5b6
commit
d9bc4ec476
35
cmake/FindLdap.cmake
Normal file
35
cmake/FindLdap.cmake
Normal 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
1
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
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
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<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);
|
||||
|
@ -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.
|
||||
|
@ -70,6 +70,7 @@ void SingleNodeMain() {
|
||||
std::experimental::filesystem::path(FLAGS_durability_directory);
|
||||
|
||||
// Auth
|
||||
auth::Init();
|
||||
auth::Auth auth{durability_directory / "auth"};
|
||||
|
||||
// Audit log
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -24,3 +24,6 @@ add_subdirectory(ha/index)
|
||||
|
||||
# audit test binaries
|
||||
add_subdirectory(audit)
|
||||
|
||||
# ldap test binaries
|
||||
add_subdirectory(ldap)
|
||||
|
@ -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
1
tests/integration/ldap/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
openldap-2.4.47
|
6
tests/integration/ldap/CMakeLists.txt
Normal file
6
tests/integration/ldap/CMakeLists.txt
Normal 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)
|
50
tests/integration/ldap/prepare.sh
Executable file
50
tests/integration/ldap/prepare.sh
Executable 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
336
tests/integration/ldap/runner.py
Executable 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)
|
92
tests/integration/ldap/schema.ldif
Normal file
92
tests/integration/ldap/schema.ldif
Normal 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
|
70
tests/integration/ldap/tester.cpp
Normal file
70
tests/integration/ldap/tester.cpp
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user