Migrate LDAP integration to auth module
Reviewers: teon.banek, buda Reviewed By: teon.banek Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D2368
This commit is contained in:
parent
73379bc57e
commit
68f19df305
@ -1,35 +0,0 @@
|
||||
# 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()
|
4
init
4
init
@ -10,7 +10,6 @@ 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
|
||||
)
|
||||
|
||||
@ -147,4 +146,7 @@ setup_virtualenv tests/qa
|
||||
# setup stress dependencies
|
||||
setup_virtualenv tests/stress
|
||||
|
||||
# setup integration/ldap dependencies
|
||||
setup_virtualenv tests/integration/ldap
|
||||
|
||||
echo "Done installing dependencies for Memgraph"
|
||||
|
@ -4,11 +4,10 @@ set(auth_src_files
|
||||
models.cpp
|
||||
module.cpp)
|
||||
|
||||
find_package(Ldap REQUIRED)
|
||||
find_package(Seccomp REQUIRED)
|
||||
|
||||
add_library(mg-auth STATIC ${auth_src_files})
|
||||
target_link_libraries(mg-auth json libbcrypt glog gflags fmt ldap)
|
||||
target_link_libraries(mg-auth json libbcrypt glog gflags fmt)
|
||||
target_link_libraries(mg-auth mg-utils)
|
||||
|
||||
target_link_libraries(mg-auth ${Seccomp_LIBRARIES})
|
||||
|
@ -8,33 +8,10 @@
|
||||
#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.");
|
||||
|
||||
DEFINE_VALIDATED_string(
|
||||
auth_module_executable, "",
|
||||
"Absolute path to the auth module executable that should be used.", {
|
||||
@ -83,144 +60,9 @@ 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), module_(FLAGS_auth_module_executable) {}
|
||||
|
||||
/// 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::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::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::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::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::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::nullopt; \
|
||||
} \
|
||||
}
|
||||
|
||||
std::optional<User> Auth::Authenticate(const std::string &username,
|
||||
const std::string &password) {
|
||||
if (module_.IsUsed()) {
|
||||
@ -294,97 +136,6 @@ std::optional<User> Auth::Authenticate(const std::string &username,
|
||||
}
|
||||
SaveUser(*user);
|
||||
return user;
|
||||
} else 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::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::nullopt;
|
||||
}
|
||||
} else {
|
||||
LOG(WARNING) << "Couldn't authenticate user '" << username
|
||||
<< "' using LDAP because the user doesn't exist!";
|
||||
return std::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::nullopt;
|
||||
}
|
||||
SaveRole(*role);
|
||||
} else {
|
||||
LOG(WARNING) << "Couldn't authenticate user '" << username
|
||||
<< "' using LDAP because the user's role '" << *rolename
|
||||
<< "' doesn't exist!";
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
user->SetRole(*role);
|
||||
} else {
|
||||
user->ClearRole();
|
||||
}
|
||||
SaveUser(*user);
|
||||
return user;
|
||||
} else {
|
||||
auto user = GetUser(username);
|
||||
if (!user) return std::nullopt;
|
||||
|
@ -11,14 +11,6 @@
|
||||
|
||||
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.
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import io
|
||||
|
||||
|
46
src/auth/reference_modules/ldap.example.yaml
Normal file
46
src/auth/reference_modules/ldap.example.yaml
Normal file
@ -0,0 +1,46 @@
|
||||
# General LDAP server connection settings.
|
||||
server:
|
||||
# LDAP server host name
|
||||
host: "127.0.0.1"
|
||||
# LDAP server port
|
||||
port: 389
|
||||
# Encryption settings, valid options are:
|
||||
# "disabled" --> don't use SSL
|
||||
# "starttls" --> issue STARTTLS on plaintext connection
|
||||
# "ssl" --> use SSL directly
|
||||
encryption: "disabled"
|
||||
# Path to SSL certificate file
|
||||
cert_file: ""
|
||||
# Path to SSL key file
|
||||
key_file: ""
|
||||
# Path to SSL CA file
|
||||
ca_file: ""
|
||||
# Set to true to enable server certificate validation
|
||||
validate_cert: false
|
||||
|
||||
# Settings used to form the user DN that will be used to bind to the LDAP
|
||||
# server.
|
||||
# DN = prefix + username + suffix
|
||||
users:
|
||||
# Prefix used when forming the user DN
|
||||
prefix: "cn="
|
||||
# Suffix used when forming the user DN
|
||||
suffix: ""
|
||||
|
||||
# Settings used to map a user to a role. The module expects that all user to
|
||||
# role mapping objects are children of a single root object whose DN and
|
||||
# objectclass needs to be specified (`root_dn` and `root_objectclass`). The
|
||||
# module then searches all children of the root object to find an object that
|
||||
# has an attribute (`mapping_attribute`) that is equal to the user's DN. The
|
||||
# object's `role_attribute` is then used to determine the user's role.
|
||||
# NOTE: If you don't want to determine roles using LDAP, just leave the
|
||||
# `root_dn` parameter empty.
|
||||
roles:
|
||||
# Object that contains all user to role mapping objects
|
||||
root_dn: ""
|
||||
# Type of the object that contains all user to role mapping objects
|
||||
root_objectclass: ""
|
||||
# Attribute that contains the user DN
|
||||
user_attribute: ""
|
||||
# Attribute that contains the role name
|
||||
role_attribute: ""
|
88
src/auth/reference_modules/ldap.py
Executable file
88
src/auth/reference_modules/ldap.py
Executable file
@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import io
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
import ldap3
|
||||
import yaml
|
||||
|
||||
# Load config file.
|
||||
CONFIG_FILE = "/etc/memgraph/auth/ldap.yaml"
|
||||
with open(CONFIG_FILE) as f:
|
||||
config = yaml.safe_load(f)
|
||||
server_config = config["server"]
|
||||
users_config = config["users"]
|
||||
roles_config = config["roles"]
|
||||
|
||||
# Initialize LDAP server.
|
||||
tls = None
|
||||
if server_config["encryption"] != "disabled":
|
||||
cert_file = server_config["cert_file"] if server_config["cert_file"] \
|
||||
else None
|
||||
key_file = server_config["key_file"] if server_config["key_file"] else None
|
||||
ca_file = server_config["ca_file"] if server_config["ca_file"] else None
|
||||
validate = ssl.CERT_REQUIRED if server_config["validate_cert"] \
|
||||
else ssl.CERT_NONE
|
||||
tls = ldap3.Tls(local_private_key_file=key_file,
|
||||
local_certificate_file=cert_file,
|
||||
ca_certs_file=ca_file,
|
||||
validate=validate)
|
||||
use_ssl = server_config["encryption"] == "ssl"
|
||||
server = ldap3.Server(server_config["host"], port=server_config["port"],
|
||||
tls=tls, use_ssl=use_ssl, get_info=ldap3.ALL)
|
||||
|
||||
|
||||
# Main authentication/authorization function.
|
||||
def authenticate(username, password):
|
||||
# LDAP doesn't support empty RDNs.
|
||||
if username == "":
|
||||
return {"authenticated": False, "role": ""}
|
||||
|
||||
# Create the DN of the user
|
||||
dn = users_config["prefix"] + ldap3.utils.dn.escape_rdn(username) + \
|
||||
users_config["suffix"]
|
||||
|
||||
# Bind to the server
|
||||
conn = ldap3.Connection(server, dn, password)
|
||||
if server_config["encryption"] == "starttls" and not conn.start_tls():
|
||||
print("ERROR: Couldn't issue STARTTLS to the LDAP server!",
|
||||
file=sys.stderr)
|
||||
return {"authenticated": False, "role": ""}
|
||||
if not conn.bind():
|
||||
return {"authenticated": False, "role": ""}
|
||||
|
||||
# Try to find out the user's role
|
||||
if roles_config["root_dn"] != "":
|
||||
# search for role
|
||||
search_filter = "(&(objectclass={objclass})({attr}={value}))".format(
|
||||
objclass=roles_config["root_objectclass"],
|
||||
attr=roles_config["user_attribute"],
|
||||
value=ldap3.utils.conv.escape_filter_chars(dn))
|
||||
succ = conn.search(roles_config["root_dn"], search_filter,
|
||||
search_scope=ldap3.LEVEL,
|
||||
attributes=[roles_config["role_attribute"]])
|
||||
if not succ or len(conn.entries) == 0:
|
||||
return {"authenticated": True, "role": ""}
|
||||
if len(conn.entries) > 1:
|
||||
roles = list(map(lambda x: x[roles_config["role_attribute"]].value,
|
||||
conn.entries))
|
||||
# Because we don't know exactly which role the user should have
|
||||
# we authorize the user with an empty role.
|
||||
print("WARNING: Found more than one role for "
|
||||
"user '" + username + "':", ", ".join(roles) + "!",
|
||||
file=sys.stderr)
|
||||
return {"authenticated": True, "role": ""}
|
||||
return {"authenticated": True,
|
||||
"role": conn.entries[0][roles_config["role_attribute"]].value}
|
||||
else:
|
||||
return {"authenticated": True, "role": ""}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
input_stream = io.FileIO(1000, mode="r")
|
||||
output_stream = io.FileIO(1001, mode="w")
|
||||
while True:
|
||||
params = json.loads(input_stream.readline().decode("ascii"))
|
||||
ret = authenticate(**params)
|
||||
output_stream.write((json.dumps(ret) + "\n").encode("ascii"))
|
@ -69,7 +69,6 @@ void SingleNodeMain() {
|
||||
auto durability_directory = std::filesystem::path(FLAGS_durability_directory);
|
||||
|
||||
// Auth
|
||||
auth::Init();
|
||||
auth::Auth auth{durability_directory / "auth"};
|
||||
|
||||
// Audit log
|
||||
|
@ -32,7 +32,6 @@ void KafkaBenchmarkMain() {
|
||||
|
||||
auto durability_directory = std::filesystem::path(FLAGS_durability_directory);
|
||||
|
||||
auth::Init();
|
||||
auth::Auth auth{durability_directory / "auth"};
|
||||
|
||||
audit::Log audit_log{durability_directory / "audit",
|
||||
|
@ -60,6 +60,8 @@
|
||||
- prepare.sh # preparation script
|
||||
- runner.py # runner script
|
||||
- schema.ldif # schema file
|
||||
- ve3 # Python virtual environment
|
||||
- ../../../src/auth/reference_modules/ldap.py # LDAP auth module
|
||||
- ../../../build_debug/memgraph # memgraph binary
|
||||
- ../../../build_debug/tests/integration/ldap/tester # tester binary
|
||||
enable_network: true
|
||||
|
2
tests/integration/ldap/requirements.txt
Normal file
2
tests/integration/ldap/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
ldap3==2.6
|
||||
pyyaml==5.1.2
|
@ -2,6 +2,7 @@
|
||||
import argparse
|
||||
import atexit
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@ -10,6 +11,27 @@ import time
|
||||
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", ".."))
|
||||
|
||||
CONFIG_TEMPLATE = """
|
||||
server:
|
||||
host: "127.0.0.1"
|
||||
port: 1389
|
||||
encryption: "{encryption}"
|
||||
cert_file: ""
|
||||
key_file: ""
|
||||
ca_file: ""
|
||||
validate_cert: false
|
||||
|
||||
users:
|
||||
prefix: "{prefix}"
|
||||
suffix: "{suffix}"
|
||||
|
||||
roles:
|
||||
root_dn: "{root_dn}"
|
||||
root_objectclass: "{root_objectclass}"
|
||||
user_attribute: "{user_attribute}"
|
||||
role_attribute: "{role_attribute}"
|
||||
"""
|
||||
|
||||
|
||||
def wait_for_server(port, delay=0.1):
|
||||
cmd = ["nc", "-z", "-w", "1", "127.0.0.1", str(port)]
|
||||
@ -35,17 +57,54 @@ class Memgraph:
|
||||
def __init__(self, binary):
|
||||
self._binary = binary
|
||||
self._storage_directory = None
|
||||
self._auth_module = None
|
||||
self._auth_config = None
|
||||
self._process = None
|
||||
|
||||
def start(self, args=[]):
|
||||
def start(self, **kwargs):
|
||||
self.stop()
|
||||
self._storage_directory = tempfile.TemporaryDirectory()
|
||||
self.restart(args)
|
||||
self._auth_module = os.path.join(self._storage_directory.name,
|
||||
"ldap.py")
|
||||
self._auth_config = os.path.join(self._storage_directory.name,
|
||||
"ldap.yaml")
|
||||
script_file = os.path.join(PROJECT_DIR, "src", "auth",
|
||||
"reference_modules", "ldap.py")
|
||||
virtualenv_bin = os.path.join(SCRIPT_DIR, "ve3", "bin", "python3")
|
||||
with open(script_file) as fin:
|
||||
data = fin.read()
|
||||
data = data.replace("/usr/bin/env python3", virtualenv_bin)
|
||||
data = data.replace("/etc/memgraph/auth/ldap.yaml",
|
||||
self._auth_config)
|
||||
with open(self._auth_module, "w") as fout:
|
||||
fout.write(data)
|
||||
os.chmod(self._auth_module, stat.S_IRWXU | stat.S_IRWXG)
|
||||
self.restart(**kwargs)
|
||||
|
||||
def restart(self, args=[]):
|
||||
def restart(self, **kwargs):
|
||||
self.stop()
|
||||
args = [self._binary, "--durability-directory",
|
||||
self._storage_directory.name] + list(map(str, args))
|
||||
config = {
|
||||
"encryption": kwargs.pop("encryption", "disabled"),
|
||||
"prefix": kwargs.pop("prefix", "cn="),
|
||||
"suffix": kwargs.pop("suffix", ",ou=people,dc=memgraph,dc=com"),
|
||||
"root_dn": kwargs.pop("root_dn", "ou=roles,dc=memgraph,dc=com"),
|
||||
"root_objectclass": kwargs.pop("root_objectclass", "groupOfNames"),
|
||||
"user_attribute": kwargs.pop("user_attribute", "member"),
|
||||
"role_attribute": kwargs.pop("role_attribute", "cn"),
|
||||
}
|
||||
with open(self._auth_config, "w") as f:
|
||||
f.write(CONFIG_TEMPLATE.format(**config))
|
||||
args = [self._binary,
|
||||
"--durability-directory", self._storage_directory.name,
|
||||
"--auth-module-executable",
|
||||
kwargs.pop("module_executable", self._auth_module)]
|
||||
for key, value in kwargs.items():
|
||||
ldap_key = "--auth-module-" + key.replace("_", "-")
|
||||
if type(value) == bool:
|
||||
args.append(ldap_key + "=" + str(value).lower())
|
||||
else:
|
||||
args.append(ldap_key)
|
||||
args.append(value)
|
||||
self._process = subprocess.Popen(args)
|
||||
time.sleep(0.1)
|
||||
assert self._process.poll() is None, "Memgraph process died " \
|
||||
@ -63,29 +122,12 @@ class Memgraph:
|
||||
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()
|
||||
memgraph.start(module_executable="")
|
||||
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)
|
||||
memgraph.restart(**kwargs)
|
||||
if check_login:
|
||||
execute_tester(tester_binary, [], "root")
|
||||
|
||||
@ -102,7 +144,7 @@ def test_basic(memgraph, tester_binary):
|
||||
|
||||
|
||||
def test_only_existing_users(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, create_user=False)
|
||||
initialize_test(memgraph, tester_binary, create_missing_user=False)
|
||||
execute_tester(tester_binary, [], "alice", auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["CREATE USER alice"], "root")
|
||||
execute_tester(tester_binary, [], "alice")
|
||||
@ -112,8 +154,7 @@ def test_only_existing_users(memgraph, tester_binary):
|
||||
|
||||
|
||||
def test_role_mapping(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary,
|
||||
role_mapping_root_dn="ou=roles,dc=memgraph,dc=com")
|
||||
initialize_test(memgraph, tester_binary)
|
||||
|
||||
execute_tester(tester_binary, [], "alice")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
@ -136,23 +177,22 @@ def test_role_mapping(memgraph, tester_binary):
|
||||
|
||||
|
||||
def test_role_removal(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary,
|
||||
role_mapping_root_dn="ou=roles,dc=memgraph,dc=com")
|
||||
initialize_test(memgraph, tester_binary)
|
||||
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)
|
||||
memgraph.restart(manage_roles=False)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
execute_tester(tester_binary, ["CLEAR ROLE FOR alice"], "root")
|
||||
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)
|
||||
initialize_test(memgraph, tester_binary, create_missing_role=False)
|
||||
execute_tester(tester_binary, [], "bob")
|
||||
execute_tester(tester_binary, [], "alice", auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator"], "root")
|
||||
@ -161,16 +201,14 @@ def test_only_existing_roles(memgraph, tester_binary):
|
||||
|
||||
|
||||
def test_role_is_user(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary,
|
||||
role_mapping_root_dn="ou=roles,dc=memgraph,dc=com")
|
||||
initialize_test(memgraph, tester_binary)
|
||||
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")
|
||||
initialize_test(memgraph, tester_binary)
|
||||
execute_tester(tester_binary, [], "carol")
|
||||
execute_tester(tester_binary, [], "admin", auth_should_fail=True)
|
||||
memgraph.stop()
|
||||
@ -185,8 +223,7 @@ def test_user_permissions_persistancy(memgraph, tester_binary):
|
||||
|
||||
|
||||
def test_role_permissions_persistancy(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary,
|
||||
role_mapping_root_dn="ou=roles,dc=memgraph,dc=com")
|
||||
initialize_test(memgraph, tester_binary)
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
@ -195,7 +232,7 @@ def test_role_permissions_persistancy(memgraph, tester_binary):
|
||||
|
||||
|
||||
def test_only_authentication(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
initialize_test(memgraph, tester_binary, manage_roles=False)
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
@ -227,21 +264,43 @@ def test_suffix_with_spaces(memgraph, tester_binary):
|
||||
|
||||
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")
|
||||
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")
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_role_mapping_wrong_root_objectclass(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, root_objectclass="person")
|
||||
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.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_role_mapping_wrong_user_attribute(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, user_attribute="cn")
|
||||
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.restart()
|
||||
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")
|
||||
initialize_test(memgraph, tester_binary)
|
||||
execute_tester(tester_binary, [], "root", password="sudo",
|
||||
auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
@ -250,22 +309,49 @@ def test_wrong_password(memgraph, tester_binary):
|
||||
|
||||
def test_password_persistancy(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, check_login=False)
|
||||
memgraph.restart()
|
||||
memgraph.restart(module_executable="")
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="sudo")
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
restart_memgraph(memgraph, tester_binary)
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, [], "root", password="sudo",
|
||||
auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
memgraph.restart()
|
||||
memgraph.restart(module_executable="")
|
||||
execute_tester(tester_binary, [], "root", password="sudo",
|
||||
auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_user_multiple_roles(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, check_login=False)
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
|
||||
query_should_fail=True)
|
||||
memgraph.restart(manage_roles=False)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
|
||||
query_should_fail=True)
|
||||
memgraph.restart(manage_roles=False, root_dn="")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
|
||||
query_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_starttls_failure(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, issue_starttls=True,
|
||||
initialize_test(memgraph, tester_binary, encryption="starttls",
|
||||
check_login=False)
|
||||
execute_tester(tester_binary, [], "root", auth_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_ssl_failure(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, encryption="ssl",
|
||||
check_login=False)
|
||||
execute_tester(tester_binary, [], "root", auth_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
@ -62,6 +62,14 @@ objectclass: top
|
||||
sn: user
|
||||
userpassword: dave
|
||||
|
||||
# User eve
|
||||
dn: cn=eve,ou=people,dc=memgraph,dc=com
|
||||
cn: eve
|
||||
objectclass: person
|
||||
objectclass: top
|
||||
sn: user
|
||||
userpassword: eve
|
||||
|
||||
# User admin
|
||||
dn: cn=admin,ou=people,dc=memgraph,dc=com
|
||||
cn: admin
|
||||
@ -80,6 +88,7 @@ ou: roles
|
||||
dn: cn=moderator,ou=roles,dc=memgraph,dc=com
|
||||
cn: moderator
|
||||
member: cn=alice,ou=people,dc=memgraph,dc=com
|
||||
member: cn=eve,ou=people,dc=memgraph,dc=com
|
||||
objectclass: groupOfNames
|
||||
objectclass: top
|
||||
|
||||
@ -88,5 +97,6 @@ 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
|
||||
member: cn=eve,ou=people,dc=memgraph,dc=com
|
||||
objectclass: groupOfNames
|
||||
objectclass: top
|
||||
|
Loading…
Reference in New Issue
Block a user