From 7177855453dfba57a6f98ba9c7e3059380211693 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov <ivadmi5@gmail.com> Date: Fri, 30 Jun 2023 20:18:37 +0300 Subject: [PATCH] feat: Implement LDAP authentication This authentication method doesn't store any secrets; instead, we delegate the whole auth to a pre-configured LDAP server. In the method's implementation, we connect to the LDAP server and perform a BIND operation which checks user's credentials. Usage example: ```lua -- Set the default auth method to LDAP and create a new user. -- NOTE that we still have to provide a dummy password; otherwise -- box.schema.user.create will setup an empty auth data. box.cfg({auth_type = 'ldap'}) box.schema.user.create('demo', { password = '' }) -- Configure LDAP server connection URL and DN format string. os = require('os') os.setenv('TT_LDAP_URL', 'ldap://localhost:1389') os.setenv('TT_LDAP_DN_FMT', 'cn=$USER,ou=users,dc=example,dc=org') -- Authenticate using the LDAP authentication method via net.box. conn = require('net.box').connect(uri, { user = 'demo', password = 'password', auth_type = 'ldap', }) ``` NO_DOC=picodata internal patch NO_CHANGELOG=picodata internal patch NO_TEST=picodata internal patch --- .gitlab-ci.yml | 2 + CMakeLists.txt | 58 ++++++ cmake/BuildLDAP.cmake | 72 +++++++ cmake/BuildSASL.cmake | 68 +++++++ cmake/FindLDAP.cmake | 68 +++++++ cmake/FindSASL.cmake | 40 ++++ src/CMakeLists.txt | 2 + src/box/CMakeLists.txt | 2 + src/box/auth_ldap.c | 306 ++++++++++++++++++++++++++++ src/box/auth_ldap.h | 23 +++ src/box/authentication.c | 3 + test/box-luatest/ldap_auth_test.lua | 133 ++++++++++++ 12 files changed, 777 insertions(+) create mode 100644 cmake/BuildLDAP.cmake create mode 100644 cmake/BuildSASL.cmake create mode 100644 cmake/FindLDAP.cmake create mode 100644 cmake/FindSASL.cmake create mode 100644 src/box/auth_ldap.c create mode 100644 src/box/auth_ldap.h create mode 100644 test/box-luatest/ldap_auth_test.lua diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a21dab4175..2e0d3e1e21 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -32,6 +32,7 @@ test-ubuntu: tags: [docker] image: docker-public.binary.picodata.io/tarantool-testing:latest script: + - export PATH="$PATH:$PWD" # for GLAuth (CMake will download it) - make -f .test.mk test-release test_mac-m1: @@ -40,6 +41,7 @@ test_mac-m1: script: - ulimit -n 10240 - sudo chown -R $(id -u) /private/tmp/t + - export PATH="$PATH:$PWD" # for GLAuth (CMake will download it) - make -f .test.mk build checkpatch: diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f41693c51..50c167ee9a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,13 @@ add_compile_flags("C;CXX" ${PREFIX_MAP_FLAGS}) set(DEPENDENCY_CFLAGS "${DEPENDENCY_CFLAGS} ${PREFIX_MAP_FLAGS}") set(DEPENDENCY_CXXFLAGS "${DEPENDENCY_CXXFLAGS} ${PREFIX_MAP_FLAGS}") +# We need this for bundled SASL & LDAP libs (in fact, for any autotools-based project) +if(APPLE) + set(DEPENDENCY_CFLAGS "${DEPENDENCY_CFLAGS} ${CMAKE_C_SYSROOT_FLAG} ${CMAKE_OSX_SYSROOT}") + set(DEPENDENCY_CPPFLAGS "${DEPENDENCY_CPPFLAGS} ${CMAKE_C_SYSROOT_FLAG} ${CMAKE_OSX_SYSROOT}") + set(DEPENDENCY_CXXFLAGS "${DEPENDENCY_CXXFLAGS} ${CMAKE_C_SYSROOT_FLAG} ${CMAKE_OSX_SYSROOT}") +endif() + set(CMAKE_REQUIRED_DEFINITIONS "-D_GNU_SOURCE") check_symbol_exists(MAP_ANON sys/mman.h HAVE_MAP_ANON) @@ -470,8 +477,10 @@ if(ENABLE_BUNDLED_OPENSSL) set(OPENSSL_USE_STATIC_LIBS ON) include(BuildOpenSSL) add_dependencies(build_bundled_libs bundled-openssl) + set(LDAP_OPENSSL_DEPS bundled-openssl) else() find_package(OpenSSL) + set(LDAP_OPENSSL_DEPS OpenSSL::SSL OpenSSL::Crypto) endif() if (OPENSSL_FOUND) message(STATUS "OpenSSL ${OPENSSL_VERSION} found") @@ -491,6 +500,55 @@ if(OPENSSL_USE_STATIC_LIBS) endif() endif() +# +# OpenLDAP +# +option(ENABLE_BUNDLED_LDAP "Enable building of the bundled libldap" ON) +if(ENABLE_BUNDLED_LDAP OR BUILD_STATIC) + include(BuildSASL) + sasl_build() + add_dependencies(build_bundled_libs bundled-sasl) + + include(BuildLDAP) + ldap_build() + add_dependencies(build_bundled_libs bundled-ldap) +else() + set(SASL_FIND_REQUIRED ON) + find_package(SASL) + + set(LDAP_FIND_REQUIRED ON) + find_package(LDAP) +endif() + +# Download GLAuth, a LDAP server used in tests +message(STATUS "Downloading GLAuth for tests") +if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + if(${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "x86_64") + set(GLAUTH_URL https://github.com/glauth/glauth/releases/download/v2.2.0/glauth-linux-amd64) + set(GLAUTH_HASH 823b4403b87e0294ca921651694b57ca) + elseif(${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "aarch64") + set(GLAUTH_URL https://github.com/glauth/glauth/releases/download/v2.2.0/glauth-linux-arm64) + set(GLAUTH_HASH c3e749a34f54e82597e7b4cd7efeae42) + else() + message(WARNING "Failed to download GLAuth: unsupported platform ${CMAKE_HOST_SYSTEM_PROCESSOR}") + endif() +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + if(${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "x86_64") + set(GLAUTH_URL https://github.com/glauth/glauth/releases/download/v2.2.0/glauth-darwin-amd64) + set(GLAUTH_HASH 19b3a8d83d6b258508df626cd1ff3f46) + elseif(${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL "arm64") + set(GLAUTH_URL https://github.com/glauth/glauth/releases/download/v2.2.0/glauth-darwin-arm64) + set(GLAUTH_HASH 45b9f2ff7a27ca1cc2b53910f0c89c67) + else() + message(WARNING "Failed to download GLAuth: unsupported platform ${CMAKE_HOST_SYSTEM_PROCESSOR}") + endif() +else() + message(WARNING "Failed to download GLAuth: unsupported OS") +endif() +set(GLAUTH_BIN ${CMAKE_BINARY_DIR}/glauth) +file(DOWNLOAD ${GLAUTH_URL} ${GLAUTH_BIN} EXPECTED_MD5 ${GLAUTH_HASH} SHOW_PROGRESS) +execute_process(COMMAND chmod 770 ${GLAUTH_BIN}) + # # Curl # diff --git a/cmake/BuildLDAP.cmake b/cmake/BuildLDAP.cmake new file mode 100644 index 0000000000..e64ad6dbd8 --- /dev/null +++ b/cmake/BuildLDAP.cmake @@ -0,0 +1,72 @@ +# +# A macro to build libldap +macro(ldap_build) + message(STATUS "Choosing bundled LDAP") + + # https://git.openldap.org/openldap/openldap + set(LDAP_VERSION 2.6.4) + set(LDAP_HASH fee2b0dca212b41c87976d0414f30f12) + set(LDAP_URL https://www.openldap.org/software/download/OpenLDAP/openldap-release/openldap-${LDAP_VERSION}.tgz) + + # Reusing approach from BuildLibCURL.cmake + get_filename_component(OPENSSL_INSTALL_DIR ${OPENSSL_INCLUDE_DIR} DIRECTORY) + + include(ExternalProject) + ExternalProject_Add(bundled-ldap + DEPENDS ${LDAP_OPENSSL_DEPS} bundled-sasl + URL ${LDAP_URL} + URL_MD5 ${LDAP_HASH} + PATCH_COMMAND + # OpenLDAP builds everything (including MAN pages) unconditionally, + # thus we have to patch its sources so as not to install soelim (Groff). + sed -i.old "/SUBDIRS/s/clients servers tests doc//" Makefile.in + CONFIGURE_COMMAND <SOURCE_DIR>/configure + "CC=${CMAKE_C_COMPILER}" + "CFLAGS=${DEPENDENCY_CFLAGS}" + "CPPFLAGS=${DEPENDENCY_CPPFLAGS} -I${OPENSSL_INCLUDE_DIR} -I${SASL_INCLUDE_DIR}" + "LDFLAGS=-L${OPENSSL_INSTALL_DIR}/lib -L${SASL_INSTALL_DIR}/lib" + "LIBS=-lssl -lcrypto -ldl -lpthread" + --prefix=<INSTALL_DIR> + + --with-cyrus-sasl + --with-tls=openssl + + --enable-static + --disable-shared + + --enable-local + --enable-ipv6 + + --disable-debug + --disable-slapd + ) + + unset(OPENSSL_INSTALL_DIR) + + ExternalProject_Get_Property(bundled-ldap install_dir) + set(LDAP_INSTALL_DIR ${install_dir}) + + # Unfortunately, we can't use find_library here, + # since the package hasn't been built yet. + # We set the same vars as in FindLDAP.cmake. + set(LDAP_FOUND TRUE) + set(LDAP_INCLUDE_DIR ${LDAP_INSTALL_DIR}/include) + set(LDAP_LIBRARIES + ${LDAP_INSTALL_DIR}/lib/libldap.a + ${LDAP_INSTALL_DIR}/lib/liblber.a + ) + + # On OS X we may also need to link libresolv.dylib. + # However, note that newer OS X releases don't have that file; + # instead, it's provided via a built-in dynamic linker cache. + if(APPLE) + find_library(RESOLV_LIBRARIES NAMES resolv PATHS + /usr/lib + /usr/local/opt/openldap/lib + /usr/local/lib + NO_CMAKE_SYSTEM_PATH) + if(NOT "${RESOLV_LIBRARIES}" STREQUAL "RESOLV_LIBRARIES-NOTFOUND") + set(LDAP_LIBRARIES ${LDAP_LIBRARIES} ${RESOLV_LIBRARIES}) + endif() + endif() +endmacro() diff --git a/cmake/BuildSASL.cmake b/cmake/BuildSASL.cmake new file mode 100644 index 0000000000..6154b165e1 --- /dev/null +++ b/cmake/BuildSASL.cmake @@ -0,0 +1,68 @@ +# +# A macro to build libsasl2 +macro(sasl_build) + message(STATUS "Choosing bundled SASL") + + # https://github.com/cyrusimap/cyrus-sasl + # Tarball is already pre-configured (autoconf) as opposed to git sources. + set(SASL_VERSION 2.1.28) + set(SASL_HASH 6f228a692516f5318a64505b46966cfa) + set(SASL_URL https://github.com/cyrusimap/cyrus-sasl/releases/download/cyrus-sasl-${SASL_VERSION}/cyrus-sasl-${SASL_VERSION}.tar.gz) + + # Reusing approach from BuildLibCURL.cmake + get_filename_component(OPENSSL_INSTALL_DIR ${OPENSSL_INCLUDE_DIR} DIRECTORY) + + include(ExternalProject) + ExternalProject_Add(bundled-sasl + DEPENDS ${LDAP_OPENSSL_DEPS} + URL ${SASL_URL} + URL_MD5 ${SASL_HASH} + CONFIGURE_COMMAND <SOURCE_DIR>/configure + "CC=${CMAKE_C_COMPILER}" + "CFLAGS=${DEPENDENCY_CFLAGS}" + "CPPFLAGS=${DEPENDENCY_CPPFLAGS} -I${OPENSSL_INCLUDE_DIR}" + "LDFLAGS=-L${OPENSSL_INSTALL_DIR}/lib" + "LIBS=-lssl -lcrypto -ldl -lpthread" + --prefix=<INSTALL_DIR> + + --with-openssl=${OPENSSL_INSTALL_DIR} + --with-dblib=none + + --enable-static + --disable-shared + + # Auth plugins listed below + --enable-anon + --enable-cram + --enable-digest + --enable-otp + --enable-plain + --enable-scram + + --disable-gssapi + --disable-krb4 + + --disable-macos-framework + --disable-sample + ) + + unset(OPENSSL_INSTALL_DIR) + + ExternalProject_Get_Property(bundled-sasl install_dir) + set(SASL_INSTALL_DIR ${install_dir}) + + # Unfortunately, we can't use find_library here, + # since the package hasn't been built yet. + # We set the same vars as in FindSASL.cmake. + set(SASL_FOUND TRUE) + set(SASL_INCLUDE_DIR ${SASL_INSTALL_DIR}/include) + set(SASL_LIBRARIES + ${SASL_INSTALL_DIR}/lib/libsasl2.a + ${SASL_INSTALL_DIR}/lib/sasl2/libanonymous.a + ${SASL_INSTALL_DIR}/lib/sasl2/libcrammd5.a + ${SASL_INSTALL_DIR}/lib/sasl2/libdigestmd5.a + ${SASL_INSTALL_DIR}/lib/sasl2/libotp.a + ${SASL_INSTALL_DIR}/lib/sasl2/libplain.a + ${SASL_INSTALL_DIR}/lib/sasl2/libscram.a + ) +endmacro() diff --git a/cmake/FindLDAP.cmake b/cmake/FindLDAP.cmake new file mode 100644 index 0000000000..792e5528b2 --- /dev/null +++ b/cmake/FindLDAP.cmake @@ -0,0 +1,68 @@ +# - Find LDAP header and library +# The module defines the following variables: +# +# LDAP_FOUND - true if libldap was found +# LDAP_INCLUDE_DIR - the directory of the LDAP headers +# LDAP_LIBRARIES - the libraries needed for linking + +# Support preference of static libs by adjusting CMAKE_FIND_LIBRARY_SUFFIXES +if(BUILD_STATIC) + set(_openldap_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(CMAKE_FIND_LIBRARY_SUFFIXES .a) +endif() + +if(APPLE) + find_path(LDAP_INCLUDE_DIR ldap.h PATHS + /usr/local/include + /usr/local/opt/openldap/include + /opt/local/include + NO_CMAKE_SYSTEM_PATH) + find_path(LDAP_LIBRARIES NAMES ldap PATHS + /usr/local/lib + /usr/local/opt/openldap/lib + /opt/local/lib + NO_CMAKE_SYSTEM_PATH) + find_path(LBER_LIBRARIES NAMES lber PATHS + /usr/local/lib + /usr/local/opt/openldap/lib + /opt/local/lib + NO_CMAKE_SYSTEM_PATH) + + # On OS X we may also need to link libresolv.dylib. + # However, note that newer OS X releases don't have that file; + # instead, it's provided via a built-in dynamic linker cache. + if(_openldap_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${_openldap_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES}) + endif() + find_library(RESOLV_LIBRARIES NAMES resolv PATHS + /usr/lib + /usr/local/opt/openldap/lib + /usr/local/lib + NO_CMAKE_SYSTEM_PATH) +else() + find_path(LDAP_INCLUDE_DIR ldap.h) + find_library(LDAP_LIBRARIES NAMES ldap) + find_library(LBER_LIBRARIES NAMES lber) +endif() + +if(LDAP_INCLUDE_DIR AND LDAP_LIBRARIES) + set(LDAP_FOUND TRUE) +endif() + +if(LDAP_FOUND) + set(LDAP_LIBRARIES ${LDAP_LIBRARIES} ${LBER_LIBRARIES}) + if(NOT "${RESOLV_LIBRARIES}" STREQUAL "RESOLV_LIBRARIES-NOTFOUND") + set(LDAP_LIBRARIES ${LDAP_LIBRARIES} ${RESOLV_LIBRARIES}) + endif() + message(STATUS "Found LDAP includes: ${LDAP_INCLUDE_DIR}") + message(STATUS "Found LDAP libraries: ${LDAP_LIBRARIES}") +else() + message(FATAL_ERROR "Could not find LDAP library") +endif() + +mark_as_advanced(LDAP_INCLUDE_DIR LDAP_LIBRARIES LBER_LIBRARIES) + +# Restore the original find library ordering +if(BUILD_STATIC) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${_openldap_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES}) +endif() diff --git a/cmake/FindSASL.cmake b/cmake/FindSASL.cmake new file mode 100644 index 0000000000..f1176e6f6a --- /dev/null +++ b/cmake/FindSASL.cmake @@ -0,0 +1,40 @@ +# - Find SASL header and library +# The module defines the following variables: +# +# SASL_FOUND - true if libldap was found +# SASL_INCLUDE_DIR - the directory of the SASL headers +# SASL_LIBRARIES - the libraries needed for linking + +# Support preference of static libs by adjusting CMAKE_FIND_LIBRARY_SUFFIXES +if(BUILD_STATIC) + set(_cyrus_sasl_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(CMAKE_FIND_LIBRARY_SUFFIXES .a) +endif() + +find_path(SASL_INCLUDE_DIR sasl/sasl.h) +find_library(SASL_LIBRARIES NAMES sasl2) + +if(SASL_INCLUDE_DIR AND SASL_LIBRARIES) + set(SASL_FOUND TRUE) +endif() + +if(SASL_FOUND) + message(STATUS "Found SASL includes: ${SASL_INCLUDE_DIR}") + message(STATUS "Found SASL libraries: ${SASL_LIBRARIES}") +else() + message(FATAL_ERROR "Could not find SASL library") +endif() + +if(BUILD_STATIC) + get_filename_component(sasl_lib_dir ${SASL_LIBRARIES} DIRECTORY) + file(GLOB_RECURSE SASL_PLUGINS "${sasl_lib_dir}/sasl2/*.a") + set(SASL_LIBRARIES ${SASL_LIBRARIES} ${SASL_PLUGINS}) + unset(sasl_lib_dir) +endif() + +mark_as_advanced(SASL_INCLUDE_DIR SASL_LIBRARIES) + +# Restore the original find library ordering +if(BUILD_STATIC) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${_cyrus_sasl_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES}) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8556687817..9404c7f4a0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -311,6 +311,8 @@ set (common_libraries ${LIBYAML_LIBRARIES} ${READLINE_LIBRARIES} ${ICONV_LIBRARIES} + ${LDAP_LIBRARIES} + ${SASL_LIBRARIES} ${OPENSSL_LIBRARIES} ) diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt index fb8b6df408..535d498d7b 100644 --- a/src/box/CMakeLists.txt +++ b/src/box/CMakeLists.txt @@ -85,6 +85,7 @@ add_custom_target(box_generate_lua_sources DEPENDS ${lua_sources}) set_property(DIRECTORY PROPERTY ADDITIONAL_MAKE_CLEAN_FILES ${lua_sources}) +include_directories(${LDAP_INCLUDE_DIR}) include_directories(${ZSTD_INCLUDE_DIRS}) include_directories(${PROJECT_BINARY_DIR}/src/box/sql) include_directories(${PROJECT_BINARY_DIR}/src/box) @@ -208,6 +209,7 @@ set(box_sources user.cc authentication.c auth_chap_sha1.c + auth_ldap.c auth_md5.c replication.cc recovery.cc diff --git a/src/box/auth_ldap.c b/src/box/auth_ldap.c new file mode 100644 index 0000000000..40bedced6b --- /dev/null +++ b/src/box/auth_ldap.c @@ -0,0 +1,306 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file. + */ +#include "auth_ldap.h" + +#include <assert.h> +#include <stddef.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#include <ldap.h> + +#include "authentication.h" +#include "coio_task.h" +#include "diag.h" +#include "errcode.h" +#include "error.h" +#include "fiber.h" +#include "msgpuck.h" +#include "small/region.h" +#include "trivia/util.h" + +#define AUTH_LDAP_NAME "ldap" + +/** ldap authenticator implementation. */ +struct auth_ldap_authenticator { + /** Base class. */ + struct authenticator base; +}; + +/** Try to format a proper LDAP Distinguished Name (DN). */ +static int +format_dn(const char *fmt, const char *user, char *buf, size_t len) +{ + /** Magic should be replaced with the actual user */ + const char *magic = "$USER"; + char *suffix = strstr(fmt, magic); + if (suffix == NULL) { + say_error("TT_LDAP_DN_FMT doesn't contain $USER"); + return -1; + } + + size_t prefix_len = suffix - fmt; + suffix += strlen(magic); + size_t suffix_len = strlen(suffix), + user_len = strlen(user); + + if (strstr(suffix, magic) != NULL) { + say_error("TT_LDAP_DN_FMT contains more than one $USER"); + return -1; + } + + size_t needed = prefix_len + user_len + suffix_len + 1; + if (needed > len) { + say_error("TT_LDAP_DN_FMT is too long (max %lu)", len); + return -1; + } + + memcpy(buf, fmt, prefix_len); + buf += prefix_len; + memcpy(buf, user, user_len); + buf += user_len; + memcpy(buf, suffix, suffix_len); + buf += suffix_len; + buf[0] = '\0'; + + return 0; +} + +/** Perform synchronous LDAP BIND method call to authenticate as user */ +static ssize_t +coio_ldap_check_password(va_list ap) +{ + const char *password = va_arg(ap, const char *); + uint32_t password_len = va_arg(ap, uint32_t); + const char *user = va_arg(ap, const char *); + + /** + * This should point to the LDAP authentication server. + * Example: `ldap://localhost:1389`. + */ + const char *url = va_arg(ap, const char *); + + /** + * This should be a proper LDAP Distinguished Name (DN) fmt string. + * Example: `cn=$USER,ou=users,dc=example,dc=org`. + */ + const char *dn_fmt = va_arg(ap, const char *); + + int ret = -1; + LDAP *ldp = NULL; + + char dn[512]; + if (format_dn(dn_fmt, user, dn, sizeof(dn)) != 0) + goto cleanup; + + /** Initialize the context, but don't connect just yet */ + ret = ldap_initialize(&ldp, url); + if (ret != LDAP_SUCCESS) { + say_error("failed to initialize LDAP connection: %s", + ldap_err2string(ret)); + goto cleanup; + } + + /** NB: older protocol versions may not be supported */ + ret = ldap_set_option(ldp, LDAP_OPT_PROTOCOL_VERSION, + &(int){LDAP_VERSION3}); + if (ret != LDAP_SUCCESS) { + say_error("failed to set LDAP connection option: %s", + ldap_err2string(ret)); + goto cleanup; + } + + say_info("connecting to LDAP server at '%s'", url); + ret = ldap_connect(ldp); + if (ret != LDAP_SUCCESS) { + say_error("failed to connect to LDAP server at '%s': %s", + url, ldap_err2string(ret)); + goto cleanup; + } + + /** Check user's credentials by binding to the server on their behalf */ + say_info("attempting LDAP BIND as '%s'", dn); + struct berval cred = { + .bv_val = (char *)password, + .bv_len = (ber_len_t)password_len, + }; + /** See definition of ldap_simple_bind_s() */ + ret = ldap_sasl_bind_s(ldp, dn, LDAP_SASL_SIMPLE, + &cred, NULL, NULL, NULL); + if (ret != LDAP_SUCCESS) { + say_error("ldap authentication failed: %s", + ldap_err2string(ret)); + goto cleanup; + } + +cleanup: + if (ldp) { + /** This also reclaims the memory */ + (void)ldap_unbind_ext_s(ldp, NULL, NULL); + } + + return (ret == LDAP_SUCCESS) ? 0 : -1; +} + +/** auth_method::auth_method_delete */ +static void +auth_ldap_delete(struct auth_method *method) +{ + TRASH(method); + free(method); +} + +/** auth_method::auth_data_prepare */ +static void +auth_ldap_data_prepare(const struct auth_method *method, + const char *password, int password_len, + const char *user, + const char **auth_data, + const char **auth_data_end) +{ + (void)method; + (void)user; + (void)password; + (void)password_len; + struct region *region = &fiber()->gc; + size_t size = mp_sizeof_str(0); + char *p = xregion_alloc(region, size); + *auth_data = p; + *auth_data_end = p + size; + p = mp_encode_strl(p, 0); +} + +/** auth_method::auth_request_prepare */ +static void +auth_ldap_request_prepare(const struct auth_method *method, + const char *password, int password_len, + const char *user, + const char *salt, + const char **auth_request, + const char **auth_request_end) +{ + (void)method; + (void)user; + (void)salt; + struct region *region = &fiber()->gc; + size_t size = mp_sizeof_str(password_len); + char *p = xregion_alloc(region, size); + *auth_request = p; + *auth_request_end = p + size; + p = mp_encode_strl(p, password_len); + memcpy(p, password, password_len); +} + +/** auth_method::auth_request_check */ +static int +auth_ldap_request_check(const struct auth_method *method, + const char *auth_request, + const char *auth_request_end) +{ + (void)method; + uint32_t password_len; + (void)password_len; + if (mp_typeof(*auth_request) == MP_STR) { + password_len = mp_decode_strl(&auth_request); + } else if (mp_typeof(*auth_request) == MP_BIN) { + password_len = mp_decode_binl(&auth_request); + } else { + diag_set(ClientError, ER_INVALID_AUTH_REQUEST, + AUTH_LDAP_NAME, "password must be string"); + return -1; + } + assert(auth_request + password_len == auth_request_end); + (void)auth_request_end; + return 0; +} + +/** auth_method::authenticator_new */ +static struct authenticator * +auth_ldap_authenticator_new(const struct auth_method *method, + const char *auth_data, + const char *auth_data_end) +{ + /** NB: we don't use stored data anyway */ + (void)auth_data; + (void)auth_data_end; + + struct auth_ldap_authenticator *auth = xmalloc(sizeof(*auth)); + auth->base.method = method; + return (struct authenticator *)auth; +} + +/** auth_method::authenticator_delete */ +static void +auth_ldap_authenticator_delete(struct authenticator *auth_) +{ + struct auth_ldap_authenticator *auth = + (struct auth_ldap_authenticator *)auth_; + TRASH(auth); + free(auth); +} + +/** auth_method::authenticator_check_request */ +static bool +auth_ldap_authenticate_request(const struct authenticator *auth, + const char *user, + const char *salt, + const char *auth_request, + const char *auth_request_end) +{ + (void)auth; + (void)salt; + uint32_t password_len; + const char *password; + if (mp_typeof(*auth_request) == MP_STR) { + password = mp_decode_str(&auth_request, &password_len); + } else if (mp_typeof(*auth_request) == MP_BIN) { + password = mp_decode_bin(&auth_request, &password_len); + } else { + unreachable(); + } + assert(auth_request == auth_request_end); + (void)auth_request_end; + + ssize_t ret = -1; + + /** NB: we shouldn't call getenv() from coio since it's MT-Unsafe */ + char url[512]; + if (getenv_safe("TT_LDAP_URL", url, sizeof(url)) == NULL) { + say_error("LDAP server not configured, " + "please set env variable TT_LDAP_URL"); + goto fail; + } + + char dn_fmt[512]; + if (getenv_safe("TT_LDAP_DN_FMT", dn_fmt, sizeof(dn_fmt)) == NULL) { + say_error("LDAP DN format string not configured, " + "please set env variable TT_LDAP_DN_FMT"); + goto fail; + } + + ret = coio_call(coio_ldap_check_password, + password, password_len, + user, url, dn_fmt); +fail: + return ret == 0; +} + +struct auth_method * +auth_ldap_new(void) +{ + struct auth_method *method = xmalloc(sizeof(*method)); + method->name = AUTH_LDAP_NAME; + method->flags = 0; + method->auth_method_delete = auth_ldap_delete; + method->auth_data_prepare = auth_ldap_data_prepare; + method->auth_request_prepare = auth_ldap_request_prepare; + method->auth_request_check = auth_ldap_request_check; + method->authenticator_new = auth_ldap_authenticator_new; + method->authenticator_delete = auth_ldap_authenticator_delete; + method->authenticate_request = auth_ldap_authenticate_request; + return method; +} diff --git a/src/box/auth_ldap.h b/src/box/auth_ldap.h new file mode 100644 index 0000000000..551e4afe1c --- /dev/null +++ b/src/box/auth_ldap.h @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file. + */ +#pragma once + +#if defined(__cplusplus) +extern "C" { +#endif /* defined(__cplusplus) */ + +struct auth_method; + +/** + * Allocates and initializes 'ldap' authentication method. + * This function never fails. + */ +struct auth_method * +auth_ldap_new(void); + +#if defined(__cplusplus) +} /* extern "C" */ +#endif /* defined(__cplusplus) */ diff --git a/src/box/authentication.c b/src/box/authentication.c index 9e2f020675..45bc3431ee 100644 --- a/src/box/authentication.c +++ b/src/box/authentication.c @@ -13,6 +13,7 @@ #include "assoc.h" #include "auth_chap_sha1.h" +#include "auth_ldap.h" #include "auth_md5.h" #include "base64.h" #include "diag.h" @@ -175,6 +176,8 @@ auth_init(void) auth_method_register(chap_sha1_method); struct auth_method *md5_method = auth_md5_new(); auth_method_register(md5_method); + struct auth_method *ldap_method = auth_ldap_new(); + auth_method_register(ldap_method); } void diff --git a/test/box-luatest/ldap_auth_test.lua b/test/box-luatest/ldap_auth_test.lua new file mode 100644 index 0000000000..f57c62f13b --- /dev/null +++ b/test/box-luatest/ldap_auth_test.lua @@ -0,0 +1,133 @@ +local net = require('net.box') +local server = require('luatest.server') +local t = require('luatest') +local urilib = require('uri') +local fio = require('fio') +local popen = require('popen') +local socket = require('socket') +local fiber = require('fiber') + +-- GLAuth (A lightweight LDAP server for development, home use, or CI) +-- https://github.com/glauth/glauth/releases/download/v2.2.0/glauth-linux-amd64 +local glauth = nil +local glauth_tmp = fio.tempdir() + +local g = t.group() + +g.before_all(function(cg) + -- NB: configure magic LDAP environment variables. + local env = { + TT_LDAP_URL = 'ldap://127.0.0.1:1389', + TT_LDAP_DN_FMT = 'cn=$USER,dc=example,dc=org' + } + + cg.server = server:new({alias = 'master', env = env}) + cg.server:start() + cg.server:exec(function() + box.cfg{auth_type = 'ldap'} + box.schema.user.create('mickey', {password = ''}) + box.session.su('admin', box.schema.user.grant, 'mickey', 'super') + end) + + local config_path = fio.pathjoin(glauth_tmp, 'ldap.cfg') + local config = fio.open(config_path, {'O_CREAT', 'O_RDWR'}, + tonumber('644', 8)) + local password = -- sha256 of `dogood` + "6478579e37aff45f013e14eeb30b3cc56c72ccdc310123bcdf53e0333e3f416a" + config:write(string.format([=[ + [ldap] + enabled = true + listen = "127.0.0.1:1389" + + [ldaps] + enabled = false + + [backend] + datastore = "config" + baseDN = "dc=example,dc=org" + + [[users]] + name = "mickey" + uidnumber = 5001 + primarygroup = 5501 + passsha256 = "%s" + [[users.capabilities]] + action = "search" + object = "*" + + [[groups]] + name = "cartoons" + gidnumber = 5501 + ]=], password)) + config:close() + + -- I'd gladly use popen.new, but it doesn't resolve path... + glauth = popen.shell('glauth -c ' .. config_path) + t.assert_not_equals(glauth, nil) + + -- Wait for GLAuth startup + local sock = nil + for _=1,40 do + sock = socket.tcp_connect('127.0.0.1', 1389) + if sock == nil and glauth.status.state == popen.state.ALIVE then + fiber.sleep(0.1) + else break end + end + t.assert_not_equals(sock, nil) + sock:close() +end) + +g.after_all(function(cg) + cg.server:drop() + glauth:close() + fio.rmtree(glauth_tmp) +end) + +g.test_net_box = function(cg) + local parsed_uri = urilib.parse(cg.server.net_box_uri) + parsed_uri.login = 'mickey' + parsed_uri.password = 'dogood' + parsed_uri.params = parsed_uri.params or {} + parsed_uri.params.auth_type = {'ldap'} + + -- Good + local uri = urilib.format(parsed_uri, true) + local conn = net.connect(cg.server.net_box_uri, uri) + t.assert_equals(conn.error, nil) + t.assert_equals(conn.state, 'active') + conn:close() + + -- Good + conn = net.connect(cg.server.net_box_uri, { + user = 'mickey', + password = 'dogood', + auth_type = 'ldap', + }) + t.assert_equals(conn.error, nil) + t.assert_equals(conn.state, 'active') + conn:close() + + -- Bad password + conn = net.connect(cg.server.net_box_uri, { + user = 'mickey', + password = 'wrong_password', + auth_type = 'ldap', + }) + t.assert_equals( + conn.error, + "User not found or supplied credentials are invalid" + ) + conn:close() + + -- Bad auth type + conn = net.connect(cg.server.net_box_uri, { + user = 'mickey', + password = 'dogood', + auth_type = 'md5', + }) + t.assert_equals( + conn.error, + "User not found or supplied credentials are invalid" + ) + conn:close() +end -- GitLab