Add Kerberos support for authenticating existing users when accessing over HTTP

This commit is contained in:
Denis Glazachev 2021-03-12 00:41:10 +04:00 committed by GitHub
parent ae8585a183
commit 290a6d273e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 5577 additions and 474 deletions

View File

@ -5,8 +5,8 @@ if (NOT EXISTS "${ClickHouse_SOURCE_DIR}/contrib/krb5/README")
set (ENABLE_KRB5 0)
endif ()
if (NOT CMAKE_SYSTEM_NAME MATCHES "Linux")
message (WARNING "krb5 disabled in non-Linux environments")
if (NOT CMAKE_SYSTEM_NAME MATCHES "Linux" AND NOT (CMAKE_SYSTEM_NAME MATCHES "Darwin" AND NOT CMAKE_CROSSCOMPILING))
message (WARNING "krb5 disabled in non-Linux and non-native-Darwin environments")
set (ENABLE_KRB5 0)
endif ()

View File

@ -474,13 +474,6 @@ add_custom_command(
WORKING_DIRECTORY "${KRB5_SOURCE_DIR}/util/et"
)
add_custom_target(
CREATE_COMPILE_ET ALL
DEPENDS ${KRB5_SOURCE_DIR}/util/et/compile_et
COMMENT "creating compile_et"
VERBATIM
)
file(GLOB_RECURSE ET_FILES
"${KRB5_SOURCE_DIR}/*.et"
)
@ -531,7 +524,7 @@ add_custom_command(
add_custom_target(
ERROR_MAP_H ALL
ERROR_MAP_H
DEPENDS ${KRB5_SOURCE_DIR}/lib/gssapi/krb5/error_map.h
COMMENT "generating error_map.h"
VERBATIM
@ -544,14 +537,14 @@ add_custom_command(
)
add_custom_target(
ERRMAP_H ALL
ERRMAP_H
DEPENDS ${KRB5_SOURCE_DIR}/lib/gssapi/generic/errmap.h
COMMENT "generating errmap.h"
VERBATIM
)
add_custom_target(
KRB_5_H ALL
KRB_5_H
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/include/krb5/krb5.h
COMMENT "generating krb5.h"
VERBATIM
@ -564,15 +557,19 @@ add_dependencies(
ERRMAP_H
ERROR_MAP_H
KRB_5_H
)
)
preprocess_et(processed_et_files ${ET_FILES})
add_custom_command(
OUTPUT ${KRB5_SOURCE_DIR}/lib/gssapi/generic/errmap.h
COMMAND perl -w -I../../../util ../../../util/gen.pl bimap errmap.h NAME=mecherrmap LEFT=OM_uint32 RIGHT=struct\ mecherror LEFTPRINT=print_OM_uint32 RIGHTPRINT=mecherror_print LEFTCMP=cmp_OM_uint32 RIGHTCMP=mecherror_cmp
WORKING_DIRECTORY "${KRB5_SOURCE_DIR}/lib/gssapi/generic"
)
if(CMAKE_SYSTEM_NAME MATCHES "Darwin")
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/include_private/kcmrpc.h ${CMAKE_CURRENT_BINARY_DIR}/include_private/kcmrpc.c
COMMAND mig -header kcmrpc.h -user kcmrpc.c -sheader /dev/null -server /dev/null -I${KRB5_SOURCE_DIR}/lib/krb5/ccache ${KRB5_SOURCE_DIR}/lib/krb5/ccache/kcmrpc.defs
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include_private"
)
list(APPEND ALL_SRCS ${CMAKE_CURRENT_BINARY_DIR}/include_private/kcmrpc.c)
endif()
target_sources(${KRB5_LIBRARY} PRIVATE
${ALL_SRCS}
@ -604,6 +601,25 @@ file(COPY ${KRB5_SOURCE_DIR}/util/et/com_err.h
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/include/
)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/osconf.h
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/include_private/
)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/profile.h
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/include_private/
)
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _system_name)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/autoconf_${_system_name}.h
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/include_private/
)
file(RENAME
${CMAKE_CURRENT_BINARY_DIR}/include_private/autoconf_${_system_name}.h
${CMAKE_CURRENT_BINARY_DIR}/include_private/autoconf.h
)
file(MAKE_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}/include/krb5
)
@ -633,7 +649,7 @@ target_include_directories(${KRB5_LIBRARY} PUBLIC
)
target_include_directories(${KRB5_LIBRARY} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR} #for autoconf.h
${CMAKE_CURRENT_BINARY_DIR}/include_private # For autoconf.h and other generated headers.
${KRB5_SOURCE_DIR}
${KRB5_SOURCE_DIR}/include
${KRB5_SOURCE_DIR}/lib/gssapi/mechglue

View File

@ -0,0 +1,764 @@
/* include/autoconf.h. Generated from autoconf.h.in by configure. */
/* include/autoconf.h.in. Generated from configure.in by autoheader. */
#ifndef KRB5_AUTOCONF_H
#define KRB5_AUTOCONF_H
/* Define if AES-NI support is enabled */
/* #undef AESNI */
/* Define if socket can't be bound to 0.0.0.0 */
/* #undef BROKEN_STREAMS_SOCKETS */
/* Define if va_list objects can be simply copied by assignment. */
/* #undef CAN_COPY_VA_LIST */
/* Define to reduce code size even if it means more cpu usage */
/* #undef CONFIG_SMALL */
/* Define if __attribute__((constructor)) works */
#define CONSTRUCTOR_ATTR_WORKS 1
/* Define to default ccache name */
#define DEFCCNAME "FILE:/tmp/krb5cc_%{uid}"
/* Define to default client keytab name */
#define DEFCKTNAME "FILE:/etc/krb5/user/%{euid}/client.keytab"
/* Define to default keytab name */
#define DEFKTNAME "FILE:/etc/krb5.keytab"
/* Define if library initialization should be delayed until first use */
#define DELAY_INITIALIZER 1
/* Define if __attribute__((destructor)) works */
#define DESTRUCTOR_ATTR_WORKS 1
/* Define to disable PKINIT plugin support */
#define DISABLE_PKINIT 1
/* Define if LDAP KDB support within the Kerberos library (mainly ASN.1 code)
should be enabled. */
/* #undef ENABLE_LDAP */
/* Define if translation functions should be used. */
/* #undef ENABLE_NLS */
/* Define if thread support enabled */
#define ENABLE_THREADS 1
/* Define as return type of endrpcent */
#define ENDRPCENT_TYPE void
/* Define if Fortuna PRNG is selected */
#define FORTUNA 1
/* Define to the type of elements in the array set by `getgroups'. Usually
this is either `int' or `gid_t'. */
#define GETGROUPS_T gid_t
/* Define if gethostbyname_r returns int rather than struct hostent * */
/* #undef GETHOSTBYNAME_R_RETURNS_INT */
/* Type of getpeername second argument. */
#define GETPEERNAME_ARG3_TYPE GETSOCKNAME_ARG3_TYPE
/* Define if getpwnam_r exists but takes only 4 arguments (e.g., POSIX draft 6
implementations like some Solaris releases). */
/* #undef GETPWNAM_R_4_ARGS */
/* Define if getpwnam_r returns an int */
#define GETPWNAM_R_RETURNS_INT 1
/* Define if getpwuid_r exists but takes only 4 arguments (e.g., POSIX draft 6
implementations like some Solaris releases). */
/* #undef GETPWUID_R_4_ARGS */
/* Define if getservbyname_r returns int rather than struct servent * */
/* #undef GETSERVBYNAME_R_RETURNS_INT */
/* Type of pointer target for argument 3 to getsockname */
#define GETSOCKNAME_ARG3_TYPE socklen_t
/* Define if gmtime_r returns int instead of struct tm pointer, as on old
HP-UX systems. */
/* #undef GMTIME_R_RETURNS_INT */
/* Define if va_copy macro or function is available. */
#define HAS_VA_COPY 1
/* Define to 1 if you have the `access' function. */
#define HAVE_ACCESS 1
/* Define to 1 if you have the <alloca.h> header file. */
#define HAVE_ALLOCA_H 1
/* Define to 1 if you have the <arpa/inet.h> header file. */
#define HAVE_ARPA_INET_H 1
/* Define to 1 if you have the `bswap16' function. */
/* #undef HAVE_BSWAP16 */
/* Define to 1 if you have the `bswap64' function. */
/* #undef HAVE_BSWAP64 */
/* Define to 1 if bswap_16 is available via byteswap.h */
/* #undef HAVE_BSWAP_16 */
/* Define to 1 if bswap_64 is available via byteswap.h */
/* #undef HAVE_BSWAP_64 */
/* Define if bt_rseq is available, for recursive btree traversal. */
#define HAVE_BT_RSEQ 1
/* Define to 1 if you have the <byteswap.h> header file. */
/* #undef HAVE_BYTESWAP_H */
/* Define to 1 if you have the `chmod' function. */
#define HAVE_CHMOD 1
/* Define if cmocka library is available. */
/* #undef HAVE_CMOCKA */
/* Define to 1 if you have the `compile' function. */
/* #undef HAVE_COMPILE */
/* Define if com_err has compatible gettext support */
#define HAVE_COM_ERR_INTL 1
/* Define to 1 if you have the <cpuid.h> header file. */
/* #undef HAVE_CPUID_H */
/* Define to 1 if you have the `daemon' function. */
#define HAVE_DAEMON 1
/* Define to 1 if you have the declaration of `strerror_r', and to 0 if you
don't. */
#define HAVE_DECL_STRERROR_R 1
/* Define to 1 if you have the <dirent.h> header file, and it defines `DIR'.
*/
#define HAVE_DIRENT_H 1
/* Define to 1 if you have the <dlfcn.h> header file. */
#define HAVE_DLFCN_H 1
/* Define to 1 if you have the `dn_skipname' function. */
#define HAVE_DN_SKIPNAME 1
/* Define to 1 if you have the <endian.h> header file. */
/* #undef HAVE_ENDIAN_H */
/* Define to 1 if you have the <errno.h> header file. */
#define HAVE_ERRNO_H 1
/* Define to 1 if you have the `fchmod' function. */
#define HAVE_FCHMOD 1
/* Define to 1 if you have the <fcntl.h> header file. */
#define HAVE_FCNTL_H 1
/* Define to 1 if you have the `flock' function. */
#define HAVE_FLOCK 1
/* Define to 1 if you have the `fnmatch' function. */
#define HAVE_FNMATCH 1
/* Define to 1 if you have the <fnmatch.h> header file. */
#define HAVE_FNMATCH_H 1
/* Define if you have the getaddrinfo function */
#define HAVE_GETADDRINFO 1
/* Define to 1 if you have the `getcwd' function. */
#define HAVE_GETCWD 1
/* Define to 1 if you have the `getenv' function. */
#define HAVE_GETENV 1
/* Define to 1 if you have the `geteuid' function. */
#define HAVE_GETEUID 1
/* Define if gethostbyname_r exists and its return type is known */
/* #undef HAVE_GETHOSTBYNAME_R */
/* Define to 1 if you have the `getnameinfo' function. */
#define HAVE_GETNAMEINFO 1
/* Define if system getopt should be used. */
#define HAVE_GETOPT 1
/* Define if system getopt_long should be used. */
#define HAVE_GETOPT_LONG 1
/* Define if getpwnam_r is available and useful. */
#define HAVE_GETPWNAM_R 1
/* Define if getpwuid_r is available and useful. */
#define HAVE_GETPWUID_R 1
/* Define if getservbyname_r exists and its return type is known */
/* #undef HAVE_GETSERVBYNAME_R */
/* Have the gettimeofday function */
#define HAVE_GETTIMEOFDAY 1
/* Define to 1 if you have the `getusershell' function. */
#define HAVE_GETUSERSHELL 1
/* Define to 1 if you have the `gmtime_r' function. */
#define HAVE_GMTIME_R 1
/* Define to 1 if you have the <ifaddrs.h> header file. */
#define HAVE_IFADDRS_H 1
/* Define to 1 if you have the `inet_ntop' function. */
#define HAVE_INET_NTOP 1
/* Define to 1 if you have the `inet_pton' function. */
#define HAVE_INET_PTON 1
/* Define to 1 if the system has the type `int16_t'. */
#define HAVE_INT16_T 1
/* Define to 1 if the system has the type `int32_t'. */
#define HAVE_INT32_T 1
/* Define to 1 if the system has the type `int8_t'. */
#define HAVE_INT8_T 1
/* Define to 1 if you have the <inttypes.h> header file. */
#define HAVE_INTTYPES_H 1
/* Define to 1 if you have the <keyutils.h> header file. */
/* #undef HAVE_KEYUTILS_H */
/* Define to 1 if you have the <lber.h> header file. */
/* #undef HAVE_LBER_H */
/* Define to 1 if you have the <ldap.h> header file. */
/* #undef HAVE_LDAP_H */
/* Define to 1 if you have the `crypto' library (-lcrypto). */
#define HAVE_LIBCRYPTO 1
/* Define if building with libedit. */
/* #undef HAVE_LIBEDIT */
/* Define to 1 if you have the `nsl' library (-lnsl). */
/* #undef HAVE_LIBNSL */
/* Define to 1 if you have the `resolv' library (-lresolv). */
#define HAVE_LIBRESOLV 1
/* Define to 1 if you have the `socket' library (-lsocket). */
/* #undef HAVE_LIBSOCKET */
/* Define if the util library is available */
#define HAVE_LIBUTIL 1
/* Define to 1 if you have the <limits.h> header file. */
#define HAVE_LIMITS_H 1
/* Define to 1 if you have the `localtime_r' function. */
#define HAVE_LOCALTIME_R 1
/* Define to 1 if you have the <machine/byte_order.h> header file. */
#define HAVE_MACHINE_BYTE_ORDER_H 1
/* Define to 1 if you have the <machine/endian.h> header file. */
#define HAVE_MACHINE_ENDIAN_H 1
/* Define to 1 if you have the <memory.h> header file. */
#define HAVE_MEMORY_H 1
/* Define to 1 if you have the `mkstemp' function. */
#define HAVE_MKSTEMP 1
/* Define to 1 if you have the <ndir.h> header file, and it defines `DIR'. */
/* #undef HAVE_NDIR_H */
/* Define to 1 if you have the <netdb.h> header file. */
#define HAVE_NETDB_H 1
/* Define if netdb.h declares h_errno */
#define HAVE_NETDB_H_H_ERRNO 1
/* Define to 1 if you have the <netinet/in.h> header file. */
#define HAVE_NETINET_IN_H 1
/* Define to 1 if you have the `ns_initparse' function. */
#define HAVE_NS_INITPARSE 1
/* Define to 1 if you have the `ns_name_uncompress' function. */
#define HAVE_NS_NAME_UNCOMPRESS 1
/* Define if OpenSSL supports cms. */
#define HAVE_OPENSSL_CMS 1
/* Define to 1 if you have the <paths.h> header file. */
#define HAVE_PATHS_H 1
/* Define if persistent keyrings are supported */
/* #undef HAVE_PERSISTENT_KEYRING */
/* Define to 1 if you have the <poll.h> header file. */
#define HAVE_POLL_H 1
/* Define if #pragma weak references work */
/* #undef HAVE_PRAGMA_WEAK_REF */
/* Define if you have POSIX threads libraries and header files. */
#define HAVE_PTHREAD 1
/* Define to 1 if you have the `pthread_once' function. */
#define HAVE_PTHREAD_ONCE 1
/* Have PTHREAD_PRIO_INHERIT. */
#define HAVE_PTHREAD_PRIO_INHERIT 1
/* Define to 1 if you have the `pthread_rwlock_init' function. */
#define HAVE_PTHREAD_RWLOCK_INIT 1
/* Define if pthread_rwlock_init is provided in the thread library. */
#define HAVE_PTHREAD_RWLOCK_INIT_IN_THREAD_LIB 1
/* Define to 1 if you have the <pwd.h> header file. */
#define HAVE_PWD_H 1
/* Define if building with GNU Readline. */
/* #undef HAVE_READLINE */
/* Define if regcomp exists and functions */
#define HAVE_REGCOMP 1
/* Define to 1 if you have the `regexec' function. */
#define HAVE_REGEXEC 1
/* Define to 1 if you have the <regexpr.h> header file. */
/* #undef HAVE_REGEXPR_H */
/* Define to 1 if you have the <regex.h> header file. */
#define HAVE_REGEX_H 1
/* Define to 1 if you have the `res_nclose' function. */
#define HAVE_RES_NCLOSE 1
/* Define to 1 if you have the `res_ndestroy' function. */
#define HAVE_RES_NDESTROY 1
/* Define to 1 if you have the `res_ninit' function. */
#define HAVE_RES_NINIT 1
/* Define to 1 if you have the `res_nsearch' function. */
#define HAVE_RES_NSEARCH 1
/* Define to 1 if you have the `res_search' function */
#define HAVE_RES_SEARCH 1
/* Define to 1 if you have the `re_comp' function. */
/* #undef HAVE_RE_COMP */
/* Define to 1 if you have the `re_exec' function. */
/* #undef HAVE_RE_EXEC */
/* Define to 1 if you have the <sasl/sasl.h> header file. */
/* #undef HAVE_SASL_SASL_H */
/* Define if struct sockaddr contains sa_len */
#define HAVE_SA_LEN 1
/* Define to 1 if you have the `setegid' function. */
#define HAVE_SETEGID 1
/* Define to 1 if you have the `setenv' function. */
#define HAVE_SETENV 1
/* Define to 1 if you have the `seteuid' function. */
#define HAVE_SETEUID 1
/* Define if setluid provided in OSF/1 security library */
/* #undef HAVE_SETLUID */
/* Define to 1 if you have the `setregid' function. */
#define HAVE_SETREGID 1
/* Define to 1 if you have the `setresgid' function. */
/* #undef HAVE_SETRESGID */
/* Define to 1 if you have the `setresuid' function. */
/* #undef HAVE_SETRESUID */
/* Define to 1 if you have the `setreuid' function. */
#define HAVE_SETREUID 1
/* Define to 1 if you have the `setsid' function. */
#define HAVE_SETSID 1
/* Define to 1 if you have the `setvbuf' function. */
#define HAVE_SETVBUF 1
/* Define if there is a socklen_t type. If not, probably use size_t */
#define HAVE_SOCKLEN_T 1
/* Define to 1 if you have the `srand' function. */
#define HAVE_SRAND 1
/* Define to 1 if you have the `srand48' function. */
#define HAVE_SRAND48 1
/* Define to 1 if you have the `srandom' function. */
#define HAVE_SRANDOM 1
/* Define to 1 if the system has the type `ssize_t'. */
#define HAVE_SSIZE_T 1
/* Define to 1 if you have the `stat' function. */
#define HAVE_STAT 1
/* Define to 1 if you have the <stddef.h> header file. */
#define HAVE_STDDEF_H 1
/* Define to 1 if you have the <stdint.h> header file. */
#define HAVE_STDINT_H 1
/* Define to 1 if you have the <stdlib.h> header file. */
#define HAVE_STDLIB_H 1
/* Define to 1 if you have the `step' function. */
/* #undef HAVE_STEP */
/* Define to 1 if you have the `strchr' function. */
#define HAVE_STRCHR 1
/* Define to 1 if you have the `strdup' function. */
#define HAVE_STRDUP 1
/* Define to 1 if you have the `strerror' function. */
#define HAVE_STRERROR 1
/* Define to 1 if you have the `strerror_r' function. */
#define HAVE_STRERROR_R 1
/* Define to 1 if you have the <strings.h> header file. */
#define HAVE_STRINGS_H 1
/* Define to 1 if you have the <string.h> header file. */
#define HAVE_STRING_H 1
/* Define to 1 if you have the `strlcpy' function. */
#define HAVE_STRLCPY 1
/* Define to 1 if you have the `strptime' function. */
#define HAVE_STRPTIME 1
/* Define to 1 if the system has the type `struct cmsghdr'. */
#define HAVE_STRUCT_CMSGHDR 1
/* Define if there is a struct if_laddrconf. */
/* #undef HAVE_STRUCT_IF_LADDRCONF */
/* Define to 1 if the system has the type `struct in6_pktinfo'. */
#define HAVE_STRUCT_IN6_PKTINFO 1
/* Define to 1 if the system has the type `struct in_pktinfo'. */
#define HAVE_STRUCT_IN_PKTINFO 1
/* Define if there is a struct lifconf. */
/* #undef HAVE_STRUCT_LIFCONF */
/* Define to 1 if the system has the type `struct rt_msghdr'. */
#define HAVE_STRUCT_RT_MSGHDR 1
/* Define to 1 if the system has the type `struct sockaddr_storage'. */
#define HAVE_STRUCT_SOCKADDR_STORAGE 1
/* Define to 1 if `st_mtimensec' is a member of `struct stat'. */
/* #undef HAVE_STRUCT_STAT_ST_MTIMENSEC */
/* Define to 1 if `st_mtimespec.tv_nsec' is a member of `struct stat'. */
#define HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC 1
/* Define to 1 if `st_mtim.tv_nsec' is a member of `struct stat'. */
/* #undef HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC */
/* Define to 1 if you have the <sys/bswap.h> header file. */
/* #undef HAVE_SYS_BSWAP_H */
/* Define to 1 if you have the <sys/dir.h> header file, and it defines `DIR'.
*/
/* #undef HAVE_SYS_DIR_H */
/* Define if sys_errlist in libc */
#define HAVE_SYS_ERRLIST 1
/* Define to 1 if you have the <sys/file.h> header file. */
#define HAVE_SYS_FILE_H 1
/* Define to 1 if you have the <sys/filio.h> header file. */
#define HAVE_SYS_FILIO_H 1
/* Define to 1 if you have the <sys/ndir.h> header file, and it defines `DIR'.
*/
/* #undef HAVE_SYS_NDIR_H */
/* Define to 1 if you have the <sys/param.h> header file. */
#define HAVE_SYS_PARAM_H 1
/* Define to 1 if you have the <sys/select.h> header file. */
#define HAVE_SYS_SELECT_H 1
/* Define to 1 if you have the <sys/socket.h> header file. */
#define HAVE_SYS_SOCKET_H 1
/* Define to 1 if you have the <sys/sockio.h> header file. */
#define HAVE_SYS_SOCKIO_H 1
/* Define to 1 if you have the <sys/stat.h> header file. */
#define HAVE_SYS_STAT_H 1
/* Define to 1 if you have the <sys/time.h> header file. */
#define HAVE_SYS_TIME_H 1
/* Define to 1 if you have the <sys/types.h> header file. */
#define HAVE_SYS_TYPES_H 1
/* Define to 1 if you have the <sys/uio.h> header file. */
#define HAVE_SYS_UIO_H 1
/* Define if tcl.h found */
/* #undef HAVE_TCL_H */
/* Define if tcl/tcl.h found */
/* #undef HAVE_TCL_TCL_H */
/* Define to 1 if you have the `timegm' function. */
#define HAVE_TIMEGM 1
/* Define to 1 if you have the <time.h> header file. */
#define HAVE_TIME_H 1
/* Define to 1 if you have the <unistd.h> header file. */
#define HAVE_UNISTD_H 1
/* Define to 1 if you have the `unsetenv' function. */
#define HAVE_UNSETENV 1
/* Define to 1 if the system has the type `u_char'. */
#define HAVE_U_CHAR 1
/* Define to 1 if the system has the type `u_int'. */
#define HAVE_U_INT 1
/* Define to 1 if the system has the type `u_int16_t'. */
#define HAVE_U_INT16_T 1
/* Define to 1 if the system has the type `u_int32_t'. */
#define HAVE_U_INT32_T 1
/* Define to 1 if the system has the type `u_int8_t'. */
#define HAVE_U_INT8_T 1
/* Define to 1 if the system has the type `u_long'. */
#define HAVE_U_LONG 1
/* Define to 1 if you have the `vasprintf' function. */
#define HAVE_VASPRINTF 1
/* Define to 1 if you have the `vsnprintf' function. */
#define HAVE_VSNPRINTF 1
/* Define to 1 if you have the `vsprintf' function. */
#define HAVE_VSPRINTF 1
/* Define to 1 if the system has the type `__int128_t'. */
#define HAVE___INT128_T 1
/* Define to 1 if the system has the type `__uint128_t'. */
#define HAVE___UINT128_T 1
/* Define if errno.h declares perror */
/* #undef HDR_HAS_PERROR */
/* May need to be defined to enable IPv6 support, for example on IRIX */
/* #undef INET6 */
/* Define if MIT Project Athena default configuration should be used */
/* #undef KRB5_ATHENA_COMPAT */
/* Define for DNS support of locating realms and KDCs */
#undef KRB5_DNS_LOOKUP
/* Define to enable DNS lookups of Kerberos realm names */
/* #undef KRB5_DNS_LOOKUP_REALM */
/* Define if the KDC should return only vague error codes to clients */
/* #undef KRBCONF_VAGUE_ERRORS */
/* define if the system header files are missing prototype for daemon() */
#define NEED_DAEMON_PROTO 1
/* Define if in6addr_any is not defined in libc */
#define NEED_INSIXADDR_ANY 1
/* define if the system header files are missing prototype for
ss_execute_command() */
/* #undef NEED_SS_EXECUTE_COMMAND_PROTO */
/* define if the system header files are missing prototype for strptime() */
/* #undef NEED_STRPTIME_PROTO */
/* define if the system header files are missing prototype for swab() */
/* #undef NEED_SWAB_PROTO */
/* Define if need to declare sys_errlist */
/* #undef NEED_SYS_ERRLIST */
/* define if the system header files are missing prototype for vasprintf() */
/* #undef NEED_VASPRINTF_PROTO */
/* Define if the KDC should use no lookaside cache */
/* #undef NOCACHE */
/* Define if references to pthread routines should be non-weak. */
/* #undef NO_WEAK_PTHREADS */
/* Define if lex produes code with yylineno */
/* #undef NO_YYLINENO */
/* Define to the address where bug reports for this package should be sent. */
#define PACKAGE_BUGREPORT "krb5-bugs@mit.edu"
/* Define to the full name of this package. */
#define PACKAGE_NAME "Kerberos 5"
/* Define to the full name and version of this package. */
#define PACKAGE_STRING "Kerberos 5 1.17.1"
/* Define to the one symbol short name of this package. */
#define PACKAGE_TARNAME "krb5"
/* Define to the home page for this package. */
#define PACKAGE_URL ""
/* Define to the version of this package. */
#define PACKAGE_VERSION "1.17.1"
/* Define if setjmp indicates POSIX interface */
#define POSIX_SETJMP 1
/* Define if POSIX signal handling is used */
#define POSIX_SIGNALS 1
/* Define if POSIX signal handlers are used */
#define POSIX_SIGTYPE 1
/* Define if termios.h exists and tcsetattr exists */
#define POSIX_TERMIOS 1
/* Define to necessary symbol if this constant uses a non-standard name on
your system. */
/* #undef PTHREAD_CREATE_JOINABLE */
/* Define as the return type of signal handlers (`int' or `void'). */
#define RETSIGTYPE void
/* Define as return type of setrpcent */
#define SETRPCENT_TYPE void
/* The size of `size_t', as computed by sizeof. */
#define SIZEOF_SIZE_T 8
/* The size of `time_t', as computed by sizeof. */
#define SIZEOF_TIME_T 8
/* Define to use OpenSSL for SPAKE preauth */
#define SPAKE_OPENSSL 1
/* Define for static plugin linkage */
/* #undef STATIC_PLUGINS */
/* Define to 1 if you have the ANSI C header files. */
#define STDC_HEADERS 1
/* Define to 1 if strerror_r returns char *. */
/* #undef STRERROR_R_CHAR_P */
/* Define if sys_errlist is defined in errno.h */
#define SYS_ERRLIST_DECLARED 1
/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. */
#define TIME_WITH_SYS_TIME 1
/* Define if no TLS implementation is selected */
/* #undef TLS_IMPL_NONE */
/* Define if TLS implementation is OpenSSL */
#define TLS_IMPL_OPENSSL 1
/* Define if you have dirent.h functionality */
#define USE_DIRENT_H 1
/* Define if dlopen should be used */
#define USE_DLOPEN 1
/* Define if the keyring ccache should be enabled */
/* #undef USE_KEYRING_CCACHE */
/* Define if link-time options for library finalization will be used */
/* #undef USE_LINKER_FINI_OPTION */
/* Define if link-time options for library initialization will be used */
/* #undef USE_LINKER_INIT_OPTION */
/* Define if sigprocmask should be used */
#define USE_SIGPROCMASK 1
/* Define if wait takes int as a argument */
#define WAIT_USES_INT 1
/* Define to 1 if `lex' declares `yytext' as a `char *' by default, not a
`char[]'. */
#define YYTEXT_POINTER 1
/* Define to enable extensions in glibc */
#define _GNU_SOURCE 1
/* Define to enable C11 extensions */
#define __STDC_WANT_LIB_EXT1__ 1
/* Define to empty if `const' does not conform to ANSI C. */
/* #undef const */
/* Define to `int' if <sys/types.h> doesn't define. */
/* #undef gid_t */
/* Define to `__inline__' or `__inline' if that's what the C compiler
calls it, or to nothing if 'inline' is not supported under any name. */
#ifndef __cplusplus
/* #undef inline */
#endif
/* Define krb5_sigtype to type of signal handler */
#define krb5_sigtype void
/* Define to `int' if <sys/types.h> does not define. */
/* #undef mode_t */
/* Define to `long int' if <sys/types.h> does not define. */
/* #undef off_t */
/* Define to `long' if <sys/types.h> does not define. */
/* #undef time_t */
/* Define to `int' if <sys/types.h> doesn't define. */
/* #undef uid_t */
#if defined(__GNUC__) && !defined(inline)
/* Silence gcc pedantic warnings about ANSI C. */
# define inline __inline__
#endif
#endif /* KRB5_AUTOCONF_H */

View File

@ -1,4 +1,4 @@
# LDAP {#external-authenticators-ldap}
# LDAP {#external-authenticators-ldap}
LDAP server can be used to authenticate ClickHouse users. There are two different approaches for doing this:
@ -87,14 +87,13 @@ Note, that user `my_user` refers to `my_ldap_server`. This LDAP server must be c
When SQL-driven [Access Control and Account Management](../access-rights.md#access-control) is enabled in ClickHouse, users that are authenticated by LDAP servers can also be created using the [CRATE USER](../../sql-reference/statements/create/user.md#create-user-statement) statement.
```sql
CREATE USER my_user IDENTIFIED WITH ldap_server BY 'my_ldap_server'
CREATE USER my_user IDENTIFIED WITH ldap SERVER 'my_ldap_server'
```
## LDAP Exernal User Directory {#ldap-external-user-directory}
In addition to the locally defined users, a remote LDAP server can be used as a source of user definitions. In order to achieve this, specify previously defined LDAP server name (see [LDAP Server Definition](#ldap-server-definition)) in the `ldap` section inside the `users_directories` section of the `config.xml` file.
In addition to the locally defined users, a remote LDAP server can be used as a source of user definitions. In order to achieve this, specify previously defined LDAP server name (see [LDAP Server Definition](#ldap-server-definition)) in an `ldap` section inside the `users_directories` section of the `config.xml` file.
At each login attempt, ClickHouse will try to find the user definition locally and authenticate it as usual, but if the user is not defined, ClickHouse will assume it exists in the external LDAP directory, and will try to "bind" to the specified DN at the LDAP server using the provided credentials. If successful, the user will be considered existing and authenticated. The user will be assigned roles from the list specified in the `roles` section. Additionally, LDAP "search" can be performed and results can be transformed and treated as role names and then be assigned to the user if the `role_mapping` section is also configured. All this implies that the SQL-driven [Access Control and Account Management](../access-rights.md#access-control) is enabled and roles are created using the [CREATE ROLE](../../sql-reference/statements/create/role.md#create-role-statement) statement.
@ -153,4 +152,3 @@ Parameters:
- `prefix` - prefix, that will be expected to be in front of each string in the original
list of strings returned by the LDAP search. Prefix will be removed from the original
strings and resulting strings will be treated as local role names. Empty, by default.

View File

@ -12,10 +12,10 @@ Syntax:
``` sql
ALTER USER [IF EXISTS] name1 [ON CLUSTER cluster_name1] [RENAME TO new_name1]
[, name2 [ON CLUSTER cluster_name2] [RENAME TO new_name2] ...]
[IDENTIFIED [WITH {PLAINTEXT_PASSWORD|SHA256_PASSWORD|DOUBLE_SHA1_PASSWORD}] BY {'password'|'hash'}]
[[ADD|DROP] HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
[NOT IDENTIFIED | IDENTIFIED {[WITH {no_password | plaintext_password | sha256_password | sha256_hash | double_sha1_password | double_sha1_hash}] BY {'password' | 'hash'}} | {WITH ldap SERVER 'server_name'} | {WITH kerberos [REALM 'realm']}]
[[ADD | DROP] HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
[DEFAULT ROLE role [,...] | ALL | ALL EXCEPT role [,...] ]
[SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [READONLY|WRITABLE] | PROFILE 'profile_name'] [,...]
[SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [READONLY | WRITABLE] | PROFILE 'profile_name'] [,...]
```
To use `ALTER USER` you must have the [ALTER USER](../../../sql-reference/statements/grant.md#grant-access-management) privilege.

View File

@ -12,10 +12,10 @@ Syntax:
``` sql
CREATE USER [IF NOT EXISTS | OR REPLACE] name1 [ON CLUSTER cluster_name1]
[, name2 [ON CLUSTER cluster_name2] ...]
[IDENTIFIED [WITH {NO_PASSWORD|PLAINTEXT_PASSWORD|SHA256_PASSWORD|SHA256_HASH|DOUBLE_SHA1_PASSWORD|DOUBLE_SHA1_HASH|LDAP_SERVER}] BY {'password'|'hash'}]
[NOT IDENTIFIED | IDENTIFIED {[WITH {no_password | plaintext_password | sha256_password | sha256_hash | double_sha1_password | double_sha1_hash}] BY {'password' | 'hash'}} | {WITH ldap SERVER 'server_name'} | {WITH kerberos [REALM 'realm']}]
[HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
[DEFAULT ROLE role [,...]]
[SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [READONLY|WRITABLE] | PROFILE 'profile_name'] [,...]
[SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [READONLY | WRITABLE] | PROFILE 'profile_name'] [,...]
```
`ON CLUSTER` clause allows creating users on a cluster, see [Distributed DDL](../../../sql-reference/distributed-ddl.md).
@ -30,7 +30,8 @@ There are multiple ways of user identification:
- `IDENTIFIED WITH sha256_hash BY 'hash'`
- `IDENTIFIED WITH double_sha1_password BY 'qwerty'`
- `IDENTIFIED WITH double_sha1_hash BY 'hash'`
- `IDENTIFIED WITH ldap_server BY 'server'`
- `IDENTIFIED WITH ldap SERVER 'server_name'`
- `IDENTIFIED WITH kerberos` or `IDENTIFIED WITH kerberos REALM 'realm'`
## User Host {#user-host}

View File

@ -2472,7 +2472,7 @@ public:
/** If "--password [value]" is used but the value is omitted, the bad argument exception will be thrown.
* implicit_value is used to avoid this exception (to allow user to type just "--password")
* Since currently boost provides no way to check if a value has been set implicitly for an option,
* the "\n" is used to distinguish this case because there is hardly a chance an user would use "\n"
* the "\n" is used to distinguish this case because there is hardly a chance a user would use "\n"
* as the password.
*/
("password", po::value<std::string>()->implicit_value("\n", ""), "password")

View File

@ -240,7 +240,7 @@ try
/// Skip networking
/// Sets external authenticators config (LDAP).
/// Sets external authenticators config (LDAP, Kerberos).
global_context->setExternalAuthenticatorsConfig(config());
setupUsers();

View File

@ -362,6 +362,27 @@
-->
</ldap_servers>
<!-- To enable Kerberos authentication support for HTTP requests (GSS-SPNEGO), for those users who are explicitly configured
to authenticate via Kerberos, define a single 'kerberos' section here.
Parameters:
principal - canonical service principal name, that will be acquired and used when accepting security contexts.
This parameter is optional, if omitted, the default principal will be used.
This parameter cannot be specified together with 'realm' parameter.
realm - a realm, that will be used to restrict authentication to only those requests whose initiator's realm matches it.
This parameter is optional, if omitted, no additional filtering by realm will be applied.
This parameter cannot be specified together with 'principal' parameter.
Example:
<kerberos />
Example:
<kerberos>
<principal>HTTP/clickhouse.example.com@EXAMPLE.COM</principal>
</kerberos>
Example:
<kerberos>
<realm>EXAMPLE.COM</realm>
</kerberos>
-->
<!-- Sources to read users, roles, access rights, profiles of settings, quotas. -->
<user_directories>
<users_xml>

View File

@ -41,9 +41,18 @@
If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
If you want to specify a previously defined LDAP server (see 'ldap_servers' in main config) for authentication, place its name in 'server' element inside 'ldap' element.
If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
place its name in 'server' element inside 'ldap' element.
Example: <ldap><server>my_ldap_server</server></ldap>
If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),
place 'kerberos' element instead of 'password' (and similar) elements.
The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.
You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests
whose initiator's realm matches it.
Example: <kerberos />
Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>
How to generate decent password:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
In first line will be password and in second - corresponding SHA256.

View File

@ -361,9 +361,9 @@ void AccessControlManager::addStoragesFromMainConfig(
}
UUID AccessControlManager::login(const String & user_name, const String & password, const Poco::Net::IPAddress & address) const
UUID AccessControlManager::login(const Credentials & credentials, const Poco::Net::IPAddress & address) const
{
return MultipleAccessStorage::login(user_name, password, address, *external_authenticators);
return MultipleAccessStorage::login(credentials, address, *external_authenticators);
}
void AccessControlManager::setExternalAuthenticatorsConfig(const Poco::Util::AbstractConfiguration & config)

View File

@ -109,7 +109,7 @@ public:
bool isSettingNameAllowed(const std::string_view & name) const;
void checkSettingNameIsAllowed(const std::string_view & name) const;
UUID login(const String & user_name, const String & password, const Poco::Net::IPAddress & address) const;
UUID login(const Credentials & credentials, const Poco::Net::IPAddress & address) const;
void setExternalAuthenticatorsConfig(const Poco::Util::AbstractConfiguration & config);
std::shared_ptr<const ContextAccess> getContextAccess(

View File

@ -14,7 +14,7 @@ namespace DB
using Strings = std::vector<String>;
/// Represents lists of hosts an user is allowed to connect to server from.
/// Represents lists of hosts a user is allowed to connect to server from.
class AllowedClientHosts
{
public:

View File

@ -1,5 +1,8 @@
#include <Access/Authentication.h>
#include <Access/Credentials.h>
#include <Access/ExternalAuthenticators.h>
#include <Access/LDAPClient.h>
#include <Access/GSSAcceptor.h>
#include <Common/Exception.h>
#include <Poco/SHA1Engine.h>
@ -8,8 +11,8 @@ namespace DB
{
namespace ErrorCodes
{
extern const int BAD_ARGUMENTS;
extern const int NOT_IMPLEMENTED;
extern const int LOGICAL_ERROR;
}
@ -32,14 +35,13 @@ Authentication::Digest Authentication::getPasswordDoubleSHA1() const
return engine.digest();
}
case SHA256_PASSWORD:
throw Exception("Cannot get password double SHA1 for user with 'SHA256_PASSWORD' authentication", ErrorCodes::BAD_ARGUMENTS);
case DOUBLE_SHA1_PASSWORD:
return password_hash;
case LDAP_SERVER:
throw Exception("Cannot get password double SHA1 for user with 'LDAP_SERVER' authentication", ErrorCodes::BAD_ARGUMENTS);
case SHA256_PASSWORD:
case LDAP:
case KERBEROS:
throw Exception("Cannot get password double SHA1 hash for authentication type " + toString(type), ErrorCodes::LOGICAL_ERROR);
case MAX_TYPE:
break;
@ -48,44 +50,76 @@ Authentication::Digest Authentication::getPasswordDoubleSHA1() const
}
bool Authentication::isCorrectPassword(const String & user_, const String & password_, const ExternalAuthenticators & external_authenticators) const
bool Authentication::areCredentialsValid(const Credentials & credentials, const ExternalAuthenticators & external_authenticators) const
{
switch (type)
if (!credentials.isReady())
return false;
if (const auto * gss_acceptor_context = dynamic_cast<const GSSAcceptorContext *>(&credentials))
{
case NO_PASSWORD:
return true;
case PLAINTEXT_PASSWORD:
switch (type)
{
if (password_ == std::string_view{reinterpret_cast<const char *>(password_hash.data()), password_hash.size()})
return true;
case NO_PASSWORD:
case PLAINTEXT_PASSWORD:
case SHA256_PASSWORD:
case DOUBLE_SHA1_PASSWORD:
case LDAP:
throw Require<BasicCredentials>("ClickHouse Basic Authentication");
// For compatibility with MySQL clients which support only native authentication plugin, SHA1 can be passed instead of password.
auto password_sha1 = encodeSHA1(password_hash);
return password_ == std::string_view{reinterpret_cast<const char *>(password_sha1.data()), password_sha1.size()};
case KERBEROS:
return external_authenticators.checkKerberosCredentials(kerberos_realm, *gss_acceptor_context);
case MAX_TYPE:
break;
}
case SHA256_PASSWORD:
return encodeSHA256(password_) == password_hash;
case DOUBLE_SHA1_PASSWORD:
{
auto first_sha1 = encodeSHA1(password_);
/// If it was MySQL compatibility server, then first_sha1 already contains double SHA1.
if (first_sha1 == password_hash)
return true;
return encodeSHA1(first_sha1) == password_hash;
}
case LDAP_SERVER:
return external_authenticators.checkLDAPCredentials(server_name, user_, password_);
case MAX_TYPE:
break;
}
throw Exception("Cannot check if the password is correct for authentication type " + toString(type), ErrorCodes::NOT_IMPLEMENTED);
if (const auto * basic_credentials = dynamic_cast<const BasicCredentials *>(&credentials))
{
switch (type)
{
case NO_PASSWORD:
return true; // N.B. even if the password is not empty!
case PLAINTEXT_PASSWORD:
{
if (basic_credentials->getPassword() == std::string_view{reinterpret_cast<const char *>(password_hash.data()), password_hash.size()})
return true;
// For compatibility with MySQL clients which support only native authentication plugin, SHA1 can be passed instead of password.
const auto password_sha1 = encodeSHA1(password_hash);
return basic_credentials->getPassword() == std::string_view{reinterpret_cast<const char *>(password_sha1.data()), password_sha1.size()};
}
case SHA256_PASSWORD:
return encodeSHA256(basic_credentials->getPassword()) == password_hash;
case DOUBLE_SHA1_PASSWORD:
{
const auto first_sha1 = encodeSHA1(basic_credentials->getPassword());
/// If it was MySQL compatibility server, then first_sha1 already contains double SHA1.
if (first_sha1 == password_hash)
return true;
return encodeSHA1(first_sha1) == password_hash;
}
case LDAP:
return external_authenticators.checkLDAPCredentials(ldap_server_name, *basic_credentials);
case KERBEROS:
throw Require<GSSAcceptorContext>(kerberos_realm);
case MAX_TYPE:
break;
}
}
if ([[maybe_unused]] const auto * always_allow_credentials = dynamic_cast<const AlwaysAllowCredentials *>(&credentials))
return true;
throw Exception("areCredentialsValid(): authentication type " + toString(type) + " not supported", ErrorCodes::NOT_IMPLEMENTED);
}
}

View File

@ -6,8 +6,6 @@
#include <Poco/SHA1Engine.h>
#include <boost/algorithm/hex.hpp>
#include <boost/algorithm/string/case_conv.hpp>
#include <set>
#include <vector>
namespace DB
@ -20,13 +18,10 @@ namespace ErrorCodes
extern const int NOT_IMPLEMENTED;
}
class Credentials;
class ExternalAuthenticators;
struct LDAPSearchParams;
using LDAPSearchParamsList = std::vector<LDAPSearchParams>;
using LDAPSearchResults = std::set<String>;
using LDAPSearchResultsList = std::vector<LDAPSearchResults>;
/// Authentication type and encrypted password for checking when an user logins.
/// Authentication type and encrypted password for checking when a user logins.
class Authentication
{
public:
@ -46,7 +41,10 @@ public:
DOUBLE_SHA1_PASSWORD,
/// Password is checked by a [remote] LDAP server. Connection will be made at each authentication attempt.
LDAP_SERVER,
LDAP,
/// Kerberos authentication performed through GSS-API negotiation loop.
KERBEROS,
MAX_TYPE,
};
@ -58,6 +56,18 @@ public:
static const TypeInfo & get(Type type_);
};
// A signaling class used to communicate requirements for credentials.
template <typename CredentialsType>
class Require : public Exception
{
public:
explicit Require(const String & realm_);
const String & getRealm() const;
private:
const String realm;
};
using Digest = std::vector<uint8_t>;
Authentication(Authentication::Type type_ = NO_PASSWORD) : type(type_) {}
@ -88,14 +98,16 @@ public:
/// Allowed to use for Type::NO_PASSWORD, Type::PLAINTEXT_PASSWORD, Type::DOUBLE_SHA1_PASSWORD.
Digest getPasswordDoubleSHA1() const;
/// Sets an external authentication server name.
/// When authentication type is LDAP_SERVER, server name is expected to be the name of a preconfigured LDAP server.
const String & getServerName() const;
void setServerName(const String & server_name_);
/// Sets the server name for authentication type LDAP.
const String & getLDAPServerName() const;
void setLDAPServerName(const String & name);
/// Checks if the provided password is correct. Returns false if not.
/// User name and external authenticators are used by the specific authentication types only (e.g., LDAP_SERVER).
bool isCorrectPassword(const String & user_, const String & password_, const ExternalAuthenticators & external_authenticators) const;
/// Sets the realm name for authentication type KERBEROS.
const String & getKerberosRealm() const;
void setKerberosRealm(const String & realm);
/// Checks the credentials (passwords, readiness, etc.)
bool areCredentialsValid(const Credentials & credentials, const ExternalAuthenticators & external_authenticators) const;
friend bool operator ==(const Authentication & lhs, const Authentication & rhs) { return (lhs.type == rhs.type) && (lhs.password_hash == rhs.password_hash); }
friend bool operator !=(const Authentication & lhs, const Authentication & rhs) { return !(lhs == rhs); }
@ -109,7 +121,8 @@ private:
Type type = Type::NO_PASSWORD;
Digest password_hash;
String server_name;
String ldap_server_name;
String kerberos_realm;
};
@ -144,16 +157,35 @@ inline const Authentication::TypeInfo & Authentication::TypeInfo::get(Type type_
static const auto info = make_info("DOUBLE_SHA1_PASSWORD");
return info;
}
case LDAP_SERVER:
case LDAP:
{
static const auto info = make_info("LDAP_SERVER");
static const auto info = make_info("LDAP");
return info;
}
case MAX_TYPE: break;
case KERBEROS:
{
static const auto info = make_info("KERBEROS");
return info;
}
case MAX_TYPE:
break;
}
throw Exception("Unknown authentication type: " + std::to_string(static_cast<int>(type_)), ErrorCodes::LOGICAL_ERROR);
}
template <typename CredentialsType>
Authentication::Require<CredentialsType>::Require(const String & realm_)
: Exception("Credentials required", ErrorCodes::BAD_ARGUMENTS)
, realm(realm_)
{
}
template <typename CredentialsType>
const String & Authentication::Require<CredentialsType>::getRealm() const
{
return realm;
}
inline String toString(Authentication::Type type_)
{
return Authentication::TypeInfo::get(type_).raw_name;
@ -186,9 +218,6 @@ inline void Authentication::setPassword(const String & password_)
{
switch (type)
{
case NO_PASSWORD:
throw Exception("Cannot specify password for the 'NO_PASSWORD' authentication type", ErrorCodes::LOGICAL_ERROR);
case PLAINTEXT_PASSWORD:
return setPasswordHashBinary(encodePlainText(password_));
@ -198,10 +227,13 @@ inline void Authentication::setPassword(const String & password_)
case DOUBLE_SHA1_PASSWORD:
return setPasswordHashBinary(encodeDoubleSHA1(password_));
case LDAP_SERVER:
throw Exception("Cannot specify password for the 'LDAP_SERVER' authentication type", ErrorCodes::LOGICAL_ERROR);
case NO_PASSWORD:
case LDAP:
case KERBEROS:
throw Exception("Cannot specify password for authentication type " + toString(type), ErrorCodes::LOGICAL_ERROR);
case MAX_TYPE: break;
case MAX_TYPE:
break;
}
throw Exception("setPassword(): authentication type " + toString(type) + " not supported", ErrorCodes::NOT_IMPLEMENTED);
}
@ -225,8 +257,9 @@ inline void Authentication::setPasswordHashHex(const String & hash)
inline String Authentication::getPasswordHashHex() const
{
if (type == LDAP_SERVER)
throw Exception("Cannot get password of a user with the 'LDAP_SERVER' authentication type", ErrorCodes::LOGICAL_ERROR);
if (type == LDAP || type == KERBEROS)
throw Exception("Cannot get password hex hash for authentication type " + toString(type), ErrorCodes::LOGICAL_ERROR);
String hex;
hex.resize(password_hash.size() * 2);
boost::algorithm::hex(password_hash.begin(), password_hash.end(), hex.data());
@ -238,9 +271,6 @@ inline void Authentication::setPasswordHashBinary(const Digest & hash)
{
switch (type)
{
case NO_PASSWORD:
throw Exception("Cannot specify password for the 'NO_PASSWORD' authentication type", ErrorCodes::LOGICAL_ERROR);
case PLAINTEXT_PASSWORD:
{
password_hash = hash;
@ -269,22 +299,35 @@ inline void Authentication::setPasswordHashBinary(const Digest & hash)
return;
}
case LDAP_SERVER:
throw Exception("Cannot specify password for the 'LDAP_SERVER' authentication type", ErrorCodes::LOGICAL_ERROR);
case NO_PASSWORD:
case LDAP:
case KERBEROS:
throw Exception("Cannot specify password binary hash for authentication type " + toString(type), ErrorCodes::LOGICAL_ERROR);
case MAX_TYPE: break;
case MAX_TYPE:
break;
}
throw Exception("setPasswordHashBinary(): authentication type " + toString(type) + " not supported", ErrorCodes::NOT_IMPLEMENTED);
}
inline const String & Authentication::getServerName() const
inline const String & Authentication::getLDAPServerName() const
{
return server_name;
return ldap_server_name;
}
inline void Authentication::setServerName(const String & server_name_)
inline void Authentication::setLDAPServerName(const String & name)
{
server_name = server_name_;
ldap_server_name = name;
}
inline const String & Authentication::getKerberosRealm() const
{
return kerberos_realm;
}
inline void Authentication::setKerberosRealm(const String & realm)
{
kerberos_realm = realm;
}
}

View File

@ -0,0 +1,86 @@
#include <Access/Credentials.h>
#include <Common/Exception.h>
namespace DB
{
namespace ErrorCodes
{
extern const int LOGICAL_ERROR;
}
Credentials::Credentials(const String & user_name_)
: user_name(user_name_)
{
}
const String & Credentials::getUserName() const
{
if (!isReady())
throwNotReady();
return user_name;
}
bool Credentials::isReady() const
{
return is_ready;
}
void Credentials::throwNotReady()
{
throw Exception("Credentials are not ready", ErrorCodes::LOGICAL_ERROR);
}
AlwaysAllowCredentials::AlwaysAllowCredentials()
{
is_ready = true;
}
AlwaysAllowCredentials::AlwaysAllowCredentials(const String & user_name_)
: Credentials(user_name_)
{
is_ready = true;
}
void AlwaysAllowCredentials::setUserName(const String & user_name_)
{
user_name = user_name_;
}
BasicCredentials::BasicCredentials()
{
is_ready = true;
}
BasicCredentials::BasicCredentials(const String & user_name_)
: Credentials(user_name_)
{
is_ready = true;
}
BasicCredentials::BasicCredentials(const String & user_name_, const String & password_)
: Credentials(user_name_)
, password(password_)
{
is_ready = true;
}
void BasicCredentials::setUserName(const String & user_name_)
{
user_name = user_name_;
}
void BasicCredentials::setPassword(const String & password_)
{
password = password_;
}
const String & BasicCredentials::getPassword() const
{
if (!isReady())
throwNotReady();
return password;
}
}

55
src/Access/Credentials.h Normal file
View File

@ -0,0 +1,55 @@
#pragma once
#include <common/types.h>
#include <memory>
namespace DB
{
class Credentials
{
public:
explicit Credentials() = default;
explicit Credentials(const String & user_name_);
virtual ~Credentials() = default;
const String & getUserName() const;
bool isReady() const;
protected:
[[noreturn]] static void throwNotReady();
protected:
bool is_ready = false;
String user_name;
};
class AlwaysAllowCredentials
: public Credentials
{
public:
explicit AlwaysAllowCredentials();
explicit AlwaysAllowCredentials(const String & user_name_);
void setUserName(const String & user_name_);
};
class BasicCredentials
: public Credentials
{
public:
explicit BasicCredentials();
explicit BasicCredentials(const String & user_name_);
explicit BasicCredentials(const String & user_name_, const String & password_);
void setUserName(const String & user_name_);
void setPassword(const String & password_);
const String & getPassword() const;
private:
String password;
};
}

View File

@ -20,14 +20,14 @@ namespace ErrorCodes
namespace
{
auto parseLDAPServer(const Poco::Util::AbstractConfiguration & config, const String & ldap_server_name)
auto parseLDAPServer(const Poco::Util::AbstractConfiguration & config, const String & name)
{
if (ldap_server_name.empty())
if (name.empty())
throw Exception("LDAP server name cannot be empty", ErrorCodes::BAD_ARGUMENTS);
LDAPServerParams params;
LDAPClient::Params params;
const String ldap_server_config = "ldap_servers." + ldap_server_name;
const String ldap_server_config = "ldap_servers." + name;
const bool has_host = config.has(ldap_server_config + ".host");
const bool has_port = config.has(ldap_server_config + ".port");
@ -75,11 +75,11 @@ auto parseLDAPServer(const Poco::Util::AbstractConfiguration & config, const Str
boost::to_lower(enable_tls_lc_str);
if (enable_tls_lc_str == "starttls")
params.enable_tls = LDAPServerParams::TLSEnable::YES_STARTTLS;
params.enable_tls = LDAPClient::Params::TLSEnable::YES_STARTTLS;
else if (config.getBool(ldap_server_config + ".enable_tls"))
params.enable_tls = LDAPServerParams::TLSEnable::YES;
params.enable_tls = LDAPClient::Params::TLSEnable::YES;
else
params.enable_tls = LDAPServerParams::TLSEnable::NO;
params.enable_tls = LDAPClient::Params::TLSEnable::NO;
}
if (has_tls_minimum_protocol_version)
@ -88,15 +88,15 @@ auto parseLDAPServer(const Poco::Util::AbstractConfiguration & config, const Str
boost::to_lower(tls_minimum_protocol_version_lc_str);
if (tls_minimum_protocol_version_lc_str == "ssl2")
params.tls_minimum_protocol_version = LDAPServerParams::TLSProtocolVersion::SSL2;
params.tls_minimum_protocol_version = LDAPClient::Params::TLSProtocolVersion::SSL2;
else if (tls_minimum_protocol_version_lc_str == "ssl3")
params.tls_minimum_protocol_version = LDAPServerParams::TLSProtocolVersion::SSL3;
params.tls_minimum_protocol_version = LDAPClient::Params::TLSProtocolVersion::SSL3;
else if (tls_minimum_protocol_version_lc_str == "tls1.0")
params.tls_minimum_protocol_version = LDAPServerParams::TLSProtocolVersion::TLS1_0;
params.tls_minimum_protocol_version = LDAPClient::Params::TLSProtocolVersion::TLS1_0;
else if (tls_minimum_protocol_version_lc_str == "tls1.1")
params.tls_minimum_protocol_version = LDAPServerParams::TLSProtocolVersion::TLS1_1;
params.tls_minimum_protocol_version = LDAPClient::Params::TLSProtocolVersion::TLS1_1;
else if (tls_minimum_protocol_version_lc_str == "tls1.2")
params.tls_minimum_protocol_version = LDAPServerParams::TLSProtocolVersion::TLS1_2;
params.tls_minimum_protocol_version = LDAPClient::Params::TLSProtocolVersion::TLS1_2;
else
throw Exception("Bad value for 'tls_minimum_protocol_version' entry, allowed values are: 'ssl2', 'ssl3', 'tls1.0', 'tls1.1', 'tls1.2'", ErrorCodes::BAD_ARGUMENTS);
}
@ -107,13 +107,13 @@ auto parseLDAPServer(const Poco::Util::AbstractConfiguration & config, const Str
boost::to_lower(tls_require_cert_lc_str);
if (tls_require_cert_lc_str == "never")
params.tls_require_cert = LDAPServerParams::TLSRequireCert::NEVER;
params.tls_require_cert = LDAPClient::Params::TLSRequireCert::NEVER;
else if (tls_require_cert_lc_str == "allow")
params.tls_require_cert = LDAPServerParams::TLSRequireCert::ALLOW;
params.tls_require_cert = LDAPClient::Params::TLSRequireCert::ALLOW;
else if (tls_require_cert_lc_str == "try")
params.tls_require_cert = LDAPServerParams::TLSRequireCert::TRY;
params.tls_require_cert = LDAPClient::Params::TLSRequireCert::TRY;
else if (tls_require_cert_lc_str == "demand")
params.tls_require_cert = LDAPServerParams::TLSRequireCert::DEMAND;
params.tls_require_cert = LDAPClient::Params::TLSRequireCert::DEMAND;
else
throw Exception("Bad value for 'tls_require_cert' entry, allowed values are: 'never', 'allow', 'try', 'demand'", ErrorCodes::BAD_ARGUMENTS);
}
@ -142,7 +142,44 @@ auto parseLDAPServer(const Poco::Util::AbstractConfiguration & config, const Str
params.port = port;
}
else
params.port = (params.enable_tls == LDAPServerParams::TLSEnable::YES ? 636 : 389);
params.port = (params.enable_tls == LDAPClient::Params::TLSEnable::YES ? 636 : 389);
return params;
}
auto parseKerberosParams(const Poco::Util::AbstractConfiguration & config)
{
GSSAcceptorContext::Params params;
Poco::Util::AbstractConfiguration::Keys keys;
config.keys("kerberos", keys);
std::size_t reealm_key_count = 0;
std::size_t principal_keys_count = 0;
for (auto key : keys)
{
const auto bracket_pos = key.find('[');
if (bracket_pos != std::string::npos)
key.resize(bracket_pos);
boost::algorithm::to_lower(key);
reealm_key_count += (key == "realm");
principal_keys_count += (key == "principal");
}
if (reealm_key_count > 0 && principal_keys_count > 0)
throw Exception("Realm and principal name cannot be specified simultaneously", ErrorCodes::BAD_ARGUMENTS);
if (reealm_key_count > 1)
throw Exception("Multiple realm sections are not allowed", ErrorCodes::BAD_ARGUMENTS);
if (principal_keys_count > 1)
throw Exception("Multiple principal sections are not allowed", ErrorCodes::BAD_ARGUMENTS);
params.realm = config.getString("kerberos.realm", "");
params.principal = config.getString("kerberos.principal", "");
return params;
}
@ -152,48 +189,82 @@ auto parseLDAPServer(const Poco::Util::AbstractConfiguration & config, const Str
void ExternalAuthenticators::reset()
{
std::scoped_lock lock(mutex);
ldap_server_params.clear();
ldap_server_caches.clear();
ldap_client_params_blueprint.clear();
ldap_caches.clear();
kerberos_params.reset();
}
void ExternalAuthenticators::setConfiguration(const Poco::Util::AbstractConfiguration & config, Poco::Logger * log)
{
std::scoped_lock lock(mutex);
reset();
Poco::Util::AbstractConfiguration::Keys all_keys;
config.keys("", all_keys);
std::size_t ldap_servers_key_count = 0;
std::size_t kerberos_keys_count = 0;
for (auto key : all_keys)
{
const auto bracket_pos = key.find('[');
if (bracket_pos != std::string::npos)
key.resize(bracket_pos);
boost::algorithm::to_lower(key);
ldap_servers_key_count += (key == "ldap_servers");
kerberos_keys_count += (key == "kerberos");
}
if (ldap_servers_key_count > 1)
throw Exception("Multiple ldap_servers sections are not allowed", ErrorCodes::BAD_ARGUMENTS);
if (kerberos_keys_count > 1)
throw Exception("Multiple kerberos sections are not allowed", ErrorCodes::BAD_ARGUMENTS);
Poco::Util::AbstractConfiguration::Keys ldap_server_names;
config.keys("ldap_servers", ldap_server_names);
for (const auto & ldap_server_name : ldap_server_names)
{
try
{
ldap_server_params.insert_or_assign(ldap_server_name, parseLDAPServer(config, ldap_server_name));
ldap_client_params_blueprint.insert_or_assign(ldap_server_name, parseLDAPServer(config, ldap_server_name));
}
catch (...)
{
tryLogCurrentException(log, "Could not parse LDAP server " + backQuote(ldap_server_name));
}
}
try
{
if (kerberos_keys_count > 0)
kerberos_params = parseKerberosParams(config);
}
catch (...)
{
tryLogCurrentException(log, "Could not parse Kerberos section");
}
}
bool ExternalAuthenticators::checkLDAPCredentials(const String & server, const String & user_name, const String & password,
const LDAPSearchParamsList * search_params, LDAPSearchResultsList * search_results) const
bool ExternalAuthenticators::checkLDAPCredentials(const String & server, const BasicCredentials & credentials,
const LDAPClient::SearchParamsList * search_params, LDAPClient::SearchResultsList * search_results) const
{
std::optional<LDAPServerParams> params;
std::optional<LDAPClient::Params> params;
std::size_t params_hash = 0;
{
std::scoped_lock lock(mutex);
// Retrieve the server parameters.
const auto pit = ldap_server_params.find(server);
if (pit == ldap_server_params.end())
const auto pit = ldap_client_params_blueprint.find(server);
if (pit == ldap_client_params_blueprint.end())
throw Exception("LDAP server '" + server + "' is not configured", ErrorCodes::BAD_ARGUMENTS);
params = pit->second;
params->user = user_name;
params->password = password;
params->user = credentials.getUserName();
params->password = credentials.getPassword();
params->combineCoreHash(params_hash);
if (search_params)
@ -207,12 +278,12 @@ bool ExternalAuthenticators::checkLDAPCredentials(const String & server, const S
// Check the cache, but only if the caching is enabled at all.
if (params->verification_cooldown > std::chrono::seconds{0})
{
const auto cit = ldap_server_caches.find(server);
if (cit != ldap_server_caches.end())
const auto cit = ldap_caches.find(server);
if (cit != ldap_caches.end())
{
auto & cache = cit->second;
const auto eit = cache.find(user_name);
const auto eit = cache.find(credentials.getUserName());
if (eit != cache.end())
{
const auto & entry = eit->second;
@ -249,7 +320,7 @@ bool ExternalAuthenticators::checkLDAPCredentials(const String & server, const S
// Erase the cache, if empty.
if (cache.empty())
ldap_server_caches.erase(cit);
ldap_caches.erase(cit);
}
}
}
@ -264,13 +335,13 @@ bool ExternalAuthenticators::checkLDAPCredentials(const String & server, const S
std::scoped_lock lock(mutex);
// If the server was removed from the config while we were checking the password, we discard the current result.
const auto pit = ldap_server_params.find(server);
if (pit == ldap_server_params.end())
const auto pit = ldap_client_params_blueprint.find(server);
if (pit == ldap_client_params_blueprint.end())
return false;
auto new_params = pit->second;
new_params.user = user_name;
new_params.password = password;
new_params.user = credentials.getUserName();
new_params.password = credentials.getPassword();
std::size_t new_params_hash = 0;
new_params.combineCoreHash(new_params_hash);
@ -286,7 +357,7 @@ bool ExternalAuthenticators::checkLDAPCredentials(const String & server, const S
if (params_hash != new_params_hash)
return false;
auto & entry = ldap_server_caches[server][user_name];
auto & entry = ldap_caches[server][credentials.getUserName()];
if (entry.last_successful_authentication_timestamp < current_check_timestamp)
{
entry.last_successful_params_hash = params_hash;
@ -314,4 +385,33 @@ bool ExternalAuthenticators::checkLDAPCredentials(const String & server, const S
return result;
}
bool ExternalAuthenticators::checkKerberosCredentials(const String & realm, const GSSAcceptorContext & credentials) const
{
std::scoped_lock lock(mutex);
if (!kerberos_params.has_value())
throw Exception("Kerberos is not enabled", ErrorCodes::BAD_ARGUMENTS);
if (!credentials.isReady())
return false;
if (credentials.isFailed())
return false;
if (!realm.empty() && realm != credentials.getRealm())
return false;
return true;
}
GSSAcceptorContext::Params ExternalAuthenticators::getKerberosParams() const
{
std::scoped_lock lock(mutex);
if (!kerberos_params.has_value())
throw Exception("Kerberos is not enabled", ErrorCodes::BAD_ARGUMENTS);
return kerberos_params.value();
}
}

View File

@ -1,11 +1,14 @@
#pragma once
#include <Access/LDAPParams.h>
#include <Access/LDAPClient.h>
#include <Access/Credentials.h>
#include <Access/GSSAcceptor.h>
#include <common/types.h>
#include <chrono>
#include <map>
#include <mutex>
#include <optional>
#include <unordered_map>
@ -28,25 +31,31 @@ class ExternalAuthenticators
public:
void reset();
void setConfiguration(const Poco::Util::AbstractConfiguration & config, Poco::Logger * log);
bool checkLDAPCredentials(const String & server, const String & user_name, const String & password,
const LDAPSearchParamsList * search_params = nullptr, LDAPSearchResultsList * search_results = nullptr) const;
// The name and readiness of the credentials must be verified before calling these.
bool checkLDAPCredentials(const String & server, const BasicCredentials & credentials,
const LDAPClient::SearchParamsList * search_params = nullptr, LDAPClient::SearchResultsList * search_results = nullptr) const;
bool checkKerberosCredentials(const String & realm, const GSSAcceptorContext & credentials) const;
GSSAcceptorContext::Params getKerberosParams() const;
private:
struct LDAPCacheEntry
{
std::size_t last_successful_params_hash = 0;
std::chrono::steady_clock::time_point last_successful_authentication_timestamp;
LDAPSearchResultsList last_successful_search_results;
LDAPClient::SearchResultsList last_successful_search_results;
};
using LDAPServerCache = std::unordered_map<String, LDAPCacheEntry>; // user name -> cache entry
using LDAPServerCaches = std::map<String, LDAPServerCache>; // server name -> cache
using LDAPServersParams = std::map<String, LDAPServerParams>; // server name -> params
using LDAPCache = std::unordered_map<String, LDAPCacheEntry>; // user name -> cache entry
using LDAPCaches = std::map<String, LDAPCache>; // server name -> cache
using LDAPParams = std::map<String, LDAPClient::Params>; // server name -> params
private:
mutable std::recursive_mutex mutex;
LDAPServersParams ldap_server_params;
mutable LDAPServerCaches ldap_server_caches;
LDAPParams ldap_client_params_blueprint;
mutable LDAPCaches ldap_caches;
std::optional<GSSAcceptorContext::Params> kerberos_params;
};
}

469
src/Access/GSSAcceptor.cpp Normal file
View File

@ -0,0 +1,469 @@
#include <Access/GSSAcceptor.h>
#include <Common/Exception.h>
#include <ext/scope_guard.h>
#include <Poco/StringTokenizer.h>
#include <mutex>
#include <tuple>
namespace DB
{
namespace ErrorCodes
{
extern const int BAD_ARGUMENTS;
extern const int FEATURE_IS_NOT_ENABLED_AT_BUILD_TIME;
extern const int KERBEROS_ERROR;
}
GSSAcceptorContext::GSSAcceptorContext(const GSSAcceptorContext::Params& params_)
: params(params_)
{
}
GSSAcceptorContext::~GSSAcceptorContext()
{
resetHandles();
}
const String & GSSAcceptorContext::getRealm() const
{
if (!isReady())
throwNotReady();
return realm;
}
bool GSSAcceptorContext::isFailed() const
{
return is_failed;
}
#if USE_KRB5
namespace
{
std::recursive_mutex gss_global_mutex;
struct PrincipalName
{
explicit PrincipalName(String principal);
// operator String() const;
String name;
std::vector<String> instances;
String realm;
};
PrincipalName::PrincipalName(String principal)
{
const auto at_pos = principal.find('@');
if (at_pos != std::string::npos)
{
realm = principal.substr(at_pos + 1);
principal.resize(at_pos);
}
Poco::StringTokenizer st(principal, "/");
auto it = st.begin();
if (it != st.end())
{
name = *it;
instances.assign(++it, st.end());
}
}
/*
PrincipalName::operator String() const
{
String principal = name;
for (const auto & instance : instances)
{
principal += '/';
principal += instance;
}
principal += '@';
principal += realm;
return principal;
}
*/
String bufferToString(const gss_buffer_desc & buf)
{
String str;
if (buf.length > 0 && buf.value != nullptr)
{
str.assign(static_cast<char *>(buf.value), buf.length);
while (!str.empty() && str.back() == '\0') { str.pop_back(); }
}
return str;
}
String extractSpecificStatusMessages(OM_uint32 status_code, int status_type, const gss_OID & mech_type)
{
std::scoped_lock lock(gss_global_mutex);
String messages;
OM_uint32 message_context = 0;
do
{
gss_buffer_desc status_string_buf;
status_string_buf.length = 0;
status_string_buf.value = nullptr;
SCOPE_EXIT({
OM_uint32 minor_status = 0;
[[maybe_unused]] OM_uint32 major_status = gss_release_buffer(
&minor_status,
&status_string_buf
);
});
OM_uint32 minor_status = 0;
[[maybe_unused]] OM_uint32 major_status = gss_display_status(
&minor_status,
status_code,
status_type,
mech_type,
&message_context,
&status_string_buf
);
const auto message = bufferToString(status_string_buf);
if (!message.empty())
{
if (!messages.empty())
messages += ", ";
messages += message;
}
} while (message_context != 0);
return messages;
}
String extractStatusMessages(OM_uint32 major_status_code, OM_uint32 minor_status_code, const gss_OID & mech_type)
{
std::scoped_lock lock(gss_global_mutex);
const auto gss_messages = extractSpecificStatusMessages(major_status_code, GSS_C_GSS_CODE, mech_type);
const auto mech_messages = extractSpecificStatusMessages(minor_status_code, GSS_C_MECH_CODE, mech_type);
String messages;
if (!gss_messages.empty())
messages += "Majors: " + gss_messages;
if (!mech_messages.empty())
{
if (!messages.empty())
messages += "; ";
messages += "Minors: " + mech_messages;
}
return messages;
}
std::pair<String, String> extractNameAndRealm(const gss_name_t & name)
{
std::scoped_lock lock(gss_global_mutex);
gss_buffer_desc name_buf;
name_buf.length = 0;
name_buf.value = nullptr;
SCOPE_EXIT({
OM_uint32 minor_status = 0;
[[maybe_unused]] OM_uint32 major_status = gss_release_buffer(
&minor_status,
&name_buf
);
});
OM_uint32 minor_status = 0;
[[maybe_unused]] OM_uint32 major_status = gss_display_name(
&minor_status,
name,
&name_buf,
nullptr
);
const PrincipalName principal(bufferToString(name_buf));
return { principal.name, principal.realm };
}
bool equalMechanisms(const String & left_str, const gss_OID & right_oid)
{
std::scoped_lock lock(gss_global_mutex);
gss_buffer_desc left_buf;
left_buf.length = left_str.size();
left_buf.value = const_cast<char *>(left_str.c_str());
gss_OID left_oid = GSS_C_NO_OID;
SCOPE_EXIT({
if (left_oid != GSS_C_NO_OID)
{
OM_uint32 minor_status = 0;
[[maybe_unused]] OM_uint32 major_status = gss_release_oid(
&minor_status,
&left_oid
);
left_oid = GSS_C_NO_OID;
}
});
OM_uint32 minor_status = 0;
OM_uint32 major_status = gss_str_to_oid(
&minor_status,
&left_buf,
&left_oid
);
if (GSS_ERROR(major_status))
return false;
return gss_oid_equal(left_oid, right_oid);
}
}
void GSSAcceptorContext::reset()
{
is_ready = false;
is_failed = false;
user_name.clear();
realm.clear();
initHandles();
}
void GSSAcceptorContext::resetHandles() noexcept
{
std::scoped_lock lock(gss_global_mutex);
if (acceptor_credentials_handle != GSS_C_NO_CREDENTIAL)
{
OM_uint32 minor_status = 0;
[[maybe_unused]] OM_uint32 major_status = gss_release_cred(
&minor_status,
&acceptor_credentials_handle
);
acceptor_credentials_handle = GSS_C_NO_CREDENTIAL;
}
if (context_handle != GSS_C_NO_CONTEXT)
{
OM_uint32 minor_status = 0;
[[maybe_unused]] OM_uint32 major_status = gss_delete_sec_context(
&minor_status,
&context_handle,
GSS_C_NO_BUFFER
);
context_handle = GSS_C_NO_CONTEXT;
}
}
void GSSAcceptorContext::initHandles()
{
std::scoped_lock lock(gss_global_mutex);
resetHandles();
if (!params.principal.empty())
{
if (!params.realm.empty())
throw Exception("Realm and principal name cannot be specified simultaneously", ErrorCodes::BAD_ARGUMENTS);
gss_buffer_desc acceptor_name_buf;
acceptor_name_buf.length = params.principal.size();
acceptor_name_buf.value = const_cast<char *>(params.principal.c_str());
gss_name_t acceptor_name = GSS_C_NO_NAME;
SCOPE_EXIT({
if (acceptor_name != GSS_C_NO_NAME)
{
OM_uint32 minor_status = 0;
[[maybe_unused]] OM_uint32 major_status = gss_release_name(
&minor_status,
&acceptor_name
);
acceptor_name = GSS_C_NO_NAME;
}
});
OM_uint32 minor_status = 0;
OM_uint32 major_status = gss_import_name(
&minor_status,
&acceptor_name_buf,
GSS_C_NT_HOSTBASED_SERVICE,
&acceptor_name
);
if (GSS_ERROR(major_status))
{
const auto messages = extractStatusMessages(major_status, minor_status, GSS_C_NO_OID);
throw Exception("gss_import_name() failed" + (messages.empty() ? "" : ": " + messages), ErrorCodes::KERBEROS_ERROR);
}
minor_status = 0;
major_status = gss_acquire_cred(
&minor_status,
acceptor_name,
GSS_C_INDEFINITE,
GSS_C_NO_OID_SET,
GSS_C_ACCEPT,
&acceptor_credentials_handle,
nullptr,
nullptr
);
if (GSS_ERROR(major_status))
{
const auto messages = extractStatusMessages(major_status, minor_status, GSS_C_NO_OID);
throw Exception("gss_acquire_cred() failed" + (messages.empty() ? "" : ": " + messages), ErrorCodes::KERBEROS_ERROR);
}
}
}
String GSSAcceptorContext::processToken(const String & input_token, Poco::Logger * log)
{
std::scoped_lock lock(gss_global_mutex);
String output_token;
try
{
if (is_ready || is_failed || context_handle == GSS_C_NO_CONTEXT)
reset();
gss_buffer_desc input_token_buf;
input_token_buf.length = input_token.size();
input_token_buf.value = const_cast<char *>(input_token.c_str());
gss_buffer_desc output_token_buf;
output_token_buf.length = 0;
output_token_buf.value = nullptr;
gss_name_t initiator_name = GSS_C_NO_NAME;
gss_OID mech_type = GSS_C_NO_OID;
OM_uint32 flags = 0;
SCOPE_EXIT({
if (initiator_name != GSS_C_NO_NAME)
{
OM_uint32 minor_status = 0;
[[maybe_unused]] OM_uint32 major_status = gss_release_name(
&minor_status,
&initiator_name
);
initiator_name = GSS_C_NO_NAME;
}
OM_uint32 minor_status = 0;
[[maybe_unused]] OM_uint32 major_status = gss_release_buffer(
&minor_status,
&output_token_buf
);
});
OM_uint32 minor_status = 0;
OM_uint32 major_status = gss_accept_sec_context(
&minor_status,
&context_handle,
acceptor_credentials_handle,
&input_token_buf,
GSS_C_NO_CHANNEL_BINDINGS,
&initiator_name,
&mech_type,
&output_token_buf,
&flags,
nullptr,
nullptr
);
if (major_status == GSS_S_COMPLETE)
{
if (!params.mechanism.empty() && !equalMechanisms(params.mechanism, mech_type))
throw Exception("gss_accept_sec_context() succeeded, but: the authentication mechanism is not what was expected", ErrorCodes::KERBEROS_ERROR);
if (flags & GSS_C_ANON_FLAG)
throw Exception("gss_accept_sec_context() succeeded, but: the initiator does not wish to be authenticated", ErrorCodes::KERBEROS_ERROR);
std::tie(user_name, realm) = extractNameAndRealm(initiator_name);
if (user_name.empty())
throw Exception("gss_accept_sec_context() succeeded, but: the initiator name cannot be extracted", ErrorCodes::KERBEROS_ERROR);
if (realm.empty())
throw Exception("gss_accept_sec_context() succeeded, but: the initiator realm cannot be extracted", ErrorCodes::KERBEROS_ERROR);
if (!params.realm.empty() && params.realm != realm)
throw Exception("gss_accept_sec_context() succeeded, but: the initiator realm is not what was expected (expected: " + params.realm + ", actual: " + realm + ")", ErrorCodes::KERBEROS_ERROR);
output_token = bufferToString(output_token_buf);
is_ready = true;
is_failed = false;
resetHandles();
}
else if (!GSS_ERROR(major_status) && (major_status & GSS_S_CONTINUE_NEEDED))
{
output_token = bufferToString(output_token_buf);
is_ready = false;
is_failed = false;
}
else
{
const auto messages = extractStatusMessages(major_status, minor_status, mech_type);
throw Exception("gss_accept_sec_context() failed" + (messages.empty() ? "" : ": " + messages), ErrorCodes::KERBEROS_ERROR);
}
}
catch (...)
{
tryLogCurrentException(log, "Could not process GSS token");
is_ready = true;
is_failed = true;
resetHandles();
}
return output_token;
}
#else // USE_KRB5
void GSSAcceptorContext::reset()
{
}
void GSSAcceptorContext::resetHandles() noexcept
{
}
void GSSAcceptorContext::initHandles()
{
}
String GSSAcceptorContext::processToken(const String &, Poco::Logger *)
{
throw Exception("ClickHouse was built without GSS-API/Kerberos support", ErrorCodes::FEATURE_IS_NOT_ENABLED_AT_BUILD_TIME);
}
#endif // USE_KRB5
}

66
src/Access/GSSAcceptor.h Normal file
View File

@ -0,0 +1,66 @@
#pragma once
#if !defined(ARCADIA_BUILD)
# include "config_core.h"
#endif
#include <Access/Credentials.h>
#include <common/types.h>
#include <memory>
#if USE_KRB5
# include <gssapi/gssapi.h>
# include <gssapi/gssapi_ext.h>
# define MAYBE_NORETURN
#else
# define MAYBE_NORETURN [[noreturn]]
#endif
namespace Poco { class Logger; }
namespace DB
{
class GSSAcceptorContext
: public Credentials
{
public:
struct Params
{
String mechanism = "1.2.840.113554.1.2.2"; // OID: krb5
String principal;
String realm;
};
explicit GSSAcceptorContext(const Params& params_);
virtual ~GSSAcceptorContext() override;
GSSAcceptorContext(const GSSAcceptorContext &) = delete;
GSSAcceptorContext(GSSAcceptorContext &&) = delete;
GSSAcceptorContext & operator= (const GSSAcceptorContext &) = delete;
GSSAcceptorContext & operator= (GSSAcceptorContext &&) = delete;
const String & getRealm() const;
bool isFailed() const;
MAYBE_NORETURN String processToken(const String & input_token, Poco::Logger * log);
private:
void reset();
void resetHandles() noexcept;
void initHandles();
private:
const Params params;
bool is_failed = false;
String realm;
#if USE_KRB5
gss_ctx_id_t context_handle = GSS_C_NO_CONTEXT;
gss_cred_id_t acceptor_credentials_handle = GSS_C_NO_CREDENTIAL;
#endif
};
}
#undef MAYBE_NORETURN

View File

@ -1,5 +1,6 @@
#include <Access/IAccessStorage.h>
#include <Access/User.h>
#include <Access/Credentials.h>
#include <Common/Exception.h>
#include <Common/quoteString.h>
#include <IO/WriteHelpers.h>
@ -417,53 +418,60 @@ void IAccessStorage::notify(const Notifications & notifications)
UUID IAccessStorage::login(
const String & user_name,
const String & password,
const Credentials & credentials,
const Poco::Net::IPAddress & address,
const ExternalAuthenticators & external_authenticators,
bool replace_exception_with_cannot_authenticate) const
{
try
{
return loginImpl(user_name, password, address, external_authenticators);
return loginImpl(credentials, address, external_authenticators);
}
catch (...)
{
if (!replace_exception_with_cannot_authenticate)
throw;
tryLogCurrentException(getLogger(), user_name + ": Authentication failed");
throwCannotAuthenticate(user_name);
tryLogCurrentException(getLogger(), credentials.getUserName() + ": Authentication failed");
throwCannotAuthenticate(credentials.getUserName());
}
}
UUID IAccessStorage::loginImpl(
const String & user_name,
const String & password,
const Credentials & credentials,
const Poco::Net::IPAddress & address,
const ExternalAuthenticators & external_authenticators) const
{
if (auto id = find<User>(user_name))
if (auto id = find<User>(credentials.getUserName()))
{
if (auto user = tryRead<User>(*id))
{
if (!isPasswordCorrectImpl(*user, password, external_authenticators))
throwInvalidPassword();
if (!isAddressAllowedImpl(*user, address))
throwAddressNotAllowed(address);
if (!areCredentialsValidImpl(*user, credentials, external_authenticators))
throwInvalidCredentials();
return *id;
}
}
throwNotFound(EntityType::USER, user_name);
throwNotFound(EntityType::USER, credentials.getUserName());
}
bool IAccessStorage::isPasswordCorrectImpl(const User & user, const String & password, const ExternalAuthenticators & external_authenticators) const
bool IAccessStorage::areCredentialsValidImpl(
const User & user,
const Credentials & credentials,
const ExternalAuthenticators & external_authenticators) const
{
return user.authentication.isCorrectPassword(user.getName(), password, external_authenticators);
if (!credentials.isReady())
return false;
if (credentials.getUserName() != user.getName())
return false;
return user.authentication.areCredentialsValid(credentials, external_authenticators);
}
@ -472,6 +480,7 @@ bool IAccessStorage::isAddressAllowedImpl(const User & user, const Poco::Net::IP
return user.allowed_client_hosts.contains(address);
}
UUID IAccessStorage::getIDOfLoggedUser(const String & user_name) const
{
return getIDOfLoggedUserImpl(user_name);
@ -578,9 +587,9 @@ void IAccessStorage::throwAddressNotAllowed(const Poco::Net::IPAddress & address
throw Exception("Connections from " + address.toString() + " are not allowed", ErrorCodes::IP_ADDRESS_NOT_ALLOWED);
}
void IAccessStorage::throwInvalidPassword()
void IAccessStorage::throwInvalidCredentials()
{
throw Exception("Invalid password", ErrorCodes::WRONG_PASSWORD);
throw Exception("Invalid credentials", ErrorCodes::WRONG_PASSWORD);
}
void IAccessStorage::throwCannotAuthenticate(const String & user_name)

View File

@ -16,6 +16,7 @@ namespace Poco::Net { class IPAddress; }
namespace DB
{
struct User;
class Credentials;
class ExternalAuthenticators;
/// Contains entities, i.e. instances of classes derived from IAccessEntity.
@ -142,11 +143,11 @@ public:
bool hasSubscription(EntityType type) const;
bool hasSubscription(const UUID & id) const;
/// Finds an user, check its password and returns the ID of the user.
/// Throws an exception if no such user or password is incorrect.
UUID login(const String & user_name, const String & password, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators, bool replace_exception_with_cannot_authenticate = true) const;
/// Finds a user, check the provided credentials and returns the ID of the user if they are valid.
/// Throws an exception if no such user or credentials are invalid.
UUID login(const Credentials & credentials, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators, bool replace_exception_with_cannot_authenticate = true) const;
/// Returns the ID of an user who has logged in (maybe on another node).
/// Returns the ID of a user who has logged in (maybe on another node).
/// The function assumes that the password has been already checked somehow, so we can skip checking it now.
UUID getIDOfLoggedUser(const String & user_name) const;
@ -164,8 +165,8 @@ protected:
virtual ext::scope_guard subscribeForChangesImpl(EntityType type, const OnChangedHandler & handler) const = 0;
virtual bool hasSubscriptionImpl(const UUID & id) const = 0;
virtual bool hasSubscriptionImpl(EntityType type) const = 0;
virtual UUID loginImpl(const String & user_name, const String & password, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators) const;
virtual bool isPasswordCorrectImpl(const User & user, const String & password, const ExternalAuthenticators & external_authenticators) const;
virtual UUID loginImpl(const Credentials & credentials, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators) const;
virtual bool areCredentialsValidImpl(const User & user, const Credentials & credentials, const ExternalAuthenticators & external_authenticators) const;
virtual bool isAddressAllowedImpl(const User & user, const Poco::Net::IPAddress & address) const;
virtual UUID getIDOfLoggedUserImpl(const String & user_name) const;
@ -183,7 +184,7 @@ protected:
[[noreturn]] void throwReadonlyCannotUpdate(EntityType type, const String & name) const;
[[noreturn]] void throwReadonlyCannotRemove(EntityType type, const String & name) const;
[[noreturn]] static void throwAddressNotAllowed(const Poco::Net::IPAddress & address);
[[noreturn]] static void throwInvalidPassword();
[[noreturn]] static void throwInvalidCredentials();
[[noreturn]] static void throwCannotAuthenticate(const String & user_name);
using Notification = std::tuple<OnChangedHandler, UUID, AccessEntityPtr>;

View File

@ -3,6 +3,7 @@
#include <Access/ExternalAuthenticators.h>
#include <Access/User.h>
#include <Access/Role.h>
#include <Access/Credentials.h>
#include <Access/LDAPClient.h>
#include <Common/Exception.h>
#include <common/logger_useful.h>
@ -35,7 +36,7 @@ LDAPAccessStorage::LDAPAccessStorage(const String & storage_name_, AccessControl
String LDAPAccessStorage::getLDAPServerName() const
{
return ldap_server;
return ldap_server_name;
}
@ -53,8 +54,8 @@ void LDAPAccessStorage::setConfiguration(AccessControlManager * access_control_m
if (!has_server)
throw Exception("Missing 'server' field for LDAP user directory", ErrorCodes::BAD_ARGUMENTS);
const auto ldap_server_cfg = config.getString(prefix_str + "server");
if (ldap_server_cfg.empty())
const auto ldap_server_name_cfg = config.getString(prefix_str + "server");
if (ldap_server_name_cfg.empty())
throw Exception("Empty 'server' field for LDAP user directory", ErrorCodes::BAD_ARGUMENTS);
std::set<String> common_roles_cfg;
@ -67,7 +68,7 @@ void LDAPAccessStorage::setConfiguration(AccessControlManager * access_control_m
common_roles_cfg.insert(role_names.begin(), role_names.end());
}
LDAPSearchParamsList role_search_params_cfg;
LDAPClient::SearchParamsList role_search_params_cfg;
if (has_role_mapping)
{
Poco::Util::AbstractConfiguration::Keys all_keys;
@ -89,17 +90,17 @@ void LDAPAccessStorage::setConfiguration(AccessControlManager * access_control_m
auto scope = config.getString(rm_prefix_str + "scope", "subtree");
boost::algorithm::to_lower(scope);
if (scope == "base") rm_params.scope = LDAPSearchParams::Scope::BASE;
else if (scope == "one_level") rm_params.scope = LDAPSearchParams::Scope::ONE_LEVEL;
else if (scope == "subtree") rm_params.scope = LDAPSearchParams::Scope::SUBTREE;
else if (scope == "children") rm_params.scope = LDAPSearchParams::Scope::CHILDREN;
if (scope == "base") rm_params.scope = LDAPClient::SearchParams::Scope::BASE;
else if (scope == "one_level") rm_params.scope = LDAPClient::SearchParams::Scope::ONE_LEVEL;
else if (scope == "subtree") rm_params.scope = LDAPClient::SearchParams::Scope::SUBTREE;
else if (scope == "children") rm_params.scope = LDAPClient::SearchParams::Scope::CHILDREN;
else
throw Exception("Invalid value of 'scope' field in '" + key + "' section of LDAP user directory, must be one of 'base', 'one_level', 'subtree', or 'children'", ErrorCodes::BAD_ARGUMENTS);
}
}
access_control_manager = access_control_manager_;
ldap_server = ldap_server_cfg;
ldap_server_name = ldap_server_name_cfg;
role_search_params.swap(role_search_params_cfg);
common_role_names.swap(common_roles_cfg);
@ -218,14 +219,14 @@ void LDAPAccessStorage::applyRoleChangeNoLock(bool grant, const UUID & role_id,
}
void LDAPAccessStorage::assignRolesNoLock(User & user, const LDAPSearchResultsList & external_roles) const
void LDAPAccessStorage::assignRolesNoLock(User & user, const LDAPClient::SearchResultsList & external_roles) const
{
const auto external_roles_hash = boost::hash<LDAPSearchResultsList>{}(external_roles);
const auto external_roles_hash = boost::hash<LDAPClient::SearchResultsList>{}(external_roles);
return assignRolesNoLock(user, external_roles, external_roles_hash);
}
void LDAPAccessStorage::assignRolesNoLock(User & user, const LDAPSearchResultsList & external_roles, const std::size_t external_roles_hash) const
void LDAPAccessStorage::assignRolesNoLock(User & user, const LDAPClient::SearchResultsList & external_roles, const std::size_t external_roles_hash) const
{
const auto & user_name = user.getName();
auto & granted_roles = user.granted_roles.roles;
@ -312,10 +313,10 @@ void LDAPAccessStorage::assignRolesNoLock(User & user, const LDAPSearchResultsLi
}
void LDAPAccessStorage::updateAssignedRolesNoLock(const UUID & id, const String & user_name, const LDAPSearchResultsList & external_roles) const
void LDAPAccessStorage::updateAssignedRolesNoLock(const UUID & id, const String & user_name, const LDAPClient::SearchResultsList & external_roles) const
{
// No need to include common_role_names in this hash each time, since they don't change.
const auto external_roles_hash = boost::hash<LDAPSearchResultsList>{}(external_roles);
const auto external_roles_hash = boost::hash<LDAPClient::SearchResultsList>{}(external_roles);
// Map and grant the roles from scratch only if the list of external role has changed.
const auto it = external_role_hashes.find(user_name);
@ -337,7 +338,7 @@ void LDAPAccessStorage::updateAssignedRolesNoLock(const UUID & id, const String
}
std::set<String> LDAPAccessStorage::mapExternalRolesNoLock(const LDAPSearchResultsList & external_roles) const
std::set<String> LDAPAccessStorage::mapExternalRolesNoLock(const LDAPClient::SearchResultsList & external_roles) const
{
std::set<String> role_names;
@ -365,10 +366,19 @@ std::set<String> LDAPAccessStorage::mapExternalRolesNoLock(const LDAPSearchResul
}
bool LDAPAccessStorage::isPasswordCorrectLDAPNoLock(const String & user_name, const String & password,
const ExternalAuthenticators & external_authenticators, LDAPSearchResultsList & search_results) const
bool LDAPAccessStorage::areLDAPCredentialsValidNoLock(const User & user, const Credentials & credentials,
const ExternalAuthenticators & external_authenticators, LDAPClient::SearchResultsList & search_results) const
{
return external_authenticators.checkLDAPCredentials(ldap_server, user_name, password, &role_search_params, &search_results);
if (!credentials.isReady())
return false;
if (credentials.getUserName() != user.getName())
return false;
if (const auto * basic_credentials = dynamic_cast<const BasicCredentials *>(&credentials))
return external_authenticators.checkLDAPCredentials(ldap_server_name, *basic_credentials, &role_search_params, &search_results);
return false;
}
@ -383,7 +393,7 @@ String LDAPAccessStorage::getStorageParamsJSON() const
std::scoped_lock lock(mutex);
Poco::JSON::Object params_json;
params_json.set("server", ldap_server);
params_json.set("server", ldap_server_name);
Poco::JSON::Array common_role_names_json;
for (const auto & role : common_role_names)
@ -405,10 +415,10 @@ String LDAPAccessStorage::getStorageParamsJSON() const
String scope;
switch (role_mapping.scope)
{
case LDAPSearchParams::Scope::BASE: scope = "base"; break;
case LDAPSearchParams::Scope::ONE_LEVEL: scope = "one_level"; break;
case LDAPSearchParams::Scope::SUBTREE: scope = "subtree"; break;
case LDAPSearchParams::Scope::CHILDREN: scope = "children"; break;
case LDAPClient::SearchParams::Scope::BASE: scope = "base"; break;
case LDAPClient::SearchParams::Scope::ONE_LEVEL: scope = "one_level"; break;
case LDAPClient::SearchParams::Scope::SUBTREE: scope = "subtree"; break;
case LDAPClient::SearchParams::Scope::CHILDREN: scope = "children"; break;
}
role_mapping_json.set("scope", scope);
@ -514,23 +524,23 @@ bool LDAPAccessStorage::hasSubscriptionImpl(EntityType type) const
return memory_storage.hasSubscription(type);
}
UUID LDAPAccessStorage::loginImpl(const String & user_name, const String & password, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators) const
UUID LDAPAccessStorage::loginImpl(const Credentials & credentials, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators) const
{
std::scoped_lock lock(mutex);
LDAPSearchResultsList external_roles;
auto id = memory_storage.find<User>(user_name);
LDAPClient::SearchResultsList external_roles;
auto id = memory_storage.find<User>(credentials.getUserName());
if (id)
{
auto user = memory_storage.read<User>(*id);
if (!isPasswordCorrectLDAPNoLock(user->getName(), password, external_authenticators, external_roles))
throwInvalidPassword();
if (!isAddressAllowedImpl(*user, address))
throwAddressNotAllowed(address);
if (!areLDAPCredentialsValidNoLock(*user, credentials, external_authenticators, external_roles))
throwInvalidCredentials();
// Just in case external_roles are changed. This will be no-op if they are not.
updateAssignedRolesNoLock(*id, user_name, external_roles);
updateAssignedRolesNoLock(*id, user->getName(), external_roles);
return *id;
}
@ -538,16 +548,16 @@ UUID LDAPAccessStorage::loginImpl(const String & user_name, const String & passw
{
// User does not exist, so we create one, and will add it if authentication is successful.
auto user = std::make_shared<User>();
user->setName(user_name);
user->authentication = Authentication(Authentication::Type::LDAP_SERVER);
user->authentication.setServerName(ldap_server);
if (!isPasswordCorrectLDAPNoLock(user->getName(), password, external_authenticators, external_roles))
throwInvalidPassword();
user->setName(credentials.getUserName());
user->authentication = Authentication(Authentication::Type::LDAP);
user->authentication.setLDAPServerName(ldap_server_name);
if (!isAddressAllowedImpl(*user, address))
throwAddressNotAllowed(address);
if (!areLDAPCredentialsValidNoLock(*user, credentials, external_authenticators, external_roles))
throwInvalidCredentials();
assignRolesNoLock(*user, external_roles);
return memory_storage.insert(user);
@ -567,10 +577,10 @@ UUID LDAPAccessStorage::getIDOfLoggedUserImpl(const String & user_name) const
// User does not exist, so we create one, and add it pretending that the authentication is successful.
auto user = std::make_shared<User>();
user->setName(user_name);
user->authentication = Authentication(Authentication::Type::LDAP_SERVER);
user->authentication.setServerName(ldap_server);
user->authentication = Authentication(Authentication::Type::LDAP);
user->authentication.setLDAPServerName(ldap_server_name);
LDAPSearchResultsList external_roles;
LDAPClient::SearchResultsList external_roles;
// TODO: mapped external roles are not available here. Without a password we can't authenticate and retrieve roles from LDAP server.

View File

@ -1,7 +1,9 @@
#pragma once
#include <Access/MemoryAccessStorage.h>
#include <Core/Types.h>
#include <Access/LDAPClient.h>
#include <Access/Credentials.h>
#include <common/types.h>
#include <ext/scope_guard.h>
#include <map>
#include <mutex>
@ -21,14 +23,10 @@ namespace Poco
namespace DB
{
class AccessControlManager;
struct LDAPSearchParams;
using LDAPSearchParamsList = std::vector<LDAPSearchParams>;
using LDAPSearchResults = std::set<String>;
using LDAPSearchResultsList = std::vector<LDAPSearchResults>;
/// Implementation of IAccessStorage which allows attaching users from a remote LDAP server.
/// Currently, any user name will be treated as a name of an existing remote user,
/// a user info entity will be created, with LDAP_SERVER authentication type.
/// a user info entity will be created, with LDAP authentication type.
class LDAPAccessStorage : public IAccessStorage
{
public:
@ -57,7 +55,7 @@ private: // IAccessStorage implementations.
virtual ext::scope_guard subscribeForChangesImpl(EntityType type, const OnChangedHandler & handler) const override;
virtual bool hasSubscriptionImpl(const UUID & id) const override;
virtual bool hasSubscriptionImpl(EntityType type) const override;
virtual UUID loginImpl(const String & user_name, const String & password, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators) const override;
virtual UUID loginImpl(const Credentials & credentials, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators) const override;
virtual UUID getIDOfLoggedUserImpl(const String & user_name) const override;
private:
@ -65,19 +63,19 @@ private:
void processRoleChange(const UUID & id, const AccessEntityPtr & entity);
void applyRoleChangeNoLock(bool grant, const UUID & role_id, const String & role_name);
void assignRolesNoLock(User & user, const LDAPSearchResultsList & external_roles) const;
void assignRolesNoLock(User & user, const LDAPSearchResultsList & external_roles, const std::size_t external_roles_hash) const;
void updateAssignedRolesNoLock(const UUID & id, const String & user_name, const LDAPSearchResultsList & external_roles) const;
std::set<String> mapExternalRolesNoLock(const LDAPSearchResultsList & external_roles) const;
bool isPasswordCorrectLDAPNoLock(const String & user_name, const String & password,
const ExternalAuthenticators & external_authenticators, LDAPSearchResultsList & search_results) const;
void assignRolesNoLock(User & user, const LDAPClient::SearchResultsList & external_roles) const;
void assignRolesNoLock(User & user, const LDAPClient::SearchResultsList & external_roles, const std::size_t external_roles_hash) const;
void updateAssignedRolesNoLock(const UUID & id, const String & user_name, const LDAPClient::SearchResultsList & external_roles) const;
std::set<String> mapExternalRolesNoLock(const LDAPClient::SearchResultsList & external_roles) const;
bool areLDAPCredentialsValidNoLock(const User & user, const Credentials & credentials,
const ExternalAuthenticators & external_authenticators, LDAPClient::SearchResultsList & search_results) const;
mutable std::recursive_mutex mutex;
AccessControlManager * access_control_manager = nullptr;
String ldap_server;
LDAPSearchParamsList role_search_params;
String ldap_server_name;
LDAPClient::SearchParamsList role_search_params;
std::set<String> common_role_names; // role name that should be granted to all users at all times
mutable std::map<String, std::size_t> external_role_hashes; // user name -> LDAPSearchResultsList hash (most recently retrieved and processed)
mutable std::map<String, std::size_t> external_role_hashes; // user name -> LDAPClient::SearchResultsList hash (most recently retrieved and processed)
mutable std::map<String, std::set<String>> users_per_roles; // role name -> user names (...it should be granted to; may but don't have to exist for common roles)
mutable std::map<String, std::set<String>> roles_per_users; // user name -> role names (...that should be granted to it; may but don't have to include common roles)
mutable std::map<UUID, String> granted_role_names; // (currently granted) role id -> its name

View File

@ -5,6 +5,7 @@
#include <Poco/Logger.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/container_hash/hash.hpp>
#include <mutex>
#include <utility>
@ -25,7 +26,25 @@ namespace ErrorCodes
extern const int LDAP_ERROR;
}
LDAPClient::LDAPClient(const LDAPServerParams & params_)
void LDAPClient::SearchParams::combineHash(std::size_t & seed) const
{
boost::hash_combine(seed, base_dn);
boost::hash_combine(seed, static_cast<int>(scope));
boost::hash_combine(seed, search_filter);
boost::hash_combine(seed, attribute);
boost::hash_combine(seed, prefix);
}
void LDAPClient::Params::combineCoreHash(std::size_t & seed) const
{
boost::hash_combine(seed, host);
boost::hash_combine(seed, port);
boost::hash_combine(seed, bind_dn);
boost::hash_combine(seed, user);
boost::hash_combine(seed, password);
}
LDAPClient::LDAPClient(const Params & params_)
: params(params_)
{
}
@ -143,7 +162,7 @@ void LDAPClient::openConnection()
LDAPURLDesc url;
std::memset(&url, 0, sizeof(url));
url.lud_scheme = const_cast<char *>(params.enable_tls == LDAPServerParams::TLSEnable::YES ? "ldaps" : "ldap");
url.lud_scheme = const_cast<char *>(params.enable_tls == LDAPClient::Params::TLSEnable::YES ? "ldaps" : "ldap");
url.lud_host = const_cast<char *>(params.host.c_str());
url.lud_port = params.port;
url.lud_scope = LDAP_SCOPE_DEFAULT;
@ -163,8 +182,8 @@ void LDAPClient::openConnection()
int value = 0;
switch (params.protocol_version)
{
case LDAPServerParams::ProtocolVersion::V2: value = LDAP_VERSION2; break;
case LDAPServerParams::ProtocolVersion::V3: value = LDAP_VERSION3; break;
case LDAPClient::Params::ProtocolVersion::V2: value = LDAP_VERSION2; break;
case LDAPClient::Params::ProtocolVersion::V3: value = LDAP_VERSION3; break;
}
diag(ldap_set_option(handle, LDAP_OPT_PROTOCOL_VERSION, &value));
}
@ -208,11 +227,11 @@ void LDAPClient::openConnection()
int value = 0;
switch (params.tls_minimum_protocol_version)
{
case LDAPServerParams::TLSProtocolVersion::SSL2: value = LDAP_OPT_X_TLS_PROTOCOL_SSL2; break;
case LDAPServerParams::TLSProtocolVersion::SSL3: value = LDAP_OPT_X_TLS_PROTOCOL_SSL3; break;
case LDAPServerParams::TLSProtocolVersion::TLS1_0: value = LDAP_OPT_X_TLS_PROTOCOL_TLS1_0; break;
case LDAPServerParams::TLSProtocolVersion::TLS1_1: value = LDAP_OPT_X_TLS_PROTOCOL_TLS1_1; break;
case LDAPServerParams::TLSProtocolVersion::TLS1_2: value = LDAP_OPT_X_TLS_PROTOCOL_TLS1_2; break;
case LDAPClient::Params::TLSProtocolVersion::SSL2: value = LDAP_OPT_X_TLS_PROTOCOL_SSL2; break;
case LDAPClient::Params::TLSProtocolVersion::SSL3: value = LDAP_OPT_X_TLS_PROTOCOL_SSL3; break;
case LDAPClient::Params::TLSProtocolVersion::TLS1_0: value = LDAP_OPT_X_TLS_PROTOCOL_TLS1_0; break;
case LDAPClient::Params::TLSProtocolVersion::TLS1_1: value = LDAP_OPT_X_TLS_PROTOCOL_TLS1_1; break;
case LDAPClient::Params::TLSProtocolVersion::TLS1_2: value = LDAP_OPT_X_TLS_PROTOCOL_TLS1_2; break;
}
diag(ldap_set_option(handle, LDAP_OPT_X_TLS_PROTOCOL_MIN, &value));
}
@ -223,10 +242,10 @@ void LDAPClient::openConnection()
int value = 0;
switch (params.tls_require_cert)
{
case LDAPServerParams::TLSRequireCert::NEVER: value = LDAP_OPT_X_TLS_NEVER; break;
case LDAPServerParams::TLSRequireCert::ALLOW: value = LDAP_OPT_X_TLS_ALLOW; break;
case LDAPServerParams::TLSRequireCert::TRY: value = LDAP_OPT_X_TLS_TRY; break;
case LDAPServerParams::TLSRequireCert::DEMAND: value = LDAP_OPT_X_TLS_DEMAND; break;
case LDAPClient::Params::TLSRequireCert::NEVER: value = LDAP_OPT_X_TLS_NEVER; break;
case LDAPClient::Params::TLSRequireCert::ALLOW: value = LDAP_OPT_X_TLS_ALLOW; break;
case LDAPClient::Params::TLSRequireCert::TRY: value = LDAP_OPT_X_TLS_TRY; break;
case LDAPClient::Params::TLSRequireCert::DEMAND: value = LDAP_OPT_X_TLS_DEMAND; break;
}
diag(ldap_set_option(handle, LDAP_OPT_X_TLS_REQUIRE_CERT, &value));
}
@ -264,12 +283,12 @@ void LDAPClient::openConnection()
}
#endif
if (params.enable_tls == LDAPServerParams::TLSEnable::YES_STARTTLS)
if (params.enable_tls == LDAPClient::Params::TLSEnable::YES_STARTTLS)
diag(ldap_start_tls_s(handle, nullptr, nullptr));
switch (params.sasl_mechanism)
{
case LDAPServerParams::SASLMechanism::SIMPLE:
case LDAPClient::Params::SASLMechanism::SIMPLE:
{
const auto escaped_user_name = escapeForLDAP(params.user);
const auto bind_dn = replacePlaceholders(params.bind_dn, { {"{user_name}", escaped_user_name} });
@ -299,19 +318,19 @@ void LDAPClient::closeConnection() noexcept
handle = nullptr;
}
LDAPSearchResults LDAPClient::search(const LDAPSearchParams & search_params)
LDAPClient::SearchResults LDAPClient::search(const SearchParams & search_params)
{
std::scoped_lock lock(ldap_global_mutex);
LDAPSearchResults result;
SearchResults result;
int scope = 0;
switch (search_params.scope)
{
case LDAPSearchParams::Scope::BASE: scope = LDAP_SCOPE_BASE; break;
case LDAPSearchParams::Scope::ONE_LEVEL: scope = LDAP_SCOPE_ONELEVEL; break;
case LDAPSearchParams::Scope::SUBTREE: scope = LDAP_SCOPE_SUBTREE; break;
case LDAPSearchParams::Scope::CHILDREN: scope = LDAP_SCOPE_CHILDREN; break;
case SearchParams::Scope::BASE: scope = LDAP_SCOPE_BASE; break;
case SearchParams::Scope::ONE_LEVEL: scope = LDAP_SCOPE_ONELEVEL; break;
case SearchParams::Scope::SUBTREE: scope = LDAP_SCOPE_SUBTREE; break;
case SearchParams::Scope::CHILDREN: scope = LDAP_SCOPE_CHILDREN; break;
}
const auto escaped_user_name = escapeForLDAP(params.user);
@ -452,7 +471,7 @@ LDAPSearchResults LDAPClient::search(const LDAPSearchParams & search_params)
return result;
}
bool LDAPSimpleAuthClient::authenticate(const LDAPSearchParamsList * search_params, LDAPSearchResultsList * search_results)
bool LDAPSimpleAuthClient::authenticate(const SearchParamsList * search_params, SearchResultsList * search_results)
{
if (params.user.empty())
throw Exception("LDAP authentication of a user with empty name is not allowed", ErrorCodes::BAD_ARGUMENTS);
@ -508,12 +527,12 @@ void LDAPClient::closeConnection() noexcept
{
}
LDAPSearchResults LDAPClient::search(const LDAPSearchParams &)
LDAPClient::SearchResults LDAPClient::search(const SearchParams &)
{
throw Exception("ClickHouse was built without LDAP support", ErrorCodes::FEATURE_IS_NOT_ENABLED_AT_BUILD_TIME);
}
bool LDAPSimpleAuthClient::authenticate(const LDAPSearchParamsList *, LDAPSearchResultsList *)
bool LDAPSimpleAuthClient::authenticate(const SearchParamsList *, SearchResultsList *)
{
throw Exception("ClickHouse was built without LDAP support", ErrorCodes::FEATURE_IS_NOT_ENABLED_AT_BUILD_TIME);
}

View File

@ -4,7 +4,6 @@
# include "config_core.h"
#endif
#include <Access/LDAPParams.h>
#include <common/types.h>
#if USE_LDAP
@ -14,6 +13,10 @@
# define MAYBE_NORETURN [[noreturn]]
#endif
#include <chrono>
#include <set>
#include <vector>
namespace DB
{
@ -21,7 +24,98 @@ namespace DB
class LDAPClient
{
public:
explicit LDAPClient(const LDAPServerParams & params_);
struct SearchParams
{
enum class Scope
{
BASE,
ONE_LEVEL,
SUBTREE,
CHILDREN
};
String base_dn;
Scope scope = Scope::SUBTREE;
String search_filter;
String attribute = "cn";
String prefix;
void combineHash(std::size_t & seed) const;
};
using SearchParamsList = std::vector<SearchParams>;
using SearchResults = std::set<String>;
using SearchResultsList = std::vector<SearchResults>;
struct Params
{
enum class ProtocolVersion
{
V2,
V3
};
enum class TLSEnable
{
NO,
YES_STARTTLS,
YES
};
enum class TLSProtocolVersion
{
SSL2,
SSL3,
TLS1_0,
TLS1_1,
TLS1_2
};
enum class TLSRequireCert
{
NEVER,
ALLOW,
TRY,
DEMAND
};
enum class SASLMechanism
{
UNKNOWN,
SIMPLE
};
ProtocolVersion protocol_version = ProtocolVersion::V3;
String host;
std::uint16_t port = 636;
TLSEnable enable_tls = TLSEnable::YES;
TLSProtocolVersion tls_minimum_protocol_version = TLSProtocolVersion::TLS1_2;
TLSRequireCert tls_require_cert = TLSRequireCert::DEMAND;
String tls_cert_file;
String tls_key_file;
String tls_ca_cert_file;
String tls_ca_cert_dir;
String tls_cipher_suite;
SASLMechanism sasl_mechanism = SASLMechanism::SIMPLE;
String bind_dn;
String user;
String password;
std::chrono::seconds verification_cooldown{0};
std::chrono::seconds operation_timeout{40};
std::chrono::seconds network_timeout{30};
std::chrono::seconds search_timeout{20};
std::uint32_t search_limit = 100;
void combineCoreHash(std::size_t & seed) const;
};
explicit LDAPClient(const Params & params_);
~LDAPClient();
LDAPClient(const LDAPClient &) = delete;
@ -33,10 +127,10 @@ protected:
MAYBE_NORETURN void diag(const int rc, String text = "");
MAYBE_NORETURN void openConnection();
void closeConnection() noexcept;
LDAPSearchResults search(const LDAPSearchParams & search_params);
SearchResults search(const SearchParams & search_params);
protected:
const LDAPServerParams params;
const Params params;
#if USE_LDAP
LDAP * handle = nullptr;
#endif
@ -47,7 +141,7 @@ class LDAPSimpleAuthClient
{
public:
using LDAPClient::LDAPClient;
bool authenticate(const LDAPSearchParamsList * search_params, LDAPSearchResultsList * search_results);
bool authenticate(const SearchParamsList * search_params, SearchResultsList * search_results);
};
}

View File

@ -1,120 +0,0 @@
#pragma once
#include <common/types.h>
#include <boost/container_hash/hash.hpp>
#include <chrono>
#include <set>
#include <vector>
namespace DB
{
struct LDAPSearchParams
{
enum class Scope
{
BASE,
ONE_LEVEL,
SUBTREE,
CHILDREN
};
String base_dn;
Scope scope = Scope::SUBTREE;
String search_filter;
String attribute = "cn";
String prefix;
void combineHash(std::size_t & seed) const
{
boost::hash_combine(seed, base_dn);
boost::hash_combine(seed, static_cast<int>(scope));
boost::hash_combine(seed, search_filter);
boost::hash_combine(seed, attribute);
boost::hash_combine(seed, prefix);
}
};
using LDAPSearchParamsList = std::vector<LDAPSearchParams>;
using LDAPSearchResults = std::set<String>;
using LDAPSearchResultsList = std::vector<LDAPSearchResults>;
struct LDAPServerParams
{
enum class ProtocolVersion
{
V2,
V3
};
enum class TLSEnable
{
NO,
YES_STARTTLS,
YES
};
enum class TLSProtocolVersion
{
SSL2,
SSL3,
TLS1_0,
TLS1_1,
TLS1_2
};
enum class TLSRequireCert
{
NEVER,
ALLOW,
TRY,
DEMAND
};
enum class SASLMechanism
{
UNKNOWN,
SIMPLE
};
ProtocolVersion protocol_version = ProtocolVersion::V3;
String host;
std::uint16_t port = 636;
TLSEnable enable_tls = TLSEnable::YES;
TLSProtocolVersion tls_minimum_protocol_version = TLSProtocolVersion::TLS1_2;
TLSRequireCert tls_require_cert = TLSRequireCert::DEMAND;
String tls_cert_file;
String tls_key_file;
String tls_ca_cert_file;
String tls_ca_cert_dir;
String tls_cipher_suite;
SASLMechanism sasl_mechanism = SASLMechanism::SIMPLE;
String bind_dn;
String user;
String password;
std::chrono::seconds verification_cooldown{0};
std::chrono::seconds operation_timeout{40};
std::chrono::seconds network_timeout{30};
std::chrono::seconds search_timeout{20};
std::uint32_t search_limit = 100;
void combineCoreHash(std::size_t & seed) const
{
boost::hash_combine(seed, host);
boost::hash_combine(seed, port);
boost::hash_combine(seed, bind_dn);
boost::hash_combine(seed, user);
boost::hash_combine(seed, password);
}
};
}

View File

@ -1,4 +1,5 @@
#include <Access/MultipleAccessStorage.h>
#include <Access/Credentials.h>
#include <Common/Exception.h>
#include <ext/range.h>
#include <boost/range/adaptor/map.hpp>
@ -382,6 +383,7 @@ void MultipleAccessStorage::updateSubscriptionsToNestedStorages(std::unique_lock
/// Lock the mutex again to store added subscriptions to the nested storages.
lock.lock();
for (auto type : ext::range(EntityType::MAX))
{
if (!added_subscriptions[static_cast<size_t>(type)].empty())
@ -399,25 +401,24 @@ void MultipleAccessStorage::updateSubscriptionsToNestedStorages(std::unique_lock
}
lock.unlock();
added_subscriptions->clear();
}
UUID MultipleAccessStorage::loginImpl(const String & user_name, const String & password, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators) const
UUID MultipleAccessStorage::loginImpl(const Credentials & credentials, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators) const
{
auto storages = getStoragesInternal();
for (const auto & storage : *storages)
{
try
{
auto id = storage->login(user_name, password, address, external_authenticators, /* replace_exception_with_cannot_authenticate = */ false);
auto id = storage->login(credentials, address, external_authenticators, /* replace_exception_with_cannot_authenticate = */ false);
std::lock_guard lock{mutex};
ids_cache.set(id, storage);
return id;
}
catch (...)
{
if (!storage->find(EntityType::USER, user_name))
if (!storage->find(EntityType::USER, credentials.getUserName()))
{
/// The authentication failed because there no users with such name in the `storage`
/// thus we can try to search in other nested storages.
@ -426,7 +427,7 @@ UUID MultipleAccessStorage::loginImpl(const String & user_name, const String & p
throw;
}
}
throwNotFound(EntityType::USER, user_name);
throwNotFound(EntityType::USER, credentials.getUserName());
}

View File

@ -48,7 +48,7 @@ protected:
ext::scope_guard subscribeForChangesImpl(EntityType type, const OnChangedHandler & handler) const override;
bool hasSubscriptionImpl(const UUID & id) const override;
bool hasSubscriptionImpl(EntityType type) const override;
UUID loginImpl(const String & user_name, const String & password, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators) const override;
UUID loginImpl(const Credentials & credentials, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators) const override;
UUID getIDOfLoggedUserImpl(const String & user_name) const override;
private:

View File

@ -62,14 +62,15 @@ namespace
bool has_password_sha256_hex = config.has(user_config + ".password_sha256_hex");
bool has_password_double_sha1_hex = config.has(user_config + ".password_double_sha1_hex");
bool has_ldap = config.has(user_config + ".ldap");
bool has_kerberos = config.has(user_config + ".kerberos");
size_t num_password_fields = has_no_password + has_password_plaintext + has_password_sha256_hex + has_password_double_sha1_hex + has_ldap;
size_t num_password_fields = has_no_password + has_password_plaintext + has_password_sha256_hex + has_password_double_sha1_hex + has_ldap + has_kerberos;
if (num_password_fields > 1)
throw Exception("More than one field of 'password', 'password_sha256_hex', 'password_double_sha1_hex', 'no_password', 'ldap' are used to specify password for user " + user_name + ". Must be only one of them.",
throw Exception("More than one field of 'password', 'password_sha256_hex', 'password_double_sha1_hex', 'no_password', 'ldap', 'kerberos' are used to specify password for user " + user_name + ". Must be only one of them.",
ErrorCodes::BAD_ARGUMENTS);
if (num_password_fields < 1)
throw Exception("Either 'password' or 'password_sha256_hex' or 'password_double_sha1_hex' or 'no_password' or 'ldap' must be specified for user " + user_name + ".", ErrorCodes::BAD_ARGUMENTS);
throw Exception("Either 'password' or 'password_sha256_hex' or 'password_double_sha1_hex' or 'no_password' or 'ldap' or 'kerberos' must be specified for user " + user_name + ".", ErrorCodes::BAD_ARGUMENTS);
if (has_password_plaintext)
{
@ -96,8 +97,15 @@ namespace
if (ldap_server_name.empty())
throw Exception("LDAP server name cannot be empty for user " + user_name + ".", ErrorCodes::BAD_ARGUMENTS);
user->authentication = Authentication{Authentication::LDAP_SERVER};
user->authentication.setServerName(ldap_server_name);
user->authentication = Authentication{Authentication::LDAP};
user->authentication.setLDAPServerName(ldap_server_name);
}
else if (has_kerberos)
{
const auto realm = config.getString(user_config + ".kerberos.realm", "");
user->authentication = Authentication{Authentication::KERBEROS};
user->authentication.setKerberosRealm(realm);
}
const auto profile_name_config = user_config + ".profile";

View File

@ -15,6 +15,7 @@ SRCS(
AllowedClientHosts.cpp
Authentication.cpp
ContextAccess.cpp
Credentials.cpp
DiskAccessStorage.cpp
EnabledQuota.cpp
EnabledRoles.cpp
@ -22,6 +23,7 @@ SRCS(
EnabledRowPolicies.cpp
EnabledSettings.cpp
ExternalAuthenticators.cpp
GSSAcceptor.cpp
GrantedRoles.cpp
IAccessEntity.cpp
IAccessStorage.cpp

View File

@ -316,6 +316,7 @@ if (USE_CYRUS_SASL)
endif()
if (USE_KRB5)
dbms_target_include_directories(SYSTEM BEFORE PRIVATE ${KRB5_INCLUDE_DIR})
dbms_target_link_libraries(PRIVATE ${KRB5_LIBRARY})
endif()

View File

@ -542,6 +542,7 @@
M(573, EPOLL_ERROR) \
M(574, DISTRIBUTED_TOO_MANY_PENDING_BYTES) \
M(575, UNKNOWN_SNAPSHOT) \
M(576, KERBEROS_ERROR) \
\
M(999, KEEPER_EXCEPTION) \
M(1000, POCO_EXCEPTION) \

View File

@ -14,3 +14,4 @@
#cmakedefine01 USE_ROCKSDB
#cmakedefine01 USE_LIBPQXX
#cmakedefine01 USE_NURAFT
#cmakedefine01 USE_KRB5

View File

@ -36,8 +36,11 @@
#include <Access/EnabledRowPolicies.h>
#include <Access/QuotaUsage.h>
#include <Access/User.h>
#include <Access/Credentials.h>
#include <Access/SettingsProfile.h>
#include <Access/SettingsConstraints.h>
#include <Access/ExternalAuthenticators.h>
#include <Access/GSSAcceptor.h>
#include <Interpreters/ExpressionJIT.h>
#include <Dictionaries/Embedded/GeoDictionariesLoader.h>
#include <Interpreters/EmbeddedDictionaries.h>
@ -669,6 +672,12 @@ void Context::setExternalAuthenticatorsConfig(const Poco::Util::AbstractConfigur
shared->access_control_manager.setExternalAuthenticatorsConfig(config);
}
std::unique_ptr<GSSAcceptorContext> Context::makeGSSAcceptorContext() const
{
auto lock = getLock();
return std::make_unique<GSSAcceptorContext>(shared->access_control_manager.getExternalAuthenticators().getKerberosParams());
}
void Context::setUsersConfig(const ConfigurationPtr & config)
{
auto lock = getLock();
@ -683,29 +692,22 @@ ConfigurationPtr Context::getUsersConfig()
}
void Context::setUserImpl(const String & name, const std::optional<String> & password, const Poco::Net::SocketAddress & address)
void Context::setUser(const Credentials & credentials, const Poco::Net::SocketAddress & address)
{
auto lock = getLock();
client_info.current_user = name;
client_info.current_user = credentials.getUserName();
client_info.current_address = address;
#if defined(ARCADIA_BUILD)
/// This is harmful field that is used only in foreign "Arcadia" build.
client_info.current_password = password.value_or("");
client_info.current_password.clear();
if (const auto * basic_credentials = dynamic_cast<const BasicCredentials *>(&credentials))
client_info.current_password = basic_credentials->getPassword();
#endif
/// Find a user with such name and check the password.
UUID new_user_id;
if (password)
new_user_id = getAccessControlManager().login(name, *password, address.host());
else
{
/// Access w/o password is done under interserver-secret (remote_servers.secret)
/// So it is okay not to check client's host in this case (since there is trust).
new_user_id = getAccessControlManager().getIDOfLoggedUser(name);
}
/// Find a user with such name and check the credentials.
auto new_user_id = getAccessControlManager().login(credentials, address.host());
auto new_access = getAccessControlManager().getContextAccess(
new_user_id, /* current_roles = */ {}, /* use_default_roles = */ true,
settings, current_database, client_info);
@ -720,12 +722,12 @@ void Context::setUserImpl(const String & name, const std::optional<String> & pas
void Context::setUser(const String & name, const String & password, const Poco::Net::SocketAddress & address)
{
setUserImpl(name, password, address);
setUser(BasicCredentials(name, password), address);
}
void Context::setUserWithoutCheckingPassword(const String & name, const Poco::Net::SocketAddress & address)
{
setUserImpl(name, {} /* no password */, address);
setUser(AlwaysAllowCredentials(name), address);
}
std::shared_ptr<const User> Context::getUser() const

View File

@ -93,6 +93,8 @@ using ActionLocksManagerPtr = std::shared_ptr<ActionLocksManager>;
class ShellCommand;
class ICompressionCodec;
class AccessControlManager;
class Credentials;
class GSSAcceptorContext;
class SettingsConstraints;
class RemoteHostFilter;
struct StorageID;
@ -322,9 +324,12 @@ public:
AccessControlManager & getAccessControlManager();
const AccessControlManager & getAccessControlManager() const;
/// Sets external authenticators config (LDAP).
/// Sets external authenticators config (LDAP, Kerberos).
void setExternalAuthenticatorsConfig(const Poco::Util::AbstractConfiguration & config);
/// Creates GSSAcceptorContext instance based on external authenticator params.
std::unique_ptr<GSSAcceptorContext> makeGSSAcceptorContext() const;
/** Take the list of users, quotas and configuration profiles from this config.
* The list of users is completely replaced.
* The accumulated quota values are not reset if the quota is not deleted.
@ -332,11 +337,12 @@ public:
void setUsersConfig(const ConfigurationPtr & config);
ConfigurationPtr getUsersConfig();
/// Sets the current user, checks the password and that the specified host is allowed.
/// Must be called before getClientInfo.
/// Sets the current user, checks the credentials and that the specified host is allowed.
/// Must be called before getClientInfo() can be called.
void setUser(const Credentials & credentials, const Poco::Net::SocketAddress & address);
void setUser(const String & name, const String & password, const Poco::Net::SocketAddress & address);
/// Sets the current user, *do not checks the password and that the specified host is allowed*.
/// Sets the current user, *does not check the password/credentials and that the specified host is allowed*.
/// Must be called before getClientInfo.
///
/// (Used only internally in cluster, if the secret matches)
@ -782,9 +788,6 @@ private:
StoragePolicySelectorPtr getStoragePolicySelector(std::lock_guard<std::mutex> & lock) const;
DiskSelectorPtr getDiskSelector(std::lock_guard<std::mutex> & /* lock */) const;
/// If the password is not set, the password will not be checked
void setUserImpl(const String & name, const std::optional<String> & password, const Poco::Net::SocketAddress & address);
};

View File

@ -34,9 +34,14 @@ namespace
}
String authentication_type_name = Authentication::TypeInfo::get(authentication_type).name;
String by_keyword = "BY";
std::optional<String> by_value;
if (show_password || authentication_type == Authentication::LDAP_SERVER)
if (
show_password ||
authentication_type == Authentication::LDAP ||
authentication_type == Authentication::KERBEROS
)
{
switch (authentication_type)
{
@ -57,9 +62,18 @@ namespace
by_value = authentication.getPasswordHashHex();
break;
}
case Authentication::LDAP_SERVER:
case Authentication::LDAP:
{
by_value = authentication.getServerName();
by_keyword = "SERVER";
by_value = authentication.getLDAPServerName();
break;
}
case Authentication::KERBEROS:
{
by_keyword = "REALM";
const auto & realm = authentication.getKerberosRealm();
if (!realm.empty())
by_value = realm;
break;
}
@ -71,9 +85,12 @@ namespace
settings.ostr << (settings.hilite ? IAST::hilite_keyword : "") << " IDENTIFIED WITH " << authentication_type_name
<< (settings.hilite ? IAST::hilite_none : "");
if (by_value)
settings.ostr << (settings.hilite ? IAST::hilite_keyword : "") << " BY " << (settings.hilite ? IAST::hilite_none : "")
<< quoteString(*by_value);
{
settings.ostr << (settings.hilite ? IAST::hilite_keyword : "") << " " << by_keyword << " "
<< (settings.hilite ? IAST::hilite_none : "") << quoteString(*by_value);
}
}

View File

@ -13,14 +13,14 @@ class ASTRolesOrUsersSet;
class ASTSettingsProfileElements;
/** CREATE USER [IF NOT EXISTS | OR REPLACE] name
* [NOT IDENTIFIED | IDENTIFIED [WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash|ldap_server}] BY {'password'|'hash'|'server_name'}]
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']}]
* [HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
* [DEFAULT ROLE role [,...]]
* [SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [READONLY|WRITABLE] | PROFILE 'profile_name'] [,...]
*
* ALTER USER [IF EXISTS] name
* [RENAME TO new_name]
* [NOT IDENTIFIED | IDENTIFIED [WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash|ldap_server}] BY {'password'|'hash'|'server_name'}]
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']}]
* [[ADD|DROP] HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
* [DEFAULT ROLE role [,...] | ALL | ALL EXCEPT role [,...] ]
* [SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [READONLY|WRITABLE] | PROFILE 'profile_name'] [,...]

View File

@ -49,7 +49,8 @@ namespace
std::optional<Authentication::Type> type;
bool expect_password = false;
bool expect_hash = false;
bool expect_server_name = false;
bool expect_ldap_server_name = false;
bool expect_kerberos_realm = false;
if (ParserKeyword{"WITH"}.ignore(pos, expected))
{
@ -59,8 +60,10 @@ namespace
{
type = check_type;
if (check_type == Authentication::LDAP_SERVER)
expect_server_name = true;
if (check_type == Authentication::LDAP)
expect_ldap_server_name = true;
else if (check_type == Authentication::KERBEROS)
expect_kerberos_realm = true;
else if (check_type != Authentication::NO_PASSWORD)
expect_password = true;
@ -92,7 +95,7 @@ namespace
}
String value;
if (expect_password || expect_hash || expect_server_name)
if (expect_password || expect_hash)
{
ASTPtr ast;
if (!ParserKeyword{"BY"}.ignore(pos, expected) || !ParserStringLiteral{}.parse(pos, ast, expected))
@ -100,14 +103,35 @@ namespace
value = ast->as<const ASTLiteral &>().value.safeGet<String>();
}
else if (expect_ldap_server_name)
{
ASTPtr ast;
if (!ParserKeyword{"SERVER"}.ignore(pos, expected) || !ParserStringLiteral{}.parse(pos, ast, expected))
return false;
value = ast->as<const ASTLiteral &>().value.safeGet<String>();
}
else if (expect_kerberos_realm)
{
if (ParserKeyword{"REALM"}.ignore(pos, expected))
{
ASTPtr ast;
if (!ParserStringLiteral{}.parse(pos, ast, expected))
return false;
value = ast->as<const ASTLiteral &>().value.safeGet<String>();
}
}
authentication = Authentication{*type};
if (expect_password)
authentication.setPassword(value);
else if (expect_hash)
authentication.setPasswordHashHex(value);
else if (expect_server_name)
authentication.setServerName(value);
else if (expect_ldap_server_name)
authentication.setLDAPServerName(value);
else if (expect_kerberos_realm)
authentication.setKerberosRealm(value);
return true;
});

View File

@ -7,13 +7,13 @@ namespace DB
{
/** Parses queries like
* CREATE USER [IF NOT EXISTS | OR REPLACE] name
* [NOT IDENTIFIED | IDENTIFIED [WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash|ldap_server}] BY {'password'|'hash'|'server_name'}]
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']}]
* [HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
* [SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [READONLY|WRITABLE] | PROFILE 'profile_name'] [,...]
*
* ALTER USER [IF EXISTS] name
* [RENAME TO new_name]
* [NOT IDENTIFIED | IDENTIFIED [WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash|ldap_server}] BY {'password'|'hash'|'server_name'}]
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']}]
* [[ADD|DROP] HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
* [SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [READONLY|WRITABLE] | PROFILE 'profile_name'] [,...]
*/

View File

@ -1,5 +1,8 @@
#include <Server/HTTPHandler.h>
#include <Access/Authentication.h>
#include <Access/Credentials.h>
#include <Access/ExternalAuthenticators.h>
#include <Compression/CompressedReadBuffer.h>
#include <Compression/CompressedWriteBuffer.h>
#include <Core/ExternalTable.h>
@ -34,13 +37,19 @@
# include <Common/config.h>
#endif
#include <Poco/Base64Decoder.h>
#include <Poco/Base64Encoder.h>
#include <Poco/File.h>
#include <Poco/Net/HTTPBasicCredentials.h>
#include <Poco/Net/HTTPStream.h>
#include <Poco/Net/NetException.h>
#include <Poco/MemoryStream.h>
#include <Poco/StreamCopier.h>
#include <Poco/String.h>
#include <chrono>
#include <iomanip>
#include <sstream>
namespace DB
@ -93,12 +102,32 @@ namespace ErrorCodes
extern const int UNKNOWN_USER;
extern const int WRONG_PASSWORD;
extern const int REQUIRED_PASSWORD;
extern const int AUTHENTICATION_FAILED;
extern const int BAD_REQUEST_PARAMETER;
extern const int INVALID_SESSION_TIMEOUT;
extern const int HTTP_LENGTH_REQUIRED;
}
static String base64Decode(const String & encoded)
{
String decoded;
Poco::MemoryInputStream istr(encoded.data(), encoded.size());
Poco::Base64Decoder decoder(istr);
Poco::StreamCopier::copyToString(decoder, decoded);
return decoded;
}
static String base64Encode(const String & decoded)
{
std::ostringstream ostr; // STYLE_CHECK_ALLOW_STD_STRING_STREAM
ostr.exceptions(std::ios::failbit);
Poco::Base64Encoder encoder(ostr);
encoder.rdbuf()->setLineLength(0);
encoder << decoded;
encoder.close();
return ostr.str();
}
static Poco::Net::HTTPResponse::HTTPStatus exceptionCodeToHTTPStatus(int exception_code)
{
@ -108,6 +137,12 @@ static Poco::Net::HTTPResponse::HTTPStatus exceptionCodeToHTTPStatus(int excepti
{
return HTTPResponse::HTTP_UNAUTHORIZED;
}
else if (exception_code == ErrorCodes::UNKNOWN_USER ||
exception_code == ErrorCodes::WRONG_PASSWORD ||
exception_code == ErrorCodes::AUTHENTICATION_FAILED)
{
return HTTPResponse::HTTP_FORBIDDEN;
}
else if (exception_code == ErrorCodes::CANNOT_PARSE_TEXT ||
exception_code == ErrorCodes::CANNOT_PARSE_ESCAPE_SEQUENCE ||
exception_code == ErrorCodes::CANNOT_PARSE_QUOTED_STRING ||
@ -233,15 +268,21 @@ HTTPHandler::HTTPHandler(IServer & server_, const std::string & name)
}
void HTTPHandler::processQuery(
/// We need d-tor to be present in this translation unit to make it play well with some
/// forward decls in the header. Other than that, the default d-tor would be OK.
HTTPHandler::~HTTPHandler()
{
(void)this;
}
bool HTTPHandler::authenticateUser(
Context & context,
HTTPServerRequest & request,
HTMLForm & params,
HTTPServerResponse & response,
Output & used_output,
std::optional<CurrentThread::QueryScope> & query_scope)
HTTPServerResponse & response)
{
LOG_TRACE(log, "Request URI: {}", request.getURI());
using namespace Poco::Net;
/// The user and password can be passed by headers (similar to X-Auth-*),
/// which is used by load balancers to pass authentication information.
@ -249,16 +290,39 @@ void HTTPHandler::processQuery(
std::string password = request.get("X-ClickHouse-Key", "");
std::string quota_key = request.get("X-ClickHouse-Quota", "");
std::string spnego_challenge;
if (user.empty() && password.empty() && quota_key.empty())
{
/// User name and password can be passed using query parameters
/// or using HTTP Basic auth (both methods are insecure).
if (request.hasCredentials())
{
Poco::Net::HTTPBasicCredentials credentials(request);
/// It is prohibited to mix different authorization schemes.
if (params.has("user") || params.has("password"))
throw Exception("Invalid authentication: it is not allowed to use Authorization HTTP header and authentication via parameters simultaneously", ErrorCodes::AUTHENTICATION_FAILED);
user = credentials.getUsername();
password = credentials.getPassword();
std::string scheme;
std::string auth_info;
request.getCredentials(scheme, auth_info);
if (Poco::icompare(scheme, "Basic") == 0)
{
HTTPBasicCredentials credentials(auth_info);
user = credentials.getUsername();
password = credentials.getPassword();
}
else if (Poco::icompare(scheme, "Negotiate") == 0)
{
spnego_challenge = auth_info;
if (spnego_challenge.empty())
throw Exception("Invalid authentication: SPNEGO challenge is empty", ErrorCodes::AUTHENTICATION_FAILED);
}
else
{
throw Exception("Invalid authentication: '" + scheme + "' HTTP Authorization scheme is not supported", ErrorCodes::AUTHENTICATION_FAILED);
}
}
else
{
@ -271,12 +335,47 @@ void HTTPHandler::processQuery(
else
{
/// It is prohibited to mix different authorization schemes.
if (request.hasCredentials()
|| params.has("user")
|| params.has("password")
|| params.has("quota_key"))
if (request.hasCredentials() || params.has("user") || params.has("password") || params.has("quota_key"))
throw Exception("Invalid authentication: it is not allowed to use X-ClickHouse HTTP headers and other authentication methods simultaneously", ErrorCodes::AUTHENTICATION_FAILED);
}
if (spnego_challenge.empty()) // I.e., now using user name and password strings ("Basic").
{
if (!request_credentials)
request_credentials = std::make_unique<BasicCredentials>();
auto * basic_credentials = dynamic_cast<BasicCredentials *>(request_credentials.get());
if (!basic_credentials)
throw Exception("Invalid authentication: unexpected 'Basic' HTTP Authorization scheme", ErrorCodes::AUTHENTICATION_FAILED);
basic_credentials->setUserName(user);
basic_credentials->setPassword(password);
}
else
{
if (!request_credentials)
request_credentials = request_context->makeGSSAcceptorContext();
auto * gss_acceptor_context = dynamic_cast<GSSAcceptorContext *>(request_credentials.get());
if (!gss_acceptor_context)
throw Exception("Invalid authentication: unexpected 'Negotiate' HTTP Authorization scheme expected", ErrorCodes::AUTHENTICATION_FAILED);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunreachable-code"
const auto spnego_response = base64Encode(gss_acceptor_context->processToken(base64Decode(spnego_challenge), log));
#pragma GCC diagnostic pop
if (!spnego_response.empty())
response.set("WWW-Authenticate", "Negotiate " + spnego_response);
if (!gss_acceptor_context->isFailed() && !gss_acceptor_context->isReady())
{
throw Exception("Invalid authentication: it is not allowed to use X-ClickHouse HTTP headers and other authentication methods simultaneously", ErrorCodes::REQUIRED_PASSWORD);
if (spnego_response.empty())
throw Exception("Invalid authentication: 'Negotiate' HTTP Authorization failure", ErrorCodes::AUTHENTICATION_FAILED);
response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED);
response.send();
return false;
}
}
@ -297,8 +396,39 @@ void HTTPHandler::processQuery(
client_info.http_referer = request.get("Referer", "");
client_info.forwarded_for = request.get("X-Forwarded-For", "");
/// This will also set client_info.current_user and current_address
context.setUser(user, password, request.clientAddress());
try
{
context.setUser(*request_credentials, request.clientAddress());
}
catch (const Authentication::Require<BasicCredentials> & required_credentials)
{
request_credentials = std::make_unique<BasicCredentials>();
if (required_credentials.getRealm().empty())
response.set("WWW-Authenticate", "Basic");
else
response.set("WWW-Authenticate", "Basic realm=\"" + required_credentials.getRealm() + "\"");
response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED);
response.send();
return false;
}
catch (const Authentication::Require<GSSAcceptorContext> & required_credentials)
{
request_credentials = request_context->makeGSSAcceptorContext();
if (required_credentials.getRealm().empty())
response.set("WWW-Authenticate", "Negotiate");
else
response.set("WWW-Authenticate", "Negotiate realm=\"" + required_credentials.getRealm() + "\"");
response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED);
response.send();
return false;
}
request_credentials.reset();
if (!quota_key.empty())
context.setQuotaKey(quota_key);
@ -306,6 +436,25 @@ void HTTPHandler::processQuery(
client_info.initial_user = client_info.current_user;
client_info.initial_address = client_info.current_address;
return true;
}
void HTTPHandler::processQuery(
Context & context,
HTTPServerRequest & request,
HTMLForm & params,
HTTPServerResponse & response,
Output & used_output,
std::optional<CurrentThread::QueryScope> & query_scope)
{
using namespace Poco::Net;
LOG_TRACE(log, "Request URI: {}", request.getURI());
if (!authenticateUser(context, request, params, response))
return; // '401 Unauthorized' response with 'Negotiate' has been sent at this point.
/// The user could specify session identifier and session timeout.
/// It allows to modify settings, create temporary tables and reuse them in subsequent requests.
@ -355,6 +504,7 @@ void HTTPHandler::processQuery(
// Set the query id supplied by the user, if any, and also update the OpenTelemetry fields.
context.setCurrentQueryId(params.get("query_id", request.get("X-ClickHouse-Query-Id", "")));
ClientInfo & client_info = context.getClientInfo();
client_info.initial_query_id = client_info.current_query_id;
/// The client can pass a HTTP header indicating supported compression method (gzip or deflate).
@ -400,7 +550,7 @@ void HTTPHandler::processQuery(
used_output.out = std::make_shared<WriteBufferFromHTTPServerResponse>(
response,
request.getMethod() == Poco::Net::HTTPRequest::HTTP_HEAD,
request.getMethod() == HTTPRequest::HTTP_HEAD,
keep_alive_timeout,
client_supports_http_compression,
http_response_compression_method);
@ -659,11 +809,7 @@ void HTTPHandler::trySendExceptionToClient(
request.getStream().ignoreAll();
}
bool auth_fail = exception_code == ErrorCodes::UNKNOWN_USER ||
exception_code == ErrorCodes::WRONG_PASSWORD ||
exception_code == ErrorCodes::REQUIRED_PASSWORD;
if (auth_fail)
if (exception_code == ErrorCodes::REQUIRED_PASSWORD)
{
response.requireAuthentication("ClickHouse server HTTP API");
}
@ -720,12 +866,23 @@ void HTTPHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse
setThreadName("HTTPHandler");
ThreadStatus thread_status;
/// Should be initialized before anything,
/// For correct memory accounting.
Context context = server.context();
SCOPE_EXIT({
// If there is no request_credentials instance waiting for the next round, then the request is processed,
// so no need to preserve request_context either.
// Needs to be performed with respect to the other destructors in the scope though.
if (!request_credentials)
request_context.reset();
});
if (!request_context)
{
// Context should be initialized before anything, for correct memory accounting.
request_context = std::make_unique<Context>(server.context());
request_credentials.reset();
}
/// Cannot be set here, since query_id is unknown.
std::optional<CurrentThread::QueryScope> query_scope;
Output used_output;
/// In case of exception, send stack trace to client.
@ -750,11 +907,15 @@ void HTTPHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse
ErrorCodes::HTTP_LENGTH_REQUIRED);
}
processQuery(context, request, params, response, used_output, query_scope);
LOG_DEBUG(log, "Done processing query");
processQuery(*request_context, request, params, response, used_output, query_scope);
LOG_DEBUG(log, (request_credentials ? "Authentication in progress..." : "Done processing query"));
}
catch (...)
{
SCOPE_EXIT({
request_credentials.reset(); // ...so that the next requests on the connection have to always start afresh in case of exceptions.
});
tryLogCurrentException(log);
/** If exception is received from remote server, then stack trace is embedded in message.

View File

@ -18,6 +18,8 @@ namespace Poco { class Logger; }
namespace DB
{
class Context;
class Credentials;
class IServer;
class WriteBufferFromHTTPServerResponse;
@ -27,6 +29,7 @@ class HTTPHandler : public HTTPRequestHandler
{
public:
HTTPHandler(IServer & server_, const std::string & name);
virtual ~HTTPHandler() override;
void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response) override;
@ -69,6 +72,22 @@ private:
CurrentMetrics::Increment metric_increment{CurrentMetrics::HTTPConnection};
// The request_context and the request_credentials instances may outlive a single request/response loop.
// This happens only when the authentication mechanism requires more than a single request/response exchange (e.g., SPNEGO).
std::unique_ptr<Context> request_context;
std::unique_ptr<Credentials> request_credentials;
// Returns true when the user successfully authenticated,
// the request_context instance will be configured accordingly, and the request_credentials instance will be dropped.
// Returns false when the user is not authenticated yet, and the 'Negotiate' response is sent,
// the request_context and request_credentials instances are preserved.
// Throws an exception if authentication failed.
bool authenticateUser(
Context & context,
HTTPServerRequest & request,
HTMLForm & params,
HTTPServerResponse & response);
/// Also initializes 'used_output'.
void processQuery(
Context & context,

View File

@ -2,6 +2,8 @@
#include <Server/HTTP/HTTPRequestHandler.h>
#include <Server/IServer.h>
#include <Access/Credentials.h>
#include <Interpreters/Context.h>
#include <Poco/Util/LayeredConfiguration.h>

View File

@ -57,6 +57,7 @@ const char * auto_config_build[]
"USE_GRPC", "@USE_GRPC@",
"USE_LDAP", "@USE_LDAP@",
"TZDATA_VERSION", "@TZDATA_VERSION@",
"USE_KRB5", "@USE_KRB5@",
nullptr, nullptr
};

View File

@ -90,11 +90,17 @@ void StorageSystemUsers::fillData(MutableColumns & res_columns, const Context &
column_storage.insertData(storage_name.data(), storage_name.length());
column_auth_type.push_back(static_cast<Int8>(authentication.getType()));
if (authentication.getType() == Authentication::Type::LDAP_SERVER)
if (
authentication.getType() == Authentication::Type::LDAP ||
authentication.getType() == Authentication::Type::KERBEROS
)
{
Poco::JSON::Object auth_params_json;
auth_params_json.set("server", authentication.getServerName());
if (authentication.getType() == Authentication::Type::LDAP)
auth_params_json.set("server", authentication.getLDAPServerName());
else if (authentication.getType() == Authentication::Type::KERBEROS)
auth_params_json.set("realm", authentication.getKerberosRealm());
std::ostringstream oss; // STYLE_CHECK_ALLOW_STD_STRING_STREAM
oss.exceptions(std::ios::failbit);

View File

@ -26,7 +26,7 @@ class Node(object):
def repr(self):
return f"Node(name='{self.name}')"
def restart(self, timeout=300, retries=5):
def restart(self, timeout=300, retries=5, safe=True):
"""Restart node.
"""
with self.cluster.lock:
@ -48,7 +48,7 @@ class Node(object):
if r.exitcode == 0:
break
def stop(self, timeout=300, retries=5):
def stop(self, timeout=300, retries=5, safe=True):
"""Stop node.
"""
with self.cluster.lock:
@ -65,6 +65,44 @@ class Node(object):
def command(self, *args, **kwargs):
return self.cluster.command(self.name, *args, **kwargs)
def cmd(self, cmd, message=None, exitcode=None, steps=True, shell_command="bash --noediting", no_checks=False,
raise_on_exception=False, step=By, *args, **kwargs):
"""Execute and check command.
:param cmd: command
:param message: expected message that should be in the output, default: None
:param exitcode: expected exitcode, default: None
"""
command = f"{cmd}"
with Step("executing command", description=command, format_description=False) if steps else NullStep():
try:
r = self.cluster.bash(self.name, command=shell_command)(command, *args, **kwargs)
except ExpectTimeoutError:
self.cluster.close_bash(self.name)
raise
if no_checks:
return r
if exitcode is not None:
with Then(f"exitcode should be {exitcode}") if steps else NullStep():
assert r.exitcode == exitcode, error(r.output)
if message is not None:
with Then(f"output should contain message", description=message) if steps else NullStep():
assert message in r.output, error(r.output)
if message is None or "Exception:" not in message:
with Then("check if output has exception") if steps else NullStep():
if "Exception:" in r.output:
if raise_on_exception:
raise QueryRuntimeException(r.output)
assert False, error(r.output)
return r
class ClickHouseNode(Node):
"""Node with ClickHouse server.
"""
@ -163,7 +201,7 @@ class ClickHouseNode(Node):
echo -e \"{sql[:100]}...\" > {query.name}
{command}
"""
with step("executing command", description=description, format_description=False) if steps else NullStep():
with Step("executing command", description=description, format_description=False) if steps else NullStep():
try:
r = self.cluster.bash(None)(command, *args, **kwargs)
except ExpectTimeoutError:
@ -173,7 +211,7 @@ class ClickHouseNode(Node):
for setting in settings:
name, value = setting
command += f" --{name} \"{value}\""
with step("executing command", description=command, format_description=False) if steps else NullStep():
with Step("executing command", description=command, format_description=False) if steps else NullStep():
try:
r = self.cluster.bash(self.name)(command, *args, **kwargs)
except ExpectTimeoutError:
@ -227,7 +265,7 @@ class Cluster(object):
self.configs_dir = caller_configs_dir
if not os.path.exists(self.configs_dir):
raise TypeError("configs directory '{self.configs_dir}' does not exist")
raise TypeError(f"configs directory '{self.configs_dir}' does not exist")
# auto set docker-compose project directory
if docker_compose_project_dir is None:
@ -256,7 +294,7 @@ class Cluster(object):
shell.timeout = timeout
return shell
def bash(self, node, timeout=300):
def bash(self, node, timeout=300, command="bash --noediting"):
"""Returns thread-local bash terminal
to a specific node.
:param node: name of the service
@ -278,7 +316,7 @@ class Cluster(object):
self._bash[id] = Shell().__enter__()
else:
self._bash[id] = Shell(command=[
"/bin/bash", "--noediting", "-c", f"{self.docker_compose} exec {node} bash --noediting"
"/bin/bash", "--noediting", "-c", f"{self.docker_compose} exec {node} {command}"
], name=node).__enter__()
self._bash[id].timeout = timeout

View File

@ -0,0 +1,6 @@
<yandex>
<timezone>Europe/Moscow</timezone>
<listen_host replace="replace">0.0.0.0</listen_host>
<path>/var/lib/clickhouse/</path>
<tmp_path>/var/lib/clickhouse/tmp/</tmp_path>
</yandex>

View File

@ -0,0 +1,17 @@
<yandex>
<shutdown_wait_unfinished>3</shutdown_wait_unfinished>
<logger>
<level>trace</level>
<log>/var/log/clickhouse-server/log.log</log>
<errorlog>/var/log/clickhouse-server/log.err.log</errorlog>
<size>1000M</size>
<count>10</count>
<stderr>/var/log/clickhouse-server/stderr.log</stderr>
<stdout>/var/log/clickhouse-server/stdout.log</stdout>
</logger>
<part_log>
<database>system</database>
<table>part_log</table>
<flush_interval_milliseconds>500</flush_interval_milliseconds>
</part_log>
</yandex>

View File

@ -0,0 +1,5 @@
<?xml version="1.0"?>
<yandex>
<https_port>8443</https_port>
<tcp_port_secure>9440</tcp_port_secure>
</yandex>

View File

@ -0,0 +1,107 @@
<?xml version="1.0"?>
<yandex>
<remote_servers>
<replicated_cluster>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>clickhouse1</host>
<port>9000</port>
</replica>
<replica>
<host>clickhouse2</host>
<port>9000</port>
</replica>
<replica>
<host>clickhouse3</host>
<port>9000</port>
</replica>
</shard>
</replicated_cluster>
<!--
<replicated_cluster_readonly>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>clickhouse1</host>
<port>9000</port>
<user>readonly</user>
</replica>
<replica>
<host>clickhouse2</host>
<port>9000</port>
<user>readonly</user>
</replica>
<replica>
<host>clickhouse3</host>
<port>9000</port>
<user>readonly</user>
</replica>
</shard>
</replicated_cluster_readonly>
-->
<replicated_cluster_secure>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>clickhouse1</host>
<port>9440</port>
<secure>1</secure>
</replica>
<replica>
<host>clickhouse2</host>
<port>9440</port>
<secure>1</secure>
</replica>
<replica>
<host>clickhouse3</host>
<port>9440</port>
<secure>1</secure>
</replica>
</shard>
</replicated_cluster_secure>
<sharded_cluster>
<shard>
<replica>
<host>clickhouse1</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>clickhouse2</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>clickhouse3</host>
<port>9000</port>
</replica>
</shard>
</sharded_cluster>
<sharded_cluster_secure>
<shard>
<replica>
<host>clickhouse1</host>
<port>9440</port>
<secure>1</secure>
</replica>
</shard>
<shard>
<replica>
<host>clickhouse2</host>
<port>9440</port>
<secure>1</secure>
</replica>
</shard>
<shard>
<replica>
<host>clickhouse3</host>
<port>9440</port>
<secure>1</secure>
</replica>
</shard>
</sharded_cluster_secure>
</remote_servers>
</yandex>

View File

@ -0,0 +1,17 @@
<yandex>
<openSSL>
<server>
<certificateFile>/etc/clickhouse-server/ssl/server.crt</certificateFile>
<privateKeyFile>/etc/clickhouse-server/ssl/server.key</privateKeyFile>
<verificationMode>none</verificationMode>
<cacheSessions>true</cacheSessions>
</server>
<client>
<cacheSessions>true</cacheSessions>
<verificationMode>none</verificationMode>
<invalidCertificateHandler>
<name>AcceptCertificateHandler</name>
</invalidCertificateHandler>
</client>
</openSSL>
</yandex>

View File

@ -0,0 +1,20 @@
<yandex>
<storage_configuration>
<disks>
<default>
<keep_free_space_bytes>1024</keep_free_space_bytes>
</default>
</disks>
<policies>
<default>
<volumes>
<default>
<disk>default</disk>
</default>
</volumes>
</default>
</policies>
</storage_configuration>
</yandex>

View File

@ -0,0 +1,10 @@
<?xml version="1.0"?>
<yandex>
<zookeeper>
<node index="1">
<host>zookeeper</host>
<port>2181</port>
</node>
<session_timeout_ms>15000</session_timeout_ms>
</zookeeper>
</yandex>

View File

@ -0,0 +1,440 @@
<?xml version="1.0"?>
<!--
NOTE: User and query level settings are set up in "users.xml" file.
-->
<yandex>
<logger>
<!-- Possible levels: https://github.com/pocoproject/poco/blob/develop/Foundation/include/Poco/Logger.h#L105 -->
<level>trace</level>
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
<size>1000M</size>
<count>10</count>
<!-- <console>1</console> --> <!-- Default behavior is autodetection (log to console if not daemon mode and is tty) -->
</logger>
<!--display_name>production</display_name--> <!-- It is the name that will be shown in the client -->
<http_port>8123</http_port>
<tcp_port>9000</tcp_port>
<!-- For HTTPS and SSL over native protocol. -->
<!--
<https_port>8443</https_port>
<tcp_port_secure>9440</tcp_port_secure>
-->
<!-- Used with https_port and tcp_port_secure. Full ssl options list: https://github.com/ClickHouse-Extras/poco/blob/master/NetSSL_OpenSSL/include/Poco/Net/SSLManager.h#L71 -->
<openSSL>
<server> <!-- Used for https server AND secure tcp port -->
<!-- openssl req -subj "/CN=localhost" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /etc/clickhouse-server/server.key -out /etc/clickhouse-server/server.crt -->
<certificateFile>/etc/clickhouse-server/server.crt</certificateFile>
<privateKeyFile>/etc/clickhouse-server/server.key</privateKeyFile>
<!-- openssl dhparam -out /etc/clickhouse-server/dhparam.pem 4096 -->
<dhParamsFile>/etc/clickhouse-server/dhparam.pem</dhParamsFile>
<verificationMode>none</verificationMode>
<loadDefaultCAFile>true</loadDefaultCAFile>
<cacheSessions>true</cacheSessions>
<disableProtocols>sslv2,sslv3</disableProtocols>
<preferServerCiphers>true</preferServerCiphers>
</server>
<client> <!-- Used for connecting to https dictionary source -->
<loadDefaultCAFile>true</loadDefaultCAFile>
<cacheSessions>true</cacheSessions>
<disableProtocols>sslv2,sslv3</disableProtocols>
<preferServerCiphers>true</preferServerCiphers>
<!-- Use for self-signed: <verificationMode>none</verificationMode> -->
<invalidCertificateHandler>
<!-- Use for self-signed: <name>AcceptCertificateHandler</name> -->
<name>RejectCertificateHandler</name>
</invalidCertificateHandler>
</client>
</openSSL>
<!-- Default root page on http[s] server. For example load UI from https://tabix.io/ when opening http://localhost:8123 -->
<!--
<http_server_default_response><![CDATA[<html ng-app="SMI2"><head><base href="http://ui.tabix.io/"></head><body><div ui-view="" class="content-ui"></div><script src="http://loader.tabix.io/master.js"></script></body></html>]]></http_server_default_response>
-->
<!-- Port for communication between replicas. Used for data exchange. -->
<interserver_http_port>9009</interserver_http_port>
<!-- Hostname that is used by other replicas to request this server.
If not specified, than it is determined analoguous to 'hostname -f' command.
This setting could be used to switch replication to another network interface.
-->
<!--
<interserver_http_host>example.yandex.ru</interserver_http_host>
-->
<!-- Listen specified host. use :: (wildcard IPv6 address), if you want to accept connections both with IPv4 and IPv6 from everywhere. -->
<!--<listen_host>::</listen_host> -->
<!-- Same for hosts with disabled ipv6: -->
<listen_host>0.0.0.0</listen_host>
<!-- Default values - try listen localhost on ipv4 and ipv6: -->
<!--<listen_host>::1</listen_host>
<listen_host>127.0.0.1</listen_host> -->
<!-- Don't exit if ipv6 or ipv4 unavailable, but listen_host with this protocol specified -->
<!--<listen_try>0</listen_try> -->
<!-- Allow listen on same address:port -->
<!-- <listen_reuse_port>0</listen_reuse_port> -->
<!-- <listen_backlog>64</listen_backlog> -->
<max_connections>4096</max_connections>
<keep_alive_timeout>3</keep_alive_timeout>
<!-- Maximum number of concurrent queries. -->
<max_concurrent_queries>100</max_concurrent_queries>
<!-- Set limit on number of open files (default: maximum). This setting makes sense on Mac OS X because getrlimit() fails to retrieve
correct maximum value. -->
<!-- <max_open_files>262144</max_open_files> -->
<!-- Size of cache of uncompressed blocks of data, used in tables of MergeTree family.
In bytes. Cache is single for server. Memory is allocated only on demand.
Cache is used when 'use_uncompressed_cache' user setting turned on (off by default).
Uncompressed cache is advantageous only for very short queries and in rare cases.
-->
<uncompressed_cache_size>8589934592</uncompressed_cache_size>
<!-- Approximate size of mark cache, used in tables of MergeTree family.
In bytes. Cache is single for server. Memory is allocated only on demand.
You should not lower this value.
-->
<mark_cache_size>5368709120</mark_cache_size>
<!-- Path to data directory, with trailing slash. -->
<path>/var/lib/clickhouse/</path>
<!-- Path to temporary data for processing hard queries. -->
<tmp_path>/var/lib/clickhouse/tmp/</tmp_path>
<!-- Directory with user provided files that are accessible by 'file' table function. -->
<user_files_path>/var/lib/clickhouse/user_files/</user_files_path>
<!-- Sources to read users, roles, access rights, profiles of settings, quotas. -->
<user_directories>
<users_xml>
<!-- Path to configuration file with predefined users. -->
<path>users.xml</path>
</users_xml>
<local_directory>
<!-- Path to folder where users created by SQL commands are stored. -->
<path>/var/lib/clickhouse/access/</path>
</local_directory>
</user_directories>
<!-- Default profile of settings. -->
<default_profile>default</default_profile>
<!-- System profile of settings. This settings are used by internal processes (Buffer storage, Distibuted DDL worker and so on). -->
<!-- <system_profile>default</system_profile> -->
<!-- Default database. -->
<default_database>default</default_database>
<!-- Server time zone could be set here.
Time zone is used when converting between String and DateTime types,
when printing DateTime in text formats and parsing DateTime from text,
it is used in date and time related functions, if specific time zone was not passed as an argument.
Time zone is specified as identifier from IANA time zone database, like UTC or Africa/Abidjan.
If not specified, system time zone at server startup is used.
Please note, that server could display time zone alias instead of specified name.
Example: W-SU is an alias for Europe/Moscow and Zulu is an alias for UTC.
-->
<!-- <timezone>Europe/Moscow</timezone> -->
<!-- You can specify umask here (see "man umask"). Server will apply it on startup.
Number is always parsed as octal. Default umask is 027 (other users cannot read logs, data files, etc; group can only read).
-->
<!-- <umask>022</umask> -->
<!-- Perform mlockall after startup to lower first queries latency
and to prevent clickhouse executable from being paged out under high IO load.
Enabling this option is recommended but will lead to increased startup time for up to a few seconds.
-->
<mlock_executable>false</mlock_executable>
<!-- Configuration of clusters that could be used in Distributed tables.
https://clickhouse.yandex/docs/en/table_engines/distributed/
-->
<remote_servers incl="remote" >
<!-- Test only shard config for testing distributed storage -->
<test_shard_localhost>
<shard>
<replica>
<host>localhost</host>
<port>9000</port>
</replica>
</shard>
</test_shard_localhost>
<test_cluster_two_shards_localhost>
<shard>
<replica>
<host>localhost</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>localhost</host>
<port>9000</port>
</replica>
</shard>
</test_cluster_two_shards_localhost>
<test_shard_localhost_secure>
<shard>
<replica>
<host>localhost</host>
<port>9440</port>
<secure>1</secure>
</replica>
</shard>
</test_shard_localhost_secure>
<test_unavailable_shard>
<shard>
<replica>
<host>localhost</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>localhost</host>
<port>1</port>
</replica>
</shard>
</test_unavailable_shard>
</remote_servers>
<!-- If element has 'incl' attribute, then for it's value will be used corresponding substitution from another file.
By default, path to file with substitutions is /etc/metrika.xml. It could be changed in config in 'include_from' element.
Values for substitutions are specified in /yandex/name_of_substitution elements in that file.
-->
<!-- ZooKeeper is used to store metadata about replicas, when using Replicated tables.
Optional. If you don't use replicated tables, you could omit that.
See https://clickhouse.yandex/docs/en/table_engines/replication/
-->
<zookeeper incl="zookeeper" optional="true" />
<!-- Substitutions for parameters of replicated tables.
Optional. If you don't use replicated tables, you could omit that.
See https://clickhouse.yandex/docs/en/table_engines/replication/#creating-replicated-tables
-->
<macros incl="macros" optional="true" />
<!-- Reloading interval for embedded dictionaries, in seconds. Default: 3600. -->
<builtin_dictionaries_reload_interval>3600</builtin_dictionaries_reload_interval>
<!-- Maximum session timeout, in seconds. Default: 3600. -->
<max_session_timeout>3600</max_session_timeout>
<!-- Default session timeout, in seconds. Default: 60. -->
<default_session_timeout>60</default_session_timeout>
<!-- Sending data to Graphite for monitoring. Several sections can be defined. -->
<!--
interval - send every X second
root_path - prefix for keys
hostname_in_path - append hostname to root_path (default = true)
metrics - send data from table system.metrics
events - send data from table system.events
asynchronous_metrics - send data from table system.asynchronous_metrics
-->
<!--
<graphite>
<host>localhost</host>
<port>42000</port>
<timeout>0.1</timeout>
<interval>60</interval>
<root_path>one_min</root_path>
<hostname_in_path>true</hostname_in_path>
<metrics>true</metrics>
<events>true</events>
<asynchronous_metrics>true</asynchronous_metrics>
</graphite>
<graphite>
<host>localhost</host>
<port>42000</port>
<timeout>0.1</timeout>
<interval>1</interval>
<root_path>one_sec</root_path>
<metrics>true</metrics>
<events>true</events>
<asynchronous_metrics>false</asynchronous_metrics>
</graphite>
-->
<!-- Query log. Used only for queries with setting log_queries = 1. -->
<query_log>
<!-- What table to insert data. If table is not exist, it will be created.
When query log structure is changed after system update,
then old table will be renamed and new table will be created automatically.
-->
<database>system</database>
<table>query_log</table>
<!--
PARTITION BY expr https://clickhouse.yandex/docs/en/table_engines/custom_partitioning_key/
Example:
event_date
toMonday(event_date)
toYYYYMM(event_date)
toStartOfHour(event_time)
-->
<partition_by>toYYYYMM(event_date)</partition_by>
<!-- Interval of flushing data. -->
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</query_log>
<!-- Trace log. Stores stack traces collected by query profilers.
See query_profiler_real_time_period_ns and query_profiler_cpu_time_period_ns settings. -->
<trace_log>
<database>system</database>
<table>trace_log</table>
<partition_by>toYYYYMM(event_date)</partition_by>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</trace_log>
<!-- Query thread log. Has information about all threads participated in query execution.
Used only for queries with setting log_query_threads = 1. -->
<query_thread_log>
<database>system</database>
<table>query_thread_log</table>
<partition_by>toYYYYMM(event_date)</partition_by>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</query_thread_log>
<!-- Uncomment if use part log.
Part log contains information about all actions with parts in MergeTree tables (creation, deletion, merges, downloads).
<part_log>
<database>system</database>
<table>part_log</table>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</part_log>
-->
<!-- Uncomment to write text log into table.
Text log contains all information from usual server log but stores it in structured and efficient way.
<text_log>
<database>system</database>
<table>text_log</table>
<flush_interval_milliseconds>7500</flush_interval_milliseconds>
</text_log>
-->
<!-- Parameters for embedded dictionaries, used in Yandex.Metrica.
See https://clickhouse.yandex/docs/en/dicts/internal_dicts/
-->
<!-- Path to file with region hierarchy. -->
<!-- <path_to_regions_hierarchy_file>/opt/geo/regions_hierarchy.txt</path_to_regions_hierarchy_file> -->
<!-- Path to directory with files containing names of regions -->
<!-- <path_to_regions_names_files>/opt/geo/</path_to_regions_names_files> -->
<!-- Configuration of external dictionaries. See:
https://clickhouse.yandex/docs/en/dicts/external_dicts/
-->
<dictionaries_config>*_dictionary.xml</dictionaries_config>
<!-- Uncomment if you want data to be compressed 30-100% better.
Don't do that if you just started using ClickHouse.
-->
<compression incl="compression">
<!--
<!- - Set of variants. Checked in order. Last matching case wins. If nothing matches, lz4 will be used. - ->
<case>
<!- - Conditions. All must be satisfied. Some conditions may be omitted. - ->
<min_part_size>10000000000</min_part_size> <!- - Min part size in bytes. - ->
<min_part_size_ratio>0.01</min_part_size_ratio> <!- - Min size of part relative to whole table size. - ->
<!- - What compression method to use. - ->
<method>zstd</method>
</case>
-->
</compression>
<!-- Allow to execute distributed DDL queries (CREATE, DROP, ALTER, RENAME) on cluster.
Works only if ZooKeeper is enabled. Comment it if such functionality isn't required. -->
<distributed_ddl>
<!-- Path in ZooKeeper to queue with DDL queries -->
<path>/clickhouse/task_queue/ddl</path>
<!-- Settings from this profile will be used to execute DDL queries -->
<!-- <profile>default</profile> -->
</distributed_ddl>
<!-- Settings to fine tune MergeTree tables. See documentation in source code, in MergeTreeSettings.h -->
<!--
<merge_tree>
<max_suspicious_broken_parts>5</max_suspicious_broken_parts>
</merge_tree>
-->
<!-- Protection from accidental DROP.
If size of a MergeTree table is greater than max_table_size_to_drop (in bytes) than table could not be dropped with any DROP query.
If you want do delete one table and don't want to restart clickhouse-server, you could create special file <clickhouse-path>/flags/force_drop_table and make DROP once.
By default max_table_size_to_drop is 50GB; max_table_size_to_drop=0 allows to DROP any tables.
The same for max_partition_size_to_drop.
Uncomment to disable protection.
-->
<!-- <max_table_size_to_drop>0</max_table_size_to_drop> -->
<!-- <max_partition_size_to_drop>0</max_partition_size_to_drop> -->
<!-- Example of parameters for GraphiteMergeTree table engine -->
<graphite_rollup_example>
<pattern>
<regexp>click_cost</regexp>
<function>any</function>
<retention>
<age>0</age>
<precision>3600</precision>
</retention>
<retention>
<age>86400</age>
<precision>60</precision>
</retention>
</pattern>
<default>
<function>max</function>
<retention>
<age>0</age>
<precision>60</precision>
</retention>
<retention>
<age>3600</age>
<precision>300</precision>
</retention>
<retention>
<age>86400</age>
<precision>3600</precision>
</retention>
</default>
</graphite_rollup_example>
<!-- Directory in <clickhouse-path> containing schema files for various input formats.
The directory will be created if it doesn't exist.
-->
<format_schema_path>/var/lib/clickhouse/format_schemas/</format_schema_path>
<!-- Uncomment to disable ClickHouse internal DNS caching. -->
<!-- <disable_internal_dns_cache>1</disable_internal_dns_cache> -->
</yandex>

View File

@ -0,0 +1,8 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEAua92DDli13gJ+//ZXyGaggjIuidqB0crXfhUlsrBk9BV1hH3i7fR
XGP9rUdk2ubnB3k2ejBStL5oBrkHm9SzUFSQHqfDjLZjKoUpOEmuDc4cHvX1XTR5
Pr1vf5cd0yEncJWG5W4zyUB8k++SUdL2qaeslSs+f491HBLDYn/h8zCgRbBvxhxb
9qeho1xcbnWeqkN6Kc9bgGozA16P9NLuuLttNnOblkH+lMBf42BSne/TWt3AlGZf
slKmmZcySUhF8aKfJnLKbkBCFqOtFRh8zBA9a7g+BT/lSANATCDPaAk1YVih2EKb
dpc3briTDbRsiqg2JKMI7+VdULY9bh3EawIBAg==
-----END DH PARAMETERS-----

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIC/TCCAeWgAwIBAgIJANjx1QSR77HBMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
BAMMCWxvY2FsaG9zdDAgFw0xODA3MzAxODE2MDhaGA8yMjkyMDUxNDE4MTYwOFow
FDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAs9uSo6lJG8o8pw0fbVGVu0tPOljSWcVSXH9uiJBwlZLQnhN4SFSFohfI
4K8U1tBDTnxPLUo/V1K9yzoLiRDGMkwVj6+4+hE2udS2ePTQv5oaMeJ9wrs+5c9T
4pOtlq3pLAdm04ZMB1nbrEysceVudHRkQbGHzHp6VG29Fw7Ga6YpqyHQihRmEkTU
7UCYNA+Vk7aDPdMS/khweyTpXYZimaK9f0ECU3/VOeG3fH6Sp2X6FN4tUj/aFXEj
sRmU5G2TlYiSIUMF2JPdhSihfk1hJVALrHPTU38SOL+GyyBRWdNcrIwVwbpvsvPg
pryMSNxnpr0AK0dFhjwnupIv5hJIOQIDAQABo1AwTjAdBgNVHQ4EFgQUjPLb3uYC
kcamyZHK4/EV8jAP0wQwHwYDVR0jBBgwFoAUjPLb3uYCkcamyZHK4/EV8jAP0wQw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAM/ocuDvfPus/KpMVD51j
4IdlU8R0vmnYLQ+ygzOAo7+hUWP5j0yvq4ILWNmQX6HNvUggCgFv9bjwDFhb/5Vr
85ieWfTd9+LTjrOzTw4avdGwpX9G+6jJJSSq15tw5ElOIFb/qNA9O4dBiu8vn03C
L/zRSXrARhSqTW5w/tZkUcSTT+M5h28+Lgn9ysx4Ff5vi44LJ1NnrbJbEAIYsAAD
+UA+4MBFKx1r6hHINULev8+lCfkpwIaeS8RL+op4fr6kQPxnULw8wT8gkuc8I4+L
P9gg/xDHB44T3ADGZ5Ib6O0DJaNiToO6rnoaaxs0KkotbvDWvRoxEytSbXKoYjYp
0g==
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCz25KjqUkbyjyn
DR9tUZW7S086WNJZxVJcf26IkHCVktCeE3hIVIWiF8jgrxTW0ENOfE8tSj9XUr3L
OguJEMYyTBWPr7j6ETa51LZ49NC/mhox4n3Cuz7lz1Pik62WreksB2bThkwHWdus
TKxx5W50dGRBsYfMenpUbb0XDsZrpimrIdCKFGYSRNTtQJg0D5WTtoM90xL+SHB7
JOldhmKZor1/QQJTf9U54bd8fpKnZfoU3i1SP9oVcSOxGZTkbZOViJIhQwXYk92F
KKF+TWElUAusc9NTfxI4v4bLIFFZ01ysjBXBum+y8+CmvIxI3GemvQArR0WGPCe6
ki/mEkg5AgMBAAECggEATrbIBIxwDJOD2/BoUqWkDCY3dGevF8697vFuZKIiQ7PP
TX9j4vPq0DfsmDjHvAPFkTHiTQXzlroFik3LAp+uvhCCVzImmHq0IrwvZ9xtB43f
7Pkc5P6h1l3Ybo8HJ6zRIY3TuLtLxuPSuiOMTQSGRL0zq3SQ5DKuGwkz+kVjHXUN
MR2TECFwMHKQ5VLrC+7PMpsJYyOMlDAWhRfUalxC55xOXTpaN8TxNnwQ8K2ISVY5
212Jz/a4hn4LdwxSz3Tiu95PN072K87HLWx3EdT6vW4Ge5P/A3y+smIuNAlanMnu
plHBRtpATLiTxZt/n6npyrfQVbYjSH7KWhB8hBHtaQKBgQDh9Cq1c/KtqDtE0Ccr
/r9tZNTUwBE6VP+3OJeKdEdtsfuxjOCkS1oAjgBJiSDOiWPh1DdoDeVZjPKq6pIu
Mq12OE3Doa8znfCXGbkSzEKOb2unKZMJxzrz99kXt40W5DtrqKPNb24CNqTiY8Aa
CjtcX+3weat82VRXvph6U8ltMwKBgQDLxjiQQzNoY7qvg7CwJCjf9qq8jmLK766g
1FHXopqS+dTxDLM8eJSRrpmxGWJvNeNc1uPhsKsKgotqAMdBUQTf7rSTbt4MyoH5
bUcRLtr+0QTK9hDWMOOvleqNXha68vATkohWYfCueNsC60qD44o8RZAS6UNy3ENq
cM1cxqe84wKBgQDKkHutWnooJtajlTxY27O/nZKT/HA1bDgniMuKaz4R4Gr1PIez
on3YW3V0d0P7BP6PWRIm7bY79vkiMtLEKdiKUGWeyZdo3eHvhDb/3DCawtau8L2K
GZsHVp2//mS1Lfz7Qh8/L/NedqCQ+L4iWiPnZ3THjjwn3CoZ05ucpvrAMwKBgB54
nay039MUVq44Owub3KDg+dcIU62U+cAC/9oG7qZbxYPmKkc4oL7IJSNecGHA5SbU
2268RFdl/gLz6tfRjbEOuOHzCjFPdvAdbysanpTMHLNc6FefJ+zxtgk9sJh0C4Jh
vxFrw9nTKKzfEl12gQ1SOaEaUIO0fEBGbe8ZpauRAoGAMAlGV+2/K4ebvAJKOVTa
dKAzQ+TD2SJmeR1HZmKDYddNqwtZlzg3v4ZhCk4eaUmGeC1Bdh8MDuB3QQvXz4Dr
vOIP4UVaOr+uM+7TgAgVnP4/K6IeJGzUDhX93pmpWhODfdu/oojEKVcpCojmEmS1
KCBtmIrQLqzMpnBpLNuSY+Q=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,133 @@
<?xml version="1.0"?>
<yandex>
<!-- Profiles of settings. -->
<profiles>
<!-- Default settings. -->
<default>
<!-- Maximum memory usage for processing single query, in bytes. -->
<max_memory_usage>10000000000</max_memory_usage>
<!-- Use cache of uncompressed blocks of data. Meaningful only for processing many of very short queries. -->
<use_uncompressed_cache>0</use_uncompressed_cache>
<!-- How to choose between replicas during distributed query processing.
random - choose random replica from set of replicas with minimum number of errors
nearest_hostname - from set of replicas with minimum number of errors, choose replica
with minimum number of different symbols between replica's hostname and local hostname
(Hamming distance).
in_order - first live replica is chosen in specified order.
first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.
-->
<load_balancing>random</load_balancing>
</default>
<!-- Profile that allows only read queries. -->
<readonly>
<readonly>1</readonly>
</readonly>
</profiles>
<!-- Users and ACL. -->
<users>
<!-- If user name was not specified, 'default' user is used. -->
<default>
<!-- Password could be specified in plaintext or in SHA256 (in hex format).
If you want to specify password in plaintext (not recommended), place it in 'password' element.
Example: <password>qwerty</password>.
Password could be empty.
If you want to specify SHA256, place it in 'password_sha256_hex' element.
Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).
If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
How to generate decent password:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
In first line will be password and in second - corresponding SHA256.
How to generate double SHA1:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | openssl dgst -sha1 -binary | openssl dgst -sha1
In first line will be password and in second - corresponding double SHA1.
-->
<password></password>
<!-- List of networks with open access.
To open access from everywhere, specify:
<ip>::/0</ip>
To open access only from localhost, specify:
<ip>::1</ip>
<ip>127.0.0.1</ip>
Each element of list has one of the following forms:
<ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
<host> Hostname. Example: server01.yandex.ru.
To check access, DNS query is performed, and all received addresses compared to peer address.
<host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.yandex\.ru$
To check access, DNS PTR query is performed for peer address and then regexp is applied.
Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
Strongly recommended that regexp is ends with $
All results of DNS requests are cached till server restart.
-->
<networks incl="networks" replace="replace">
<ip>::/0</ip>
</networks>
<!-- Settings profile for user. -->
<profile>default</profile>
<!-- Quota for user. -->
<quota>default</quota>
<!-- Allow access management -->
<access_management>1</access_management>
<!-- Example of row level security policy. -->
<!-- <databases>
<test>
<filtered_table1>
<filter>a = 1</filter>
</filtered_table1>
<filtered_table2>
<filter>a + b &lt; 1 or c - d &gt; 5</filter>
</filtered_table2>
</test>
</databases> -->
</default>
<!-- Example of user with readonly access. -->
<!-- <readonly>
<password></password>
<networks incl="networks" replace="replace">
<ip>::1</ip>
<ip>127.0.0.1</ip>
</networks>
<profile>readonly</profile>
<quota>default</quota>
</readonly> -->
</users>
<!-- Quotas. -->
<quotas>
<!-- Name of quota. -->
<default>
<!-- Limits for time interval. You could specify many intervals with different limits. -->
<interval>
<!-- Length of interval. -->
<duration>3600</duration>
<!-- No limits. Just calculate resource usage for time interval. -->
<queries>0</queries>
<errors>0</errors>
<result_rows>0</result_rows>
<read_rows>0</read_rows>
<execution_time>0</execution_time>
</interval>
</default>
</quotas>
</yandex>

View File

@ -0,0 +1,5 @@
<yandex>
<kerberos>
<realm>EXAMPLE.COM</realm>
</kerberos>
</yandex>

View File

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<yandex>
<macros>
<replica>clickhouse1</replica>
<shard>01</shard>
<shard2>01</shard2>
</macros>
</yandex>

View File

@ -0,0 +1,10 @@
<yandex>
<users>
<kerberos_user>
<kerberos>
<realm>EXAMPLE.COM</realm>
</kerberos>
<access_management>1</access_management>
</kerberos_user>
</users>
</yandex>

View File

@ -0,0 +1,5 @@
<yandex>
<kerberos>
<realm>EXAMPLE.COM</realm>
</kerberos>
</yandex>

View File

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<yandex>
<macros>
<replica>clickhouse2</replica>
<shard>01</shard>
<shard2>02</shard2>
</macros>
</yandex>

View File

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<yandex>
<macros>
<replica>clickhouse3</replica>
<shard>01</shard>
<shard2>03</shard2>
</macros>
</yandex>

View File

@ -0,0 +1,38 @@
[kdc]
require-preauth = false
[libdefaults]
default_realm = EXAMPLE.COM
ticket_lifetime = 24000
dns_lookup_realm = false
dns_lookup_kdc = false
dns_fallback = false
rdns = false
[realms]
EXAMPLE.COM = {
kdc = kerberos
admin_server = kerberos
}
OTHER.COM = {
kdc = kerberos
admin_server = kerberos
}
[domain_realm]
docker-compose_default = EXAMPLE.COM
.docker-compose_default = EXAMPLE.COM
[appdefaults]
validate = false
pam = {
debug = false
ticket_lifetime = 36000
renew_lifetime = 36000
forwardable = true
krb4_convert = false
}
[logging]
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmin.log

View File

@ -0,0 +1,15 @@
[kdcdefaults]
kdc_ports = 88
[realms]
EXAMPLE.COM = {
kadmind_port = 749
max_life = 12h 0m 0s
max_renewable_life = 7d 0h 0m 0s
master_key_type = des3-hmac-sha1
supported_enctypes = des3-hmac-sha1:normal des-cbc-crc:normal des-cbc-crc:v4
}
[logging]
kdc = FILE:/usr/local/var/krb5kdc/kdc.log
admin_server = FILE:/usr/local/var/krb5kdc/kadmin.log

View File

@ -0,0 +1,31 @@
[inet_http_server]
port=9001
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl = http://127.0.0.1:9001
[program:krb5kdc]
command = /usr/sbin/krb5kdc -n
startretries = 1
startsecs = 5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
[program:kadmind]
command = /usr/sbin/kadmind -nofork
startretries = 1
startsecs = 5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
[supervisord]
logfile = /tmp/supervisord_zbx_server.log
loglevel = critical
nodaemon = true
user = root
pidfile = /tmp/supervisord_zbx_server.pid
directory = /tmp

View File

@ -0,0 +1,32 @@
version: '2.3'
services:
clickhouse:
image: yandex/clickhouse-integration-test:21454
expose:
- "9000"
- "9009"
- "8123"
volumes:
- "${CLICKHOUSE_TESTS_DIR}/configs/clickhouse/ssl:/etc/clickhouse-server/ssl"
- "${CLICKHOUSE_TESTS_DIR}/configs/clickhouse/config.xml:/etc/clickhouse-server/config.xml"
- "${CLICKHOUSE_TESTS_DIR}/configs/clickhouse/users.xml:/etc/clickhouse-server/users.xml"
- "${CLICKHOUSE_TESTS_SERVER_BIN_PATH:-/usr/bin/clickhouse}:/usr/bin/clickhouse"
- "${CLICKHOUSE_TESTS_ODBC_BRIDGE_BIN_PATH:-/usr/bin/clickhouse-odbc-bridge}:/usr/bin/clickhouse-odbc-bridge"
- "${CLICKHOUSE_TESTS_DIR}/configs/kerberos/etc/krb5.conf:/etc/krb5.conf"
entrypoint: bash -c "clickhouse server --config-file=/etc/clickhouse-server/config.xml --log-file=/var/log/clickhouse-server/clickhouse-server.log --errorlog-file=/var/log/clickhouse-server/clickhouse-server.err.log"
healthcheck:
test: clickhouse client --query='select 1'
interval: 10s
timeout: 10s
retries: 3
start_period: 300s
environment:
KRB5_CLIENT_KTNAME: /etc/krb5.keytab
KRB5_KTNAME: /etc/krb5.keytab
cap_add:
- SYS_PTRACE
security_opt:
- label:disable

View File

@ -0,0 +1,75 @@
version: '2.3'
services:
zookeeper:
extends:
file: zookeeper-service.yml
service: zookeeper
kerberos:
extends:
file: kerberos-service.yml
service: kerberos
hostname: kerberos
depends_on:
zookeeper:
condition: service_healthy
clickhouse1:
extends:
file: clickhouse-service.yml
service: clickhouse
hostname: clickhouse1
volumes:
- "${CLICKHOUSE_TESTS_DIR}/_instances/clickhouse1/database/:/var/lib/clickhouse/"
- "${CLICKHOUSE_TESTS_DIR}/_instances/clickhouse1/logs/:/var/log/clickhouse-server/"
- "${CLICKHOUSE_TESTS_DIR}/configs/clickhouse1/config.d:/etc/clickhouse-server/config.d"
- "${CLICKHOUSE_TESTS_DIR}/configs/clickhouse1/users.d:/etc/clickhouse-server/users.d"
depends_on:
zookeeper:
condition: service_healthy
clickhouse2:
extends:
file: clickhouse-service.yml
service: clickhouse
hostname: clickhouse2
volumes:
- "${CLICKHOUSE_TESTS_DIR}/_instances/clickhouse2/database/:/var/lib/clickhouse/"
- "${CLICKHOUSE_TESTS_DIR}/_instances/clickhouse2/logs/:/var/log/clickhouse-server/"
- "${CLICKHOUSE_TESTS_DIR}/configs/clickhouse2/config.d:/etc/clickhouse-server/config.d"
- "${CLICKHOUSE_TESTS_DIR}/configs/clickhouse2/users.d:/etc/clickhouse-server/users.d"
depends_on:
zookeeper:
condition: service_healthy
clickhouse3:
extends:
file: clickhouse-service.yml
service: clickhouse
hostname: clickhouse3
volumes:
- "${CLICKHOUSE_TESTS_DIR}/_instances/clickhouse3/database/:/var/lib/clickhouse/"
- "${CLICKHOUSE_TESTS_DIR}/_instances/clickhouse3/logs/:/var/log/clickhouse-server/"
- "${CLICKHOUSE_TESTS_DIR}/configs/clickhouse3/config.d:/etc/clickhouse-server/config.d"
- "${CLICKHOUSE_TESTS_DIR}/configs/clickhouse3/users.d:/etc/clickhouse-server/users.d"
depends_on:
zookeeper:
condition: service_healthy
# dummy service which does nothing, but allows to postpone
# 'docker-compose up -d' till all dependecies will go healthy
all_services_ready:
image: hello-world
depends_on:
clickhouse1:
condition: service_healthy
clickhouse2:
condition: service_healthy
clickhouse3:
condition: service_healthy
zookeeper:
condition: service_healthy
kerberos:
condition: service_healthy

View File

@ -0,0 +1,27 @@
version: '2.3'
services:
kerberos:
image: zvonand/docker-krb5-server:1.0.0
restart: always
expose:
- "88"
- "464"
- "749"
healthcheck:
test: echo 1
interval: 10s
timeout: 10s
retries: 3
start_period: 300s
environment:
KRB5_PASS: pwd
KRB5_REALM: EXAMPLE.COM
KRB5_KDC: localhost
volumes:
- "${CLICKHOUSE_TESTS_DIR}/configs/kerberos/etc/krb5kdc/kdc.conf:/etc/krb5kdc/kdc.conf"
- "${CLICKHOUSE_TESTS_DIR}/_instances/kerberos/krb5kdc/log/kdc.log:/usr/local/var/krb5kdc/kdc.log"
- "${CLICKHOUSE_TESTS_DIR}/_instances/kerberos/krb5kdc/log/kadmin.log:/usr/local/var/krb5kdc/kadmin.log"
- "${CLICKHOUSE_TESTS_DIR}/_instances/kerberos/var/log:/var/log"
security_opt:
- label:disable

View File

@ -0,0 +1,18 @@
version: '2.3'
services:
zookeeper:
image: zookeeper:3.4.12
expose:
- "2181"
environment:
ZOO_TICK_TIME: 500
ZOO_MY_ID: 1
healthcheck:
test: echo stat | nc localhost 2181
interval: 10s
timeout: 10s
retries: 3
start_period: 300s
security_opt:
- label:disable

View File

@ -0,0 +1,38 @@
import sys
from testflows.core import *
append_path(sys.path, "..")
from helpers.cluster import Cluster
from helpers.argparser import argparser
from kerberos.requirements.requirements import *
xfails = {
}
@TestModule
@Name("kerberos")
@ArgumentParser(argparser)
@Requirements(
RQ_SRS_016_Kerberos("1.0")
)
@XFails(xfails)
def regression(self, local, clickhouse_binary_path, stress=None, parallel=None):
"""ClickHouse Kerberos authentication test regression module.
"""
nodes = {
"clickhouse": ("clickhouse1", "clickhouse2", "clickhouse3"),
"kerberos": ("kerberos", ),
}
with Cluster(local, clickhouse_binary_path, nodes=nodes) as cluster:
self.context.cluster = cluster
Feature(run=load("kerberos.tests.generic", "generic"), flags=TE)
Feature(run=load("kerberos.tests.config", "config"), flags=TE)
Feature(run=load("kerberos.tests.parallel", "parallel"), flags=TE)
if main():
regression()

View File

@ -0,0 +1,281 @@
# QA-SRS016 ClickHouse Kerberos Authentication
# Software Requirements Specification
## Table of Contents
* 1 [Revision History](#revision-history)
* 2 [Introduction](#introduction)
* 3 [Terminology](#terminology)
* 4 [Requirements](#requirements)
* 4.1 [Generic](#generic)
* 4.1.1 [RQ.SRS-016.Kerberos](#rqsrs-016kerberos)
* 4.2 [Configuration](#configuration)
* 4.2.1 [RQ.SRS-016.Kerberos.Configuration.MultipleAuthMethods](#rqsrs-016kerberosconfigurationmultipleauthmethods)
* 4.2.2 [RQ.SRS-016.Kerberos.Configuration.KerberosNotEnabled](#rqsrs-016kerberosconfigurationkerberosnotenabled)
* 4.2.3 [RQ.SRS-016.Kerberos.Configuration.MultipleKerberosSections](#rqsrs-016kerberosconfigurationmultiplekerberossections)
* 4.2.4 [RQ.SRS-016.Kerberos.Configuration.WrongUserRealm](#rqsrs-016kerberosconfigurationwronguserrealm)
* 4.2.5 [RQ.SRS-016.Kerberos.Configuration.PrincipalAndRealmSpecified](#rqsrs-016kerberosconfigurationprincipalandrealmspecified)
* 4.2.6 [RQ.SRS-016.Kerberos.Configuration.MultiplePrincipalSections](#rqsrs-016kerberosconfigurationmultipleprincipalsections)
* 4.2.7 [RQ.SRS-016.Kerberos.Configuration.MultipleRealmSections](#rqsrs-016kerberosconfigurationmultiplerealmsections)
* 4.3 [Valid User](#valid-user)
* 4.3.1 [RQ.SRS-016.Kerberos.ValidUser.XMLConfiguredUser](#rqsrs-016kerberosvaliduserxmlconfigureduser)
* 4.3.2 [RQ.SRS-016.Kerberos.ValidUser.RBACConfiguredUser](#rqsrs-016kerberosvaliduserrbacconfigureduser)
* 4.3.3 [RQ.SRS-016.Kerberos.ValidUser.KerberosNotConfigured](#rqsrs-016kerberosvaliduserkerberosnotconfigured)
* 4.4 [Invalid User](#invalid-user)
* 4.4.1 [RQ.SRS-016.Kerberos.InvalidUser](#rqsrs-016kerberosinvaliduser)
* 4.4.2 [RQ.SRS-016.Kerberos.InvalidUser.UserDeleted](#rqsrs-016kerberosinvaliduseruserdeleted)
* 4.5 [Kerberos Not Available](#kerberos-not-available)
* 4.5.1 [RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidServerTicket](#rqsrs-016kerberoskerberosnotavailableinvalidserverticket)
* 4.5.2 [RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidClientTicket](#rqsrs-016kerberoskerberosnotavailableinvalidclientticket)
* 4.5.3 [RQ.SRS-016.Kerberos.KerberosNotAvailable.ValidTickets](#rqsrs-016kerberoskerberosnotavailablevalidtickets)
* 4.6 [Kerberos Restarted](#kerberos-restarted)
* 4.6.1 [RQ.SRS-016.Kerberos.KerberosServerRestarted](#rqsrs-016kerberoskerberosserverrestarted)
* 4.7 [Performance](#performance)
* 4.7.1 [RQ.SRS-016.Kerberos.Performance](#rqsrs-016kerberosperformance)
* 4.8 [Parallel Requests processing](#parallel-requests-processing)
* 4.8.1 [RQ.SRS-016.Kerberos.Parallel](#rqsrs-016kerberosparallel)
* 4.8.2 [RQ.SRS-016.Kerberos.Parallel.ValidRequests.KerberosAndNonKerberos](#rqsrs-016kerberosparallelvalidrequestskerberosandnonkerberos)
* 4.8.3 [RQ.SRS-016.Kerberos.Parallel.ValidRequests.SameCredentials](#rqsrs-016kerberosparallelvalidrequestssamecredentials)
* 4.8.4 [RQ.SRS-016.Kerberos.Parallel.ValidRequests.DifferentCredentials](#rqsrs-016kerberosparallelvalidrequestsdifferentcredentials)
* 4.8.5 [RQ.SRS-016.Kerberos.Parallel.ValidInvalid](#rqsrs-016kerberosparallelvalidinvalid)
* 4.8.6 [RQ.SRS-016.Kerberos.Parallel.Deletion](#rqsrs-016kerberosparalleldeletion)
* 5 [References](#references)
## Revision History
This document is stored in an electronic form using [Git] source control management software
hosted in a [GitHub Repository].
All the updates are tracked using the [Git]'s [Revision History].
## Introduction
This document specifies the behavior for authenticating existing users via [Kerberos] authentication protocol.
Existing [ClickHouse] users, that are properly configured, have an ability to authenticate using [Kerberos]. Kerberos authentication is only supported for HTTP requests, and users configured to authenticate via Kerberos cannot be authenticated by any other means of authentication.
In order to use Kerberos authentication, Kerberos needs to be properly configured in the environment: Kerberos server must be present and user's and server's credentials must be set up. Configuring the Kerberos environment is outside the scope of this document.
## Terminology
* **Principal** -
A unique identity that uses [Kerberos].
* **Realm** -
A logical group of resources and identities that use [Kerberos].
* **Ticket** -
An encrypted block of data that authenticates principal.
* **Credentials** -
A Kerberos ticket and a session key.
* **Kerberized request** -
A HTTP query to ClickHouse server, which uses GSS [SPNEGO] and [Kerberos] to authenticate client.
* **Unkerberized request** -
A HTTP query to ClickHouse server, which uses any other mean of authentication than GSS [SPNEGO] or [Kerberos].
For a more detailed descriprion, visit [Kerberos terminology].
## Requirements
### Generic
#### RQ.SRS-016.Kerberos
version: 1.0
[ClickHouse] SHALL support user authentication using [Kerberos] server.
### Configuration
#### RQ.SRS-016.Kerberos.Configuration.MultipleAuthMethods
version: 1.0
[ClickHouse] SHALL generate an exception and TERMINATE in case some user in `users.xml` has a `<kerberos>` section specified alongside with any other authentication method's section, e.g. `ldap`, `password`.
#### RQ.SRS-016.Kerberos.Configuration.KerberosNotEnabled
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication in case user is properly configured for using Kerberos, but Kerberos itself is not enabled in `config.xml`. For example:
```xml
<yandex>
<!- ... -->
<kerberos />
</yandex>
```
```xml
<yandex>
<!- ... -->
<kerberos>
<principal>HTTP/clickhouse.example.com@EXAMPLE.COM</principal>
</kerberos>
</yandex>
```
```xml
<yandex>
<!- ... -->
<kerberos>
<realm>EXAMPLE.COM</realm>
</kerberos>
</yandex>
```
#### RQ.SRS-016.Kerberos.Configuration.MultipleKerberosSections
version: 1.0
[ClickHouse] SHALL disable [Kerberos] and reject [Kerberos] authentication in case multiple `kerberos` sections are present in `config.xml`.
#### RQ.SRS-016.Kerberos.Configuration.WrongUserRealm
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if user's realm specified in `users.xml` doesn't match the realm of the principal trying to authenticate.
#### RQ.SRS-016.Kerberos.Configuration.PrincipalAndRealmSpecified
version: 1.0
[ClickHouse] SHALL generate an exception and disable [Kerberos] in case both `realm` and `principal` sections are defined in `config.xml`.
#### RQ.SRS-016.Kerberos.Configuration.MultiplePrincipalSections
version: 1.0
[ClickHouse] SHALL generate an exception and disable [Kerberos] in case multiple `principal` sections are specified inside `kerberos` section in `config.xml`.
#### RQ.SRS-016.Kerberos.Configuration.MultipleRealmSections
version: 1.0
[ClickHouse] SHALL generate an exception and disable [Kerberos] in case multiple `realm` sections are specified inside `kerberos` section in `config.xml`.
### Valid User
#### RQ.SRS-016.Kerberos.ValidUser.XMLConfiguredUser
version: 1.0
[ClickHouse] SHALL accept [Kerberos] authentication for a user that is configured in `users.xml` and has [Kerberos] enabled, i.e.:
```xml
<yandex>
<!- ... -->
<users>
<!- ... -->
<my_user>
<!- ... -->
<kerberos>
<realm>EXAMPLE.COM</realm>
</kerberos>
</my_user>
</users>
</yandex>
```
#### RQ.SRS-016.Kerberos.ValidUser.RBACConfiguredUser
version: 1.0
[ClickHouse] SHALL accept [Kerberos] authentication if user is configured to authenticate via [Kerberos] using SQL queries
```sql
CREATE USER my_user IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM'
```
or
```sql
CREATE USER my_user IDENTIFIED WITH kerberos
```
#### RQ.SRS-016.Kerberos.ValidUser.KerberosNotConfigured
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if username is valid but [ClickHouse] user is not configured to be authenticated using [Kerberos].
### Invalid User
#### RQ.SRS-016.Kerberos.InvalidUser
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if name of the principal attempting to authenticate does not translate to a valid [ClickHouse] username configured in `users.xml` or via SQL workflow.
#### RQ.SRS-016.Kerberos.InvalidUser.UserDeleted
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if [ClickHouse] user was removed from the database using an SQL query.
### Kerberos Not Available
#### RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidServerTicket
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if [ClickHouse] user is configured to be authenticated using [Kerberos] and [Kerberos] server is unavailable, but [ClickHouse] doesn't have a valid Kerberos ticket or the ticket is expired.
#### RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidClientTicket
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if [ClickHouse] user is configured to to be authenticated using [Kerberos] and [Kerberos] server is unavailable, but the client doesn't have a valid Kerberos ticket or the ticket is expired.
#### RQ.SRS-016.Kerberos.KerberosNotAvailable.ValidTickets
version: 1.0
[ClickHouse] SHALL accept [Kerberos] authentication if no [Kerberos] server is reachable, but [ClickHouse] is configured to use valid credentials and [ClickHouse] has already processed some valid kerberized request (so it was granted a ticket), and the client has a valid ticket as well.
### Kerberos Restarted
#### RQ.SRS-016.Kerberos.KerberosServerRestarted
version: 1.0
[ClickHouse] SHALL accept [Kerberos] authentication if [Kerberos] server was restarted.
### Performance
#### RQ.SRS-016.Kerberos.Performance
version: 1.0
[ClickHouse]'s performance for [Kerberos] authentication SHALL be comparable to regular authentication.
### Parallel Requests processing
#### RQ.SRS-016.Kerberos.Parallel
version: 1.0
[ClickHouse] SHALL support parallel authentication using [Kerberos].
#### RQ.SRS-016.Kerberos.Parallel.ValidRequests.KerberosAndNonKerberos
version: 1.0
[ClickHouse] SHALL support processing of simultaneous kerberized (for users configured to authenticate via [Kerberos]) and non-kerberized (for users configured to authenticate with any other means) requests.
#### RQ.SRS-016.Kerberos.Parallel.ValidRequests.SameCredentials
version: 1.0
[ClickHouse] SHALL support processing of simultaneously sent [Kerberos] requests under the same credentials.
#### RQ.SRS-016.Kerberos.Parallel.ValidRequests.DifferentCredentials
version: 1.0
[ClickHouse] SHALL support processing of simultaneously sent [Kerberos] requests under different credentials.
#### RQ.SRS-016.Kerberos.Parallel.ValidInvalid
version: 1.0
[ClickHouse] SHALL support parallel authentication of users using [Kerberos] server, some of which are valid and some invalid. Valid users' authentication should not be affected by invalid users' attempts.
#### RQ.SRS-016.Kerberos.Parallel.Deletion
version: 1.0
[ClickHouse] SHALL not crash when two or more [Kerberos] users are simultaneously deleting one another.
## References
* **ClickHouse:** https://clickhouse.tech
* **GitHub Repository:** https://github.com/ClickHouse/ClickHouse/blob/master/tests/testflows/kerberos/requirements/requirements.md
* **Revision History:** https://github.com/ClickHouse/ClickHouse/commits/master/tests/testflows/kerberos/requirements/requirements.md
* **Git:** https://git-scm.com/
* **Kerberos terminology:** https://web.mit.edu/kerberos/kfw-4.1/kfw-4.1/kfw-4.1-help/html/kerberos_terminology.htm
[Kerberos]: https://en.wikipedia.org/wiki/Kerberos_(protocol)
[SPNEGO]: https://en.wikipedia.org/wiki/SPNEGO
[ClickHouse]: https://clickhouse.tech
[GitHub]: https://gitlab.com
[GitHub Repository]: https://github.com/ClickHouse/ClickHouse/blob/master/tests/testflows/kerberos/requirements/requirements.md
[Revision History]: https://github.com/ClickHouse/ClickHouse/commits/master/tests/testflows/kerberos/requirements/requirements.md
[Git]: https://git-scm.com/
[Kerberos terminology]: https://web.mit.edu/kerberos/kfw-4.1/kfw-4.1/kfw-4.1-help/html/kerberos_terminology.htm

View File

@ -0,0 +1,800 @@
# These requirements were auto generated
# from software requirements specification (SRS)
# document by TestFlows v1.6.201216.1172002.
# Do not edit by hand but re-generate instead
# using 'tfs requirements generate' command.
from testflows.core import Specification
from testflows.core import Requirement
Heading = Specification.Heading
RQ_SRS_016_Kerberos = Requirement(
name='RQ.SRS-016.Kerberos',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL support user authentication using [Kerberos] server.\n'
'\n'
),
link=None,
level=3,
num='4.1.1')
RQ_SRS_016_Kerberos_Configuration_MultipleAuthMethods = Requirement(
name='RQ.SRS-016.Kerberos.Configuration.MultipleAuthMethods',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
"[ClickHouse] SHALL generate an exception and TERMINATE in case some user in `users.xml` has a `<kerberos>` section specified alongside with any other authentication method's section, e.g. `ldap`, `password`.\n"
'\n'
),
link=None,
level=3,
num='4.2.1')
RQ_SRS_016_Kerberos_Configuration_KerberosNotEnabled = Requirement(
name='RQ.SRS-016.Kerberos.Configuration.KerberosNotEnabled',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL reject [Kerberos] authentication in case user is properly configured for using Kerberos, but Kerberos itself is not enabled in `config.xml`. For example:\n'
'\n'
'```xml\n'
'<yandex>\n'
' <!- ... -->\n'
' <kerberos />\n'
'</yandex>\n'
'```\n'
'```xml\n'
'<yandex>\n'
' <!- ... -->\n'
' <kerberos>\n'
' <principal>HTTP/clickhouse.example.com@EXAMPLE.COM</principal>\n'
' </kerberos>\n'
'</yandex>\n'
'```\n'
'```xml\n'
'<yandex>\n'
' <!- ... -->\n'
' <kerberos>\n'
' <realm>EXAMPLE.COM</realm>\n'
' </kerberos>\n'
'</yandex>\n'
'```\n'
'\n'
),
link=None,
level=3,
num='4.2.2')
RQ_SRS_016_Kerberos_Configuration_MultipleKerberosSections = Requirement(
name='RQ.SRS-016.Kerberos.Configuration.MultipleKerberosSections',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL disable [Kerberos] and reject [Kerberos] authentication in case multiple `kerberos` sections are present in `config.xml`.\n'
'\n'
),
link=None,
level=3,
num='4.2.3')
RQ_SRS_016_Kerberos_Configuration_WrongUserRealm = Requirement(
name='RQ.SRS-016.Kerberos.Configuration.WrongUserRealm',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
"[ClickHouse] SHALL reject [Kerberos] authentication if user's realm specified in `users.xml` doesn't match the realm of the principal trying to authenticate.\n"
'\n'
),
link=None,
level=3,
num='4.2.4')
RQ_SRS_016_Kerberos_Configuration_PrincipalAndRealmSpecified = Requirement(
name='RQ.SRS-016.Kerberos.Configuration.PrincipalAndRealmSpecified',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL generate an exception and disable [Kerberos] in case both `realm` and `principal` sections are defined in `config.xml`.\n'
'\n'
),
link=None,
level=3,
num='4.2.5')
RQ_SRS_016_Kerberos_Configuration_MultiplePrincipalSections = Requirement(
name='RQ.SRS-016.Kerberos.Configuration.MultiplePrincipalSections',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL generate an exception and disable [Kerberos] in case multiple `principal` sections are specified inside `kerberos` section in `config.xml`.\n'
'\n'
),
link=None,
level=3,
num='4.2.6')
RQ_SRS_016_Kerberos_Configuration_MultipleRealmSections = Requirement(
name='RQ.SRS-016.Kerberos.Configuration.MultipleRealmSections',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL generate an exception and disable [Kerberos] in case multiple `realm` sections are specified inside `kerberos` section in `config.xml`.\n'
'\n'
),
link=None,
level=3,
num='4.2.7')
RQ_SRS_016_Kerberos_ValidUser_XMLConfiguredUser = Requirement(
name='RQ.SRS-016.Kerberos.ValidUser.XMLConfiguredUser',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL accept [Kerberos] authentication for a user that is configured in `users.xml` and has [Kerberos] enabled, i.e.:\n'
'\n'
'```xml\n'
'<yandex>\n'
' <!- ... -->\n'
' <users>\n'
' <!- ... -->\n'
' <my_user>\n'
' <!- ... -->\n'
' <kerberos>\n'
' <realm>EXAMPLE.COM</realm>\n'
' </kerberos>\n'
' </my_user>\n'
' </users>\n'
'</yandex>\n'
'```\n'
'\n'
),
link=None,
level=3,
num='4.3.1')
RQ_SRS_016_Kerberos_ValidUser_RBACConfiguredUser = Requirement(
name='RQ.SRS-016.Kerberos.ValidUser.RBACConfiguredUser',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL accept [Kerberos] authentication if user is configured to authenticate via [Kerberos] using SQL queries\n'
'\n'
'```sql\n'
"CREATE USER my_user IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM'\n"
'```\n'
'\n'
'or\n'
'\n'
'```sql\n'
'CREATE USER my_user IDENTIFIED WITH kerberos\n'
'```\n'
'\n'
),
link=None,
level=3,
num='4.3.2')
RQ_SRS_016_Kerberos_ValidUser_KerberosNotConfigured = Requirement(
name='RQ.SRS-016.Kerberos.ValidUser.KerberosNotConfigured',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL reject [Kerberos] authentication if username is valid but [ClickHouse] user is not configured to be authenticated using [Kerberos].\n'
'\n'
),
link=None,
level=3,
num='4.3.3')
RQ_SRS_016_Kerberos_InvalidUser = Requirement(
name='RQ.SRS-016.Kerberos.InvalidUser',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL reject [Kerberos] authentication if name of the principal attempting to authenticate does not translate to a valid [ClickHouse] username configured in `users.xml` or via SQL workflow.\n'
'\n'
),
link=None,
level=3,
num='4.4.1')
RQ_SRS_016_Kerberos_InvalidUser_UserDeleted = Requirement(
name='RQ.SRS-016.Kerberos.InvalidUser.UserDeleted',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL reject [Kerberos] authentication if [ClickHouse] user was removed from the database using an SQL query.\n'
'\n'
),
link=None,
level=3,
num='4.4.2')
RQ_SRS_016_Kerberos_KerberosNotAvailable_InvalidServerTicket = Requirement(
name='RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidServerTicket',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
"[ClickHouse] SHALL reject [Kerberos] authentication if [ClickHouse] user is configured to be authenticated using [Kerberos] and [Kerberos] server is unavailable, but [ClickHouse] doesn't have a valid Kerberos ticket or the ticket is expired.\n"
'\n'
),
link=None,
level=3,
num='4.5.1')
RQ_SRS_016_Kerberos_KerberosNotAvailable_InvalidClientTicket = Requirement(
name='RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidClientTicket',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
"[ClickHouse] SHALL reject [Kerberos] authentication if [ClickHouse] user is configured to to be authenticated using [Kerberos] and [Kerberos] server is unavailable, but the client doesn't have a valid Kerberos ticket or the ticket is expired.\n"
'\n'
),
link=None,
level=3,
num='4.5.2')
RQ_SRS_016_Kerberos_KerberosNotAvailable_ValidTickets = Requirement(
name='RQ.SRS-016.Kerberos.KerberosNotAvailable.ValidTickets',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL accept [Kerberos] authentication if no [Kerberos] server is reachable, but [ClickHouse] is configured to use valid credentials and [ClickHouse] has already processed some valid kerberized request (so it was granted a ticket), and the client has a valid ticket as well.\n'
'\n'
),
link=None,
level=3,
num='4.5.3')
RQ_SRS_016_Kerberos_KerberosServerRestarted = Requirement(
name='RQ.SRS-016.Kerberos.KerberosServerRestarted',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL accept [Kerberos] authentication if [Kerberos] server was restarted.\n'
'\n'
),
link=None,
level=3,
num='4.6.1')
RQ_SRS_016_Kerberos_Performance = Requirement(
name='RQ.SRS-016.Kerberos.Performance',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
"[ClickHouse]'s performance for [Kerberos] authentication SHALL be comparable to regular authentication.\n"
'\n'
),
link=None,
level=3,
num='4.7.1')
RQ_SRS_016_Kerberos_Parallel = Requirement(
name='RQ.SRS-016.Kerberos.Parallel',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL support parallel authentication using [Kerberos].\n'
'\n'
),
link=None,
level=3,
num='4.8.1')
RQ_SRS_016_Kerberos_Parallel_ValidRequests_KerberosAndNonKerberos = Requirement(
name='RQ.SRS-016.Kerberos.Parallel.ValidRequests.KerberosAndNonKerberos',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL support processing of simultaneous kerberized (for users configured to authenticate via [Kerberos]) and non-kerberized (for users configured to authenticate with any other means) requests.\n'
'\n'
),
link=None,
level=3,
num='4.8.2')
RQ_SRS_016_Kerberos_Parallel_ValidRequests_SameCredentials = Requirement(
name='RQ.SRS-016.Kerberos.Parallel.ValidRequests.SameCredentials',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL support processing of simultaneously sent [Kerberos] requests under the same credentials.\n'
'\n'
),
link=None,
level=3,
num='4.8.3')
RQ_SRS_016_Kerberos_Parallel_ValidRequests_DifferentCredentials = Requirement(
name='RQ.SRS-016.Kerberos.Parallel.ValidRequests.DifferentCredentials',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL support processing of simultaneously sent [Kerberos] requests under different credentials.\n'
'\n'
),
link=None,
level=3,
num='4.8.4')
RQ_SRS_016_Kerberos_Parallel_ValidInvalid = Requirement(
name='RQ.SRS-016.Kerberos.Parallel.ValidInvalid',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
"[ClickHouse] SHALL support parallel authentication of users using [Kerberos] server, some of which are valid and some invalid. Valid users' authentication should not be affected by invalid users' attempts.\n"
'\n'
),
link=None,
level=3,
num='4.8.5')
RQ_SRS_016_Kerberos_Parallel_Deletion = Requirement(
name='RQ.SRS-016.Kerberos.Parallel.Deletion',
version='1.0',
priority=None,
group=None,
type=None,
uid=None,
description=(
'[ClickHouse] SHALL not crash when two or more [Kerberos] users are simultaneously deleting one another.\n'
'\n'
),
link=None,
level=3,
num='4.8.6')
QA_SRS016_ClickHouse_Kerberos_Authentication = Specification(
name='QA-SRS016 ClickHouse Kerberos Authentication',
description=None,
author='Andrey Zvonov',
date='December 14, 2020',
status='-',
approved_by='-',
approved_date='-',
approved_version='-',
version=None,
group=None,
type=None,
link=None,
uid=None,
parent=None,
children=None,
headings=(
Heading(name='Revision History', level=1, num='1'),
Heading(name='Introduction', level=1, num='2'),
Heading(name='Terminology', level=1, num='3'),
Heading(name='Requirements', level=1, num='4'),
Heading(name='Generic', level=2, num='4.1'),
Heading(name='RQ.SRS-016.Kerberos', level=3, num='4.1.1'),
Heading(name='Configuration', level=2, num='4.2'),
Heading(name='RQ.SRS-016.Kerberos.Configuration.MultipleAuthMethods', level=3, num='4.2.1'),
Heading(name='RQ.SRS-016.Kerberos.Configuration.KerberosNotEnabled', level=3, num='4.2.2'),
Heading(name='RQ.SRS-016.Kerberos.Configuration.MultipleKerberosSections', level=3, num='4.2.3'),
Heading(name='RQ.SRS-016.Kerberos.Configuration.WrongUserRealm', level=3, num='4.2.4'),
Heading(name='RQ.SRS-016.Kerberos.Configuration.PrincipalAndRealmSpecified', level=3, num='4.2.5'),
Heading(name='RQ.SRS-016.Kerberos.Configuration.MultiplePrincipalSections', level=3, num='4.2.6'),
Heading(name='RQ.SRS-016.Kerberos.Configuration.MultipleRealmSections', level=3, num='4.2.7'),
Heading(name='Valid User', level=2, num='4.3'),
Heading(name='RQ.SRS-016.Kerberos.ValidUser.XMLConfiguredUser', level=3, num='4.3.1'),
Heading(name='RQ.SRS-016.Kerberos.ValidUser.RBACConfiguredUser', level=3, num='4.3.2'),
Heading(name='RQ.SRS-016.Kerberos.ValidUser.KerberosNotConfigured', level=3, num='4.3.3'),
Heading(name='Invalid User', level=2, num='4.4'),
Heading(name='RQ.SRS-016.Kerberos.InvalidUser', level=3, num='4.4.1'),
Heading(name='RQ.SRS-016.Kerberos.InvalidUser.UserDeleted', level=3, num='4.4.2'),
Heading(name='Kerberos Not Available', level=2, num='4.5'),
Heading(name='RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidServerTicket', level=3, num='4.5.1'),
Heading(name='RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidClientTicket', level=3, num='4.5.2'),
Heading(name='RQ.SRS-016.Kerberos.KerberosNotAvailable.ValidTickets', level=3, num='4.5.3'),
Heading(name='Kerberos Restarted', level=2, num='4.6'),
Heading(name='RQ.SRS-016.Kerberos.KerberosServerRestarted', level=3, num='4.6.1'),
Heading(name='Performance', level=2, num='4.7'),
Heading(name='RQ.SRS-016.Kerberos.Performance', level=3, num='4.7.1'),
Heading(name='Parallel Requests processing', level=2, num='4.8'),
Heading(name='RQ.SRS-016.Kerberos.Parallel', level=3, num='4.8.1'),
Heading(name='RQ.SRS-016.Kerberos.Parallel.ValidRequests.KerberosAndNonKerberos', level=3, num='4.8.2'),
Heading(name='RQ.SRS-016.Kerberos.Parallel.ValidRequests.SameCredentials', level=3, num='4.8.3'),
Heading(name='RQ.SRS-016.Kerberos.Parallel.ValidRequests.DifferentCredentials', level=3, num='4.8.4'),
Heading(name='RQ.SRS-016.Kerberos.Parallel.ValidInvalid', level=3, num='4.8.5'),
Heading(name='RQ.SRS-016.Kerberos.Parallel.Deletion', level=3, num='4.8.6'),
Heading(name='References', level=1, num='5'),
),
requirements=(
RQ_SRS_016_Kerberos,
RQ_SRS_016_Kerberos_Configuration_MultipleAuthMethods,
RQ_SRS_016_Kerberos_Configuration_KerberosNotEnabled,
RQ_SRS_016_Kerberos_Configuration_MultipleKerberosSections,
RQ_SRS_016_Kerberos_Configuration_WrongUserRealm,
RQ_SRS_016_Kerberos_Configuration_PrincipalAndRealmSpecified,
RQ_SRS_016_Kerberos_Configuration_MultiplePrincipalSections,
RQ_SRS_016_Kerberos_Configuration_MultipleRealmSections,
RQ_SRS_016_Kerberos_ValidUser_XMLConfiguredUser,
RQ_SRS_016_Kerberos_ValidUser_RBACConfiguredUser,
RQ_SRS_016_Kerberos_ValidUser_KerberosNotConfigured,
RQ_SRS_016_Kerberos_InvalidUser,
RQ_SRS_016_Kerberos_InvalidUser_UserDeleted,
RQ_SRS_016_Kerberos_KerberosNotAvailable_InvalidServerTicket,
RQ_SRS_016_Kerberos_KerberosNotAvailable_InvalidClientTicket,
RQ_SRS_016_Kerberos_KerberosNotAvailable_ValidTickets,
RQ_SRS_016_Kerberos_KerberosServerRestarted,
RQ_SRS_016_Kerberos_Performance,
RQ_SRS_016_Kerberos_Parallel,
RQ_SRS_016_Kerberos_Parallel_ValidRequests_KerberosAndNonKerberos,
RQ_SRS_016_Kerberos_Parallel_ValidRequests_SameCredentials,
RQ_SRS_016_Kerberos_Parallel_ValidRequests_DifferentCredentials,
RQ_SRS_016_Kerberos_Parallel_ValidInvalid,
RQ_SRS_016_Kerberos_Parallel_Deletion,
),
content='''
# QA-SRS016 ClickHouse Kerberos Authentication
# Software Requirements Specification
(c) 2020 Altinity LTD. All Rights Reserved.
**Document status:** Confidential
**Author:** Andrey Zvonov
**Date:** December 14, 2020
## Approval
**Status:** -
**Version:** -
**Approved by:** -
**Date:** -
## Table of Contents
* 1 [Revision History](#revision-history)
* 2 [Introduction](#introduction)
* 3 [Terminology](#terminology)
* 4 [Requirements](#requirements)
* 4.1 [Generic](#generic)
* 4.1.1 [RQ.SRS-016.Kerberos](#rqsrs-016kerberos)
* 4.2 [Configuration](#configuration)
* 4.2.1 [RQ.SRS-016.Kerberos.Configuration.MultipleAuthMethods](#rqsrs-016kerberosconfigurationmultipleauthmethods)
* 4.2.2 [RQ.SRS-016.Kerberos.Configuration.KerberosNotEnabled](#rqsrs-016kerberosconfigurationkerberosnotenabled)
* 4.2.3 [RQ.SRS-016.Kerberos.Configuration.MultipleKerberosSections](#rqsrs-016kerberosconfigurationmultiplekerberossections)
* 4.2.4 [RQ.SRS-016.Kerberos.Configuration.WrongUserRealm](#rqsrs-016kerberosconfigurationwronguserrealm)
* 4.2.5 [RQ.SRS-016.Kerberos.Configuration.PrincipalAndRealmSpecified](#rqsrs-016kerberosconfigurationprincipalandrealmspecified)
* 4.2.6 [RQ.SRS-016.Kerberos.Configuration.MultiplePrincipalSections](#rqsrs-016kerberosconfigurationmultipleprincipalsections)
* 4.2.7 [RQ.SRS-016.Kerberos.Configuration.MultipleRealmSections](#rqsrs-016kerberosconfigurationmultiplerealmsections)
* 4.3 [Valid User](#valid-user)
* 4.3.1 [RQ.SRS-016.Kerberos.ValidUser.XMLConfiguredUser](#rqsrs-016kerberosvaliduserxmlconfigureduser)
* 4.3.2 [RQ.SRS-016.Kerberos.ValidUser.RBACConfiguredUser](#rqsrs-016kerberosvaliduserrbacconfigureduser)
* 4.3.3 [RQ.SRS-016.Kerberos.ValidUser.KerberosNotConfigured](#rqsrs-016kerberosvaliduserkerberosnotconfigured)
* 4.4 [Invalid User](#invalid-user)
* 4.4.1 [RQ.SRS-016.Kerberos.InvalidUser](#rqsrs-016kerberosinvaliduser)
* 4.4.2 [RQ.SRS-016.Kerberos.InvalidUser.UserDeleted](#rqsrs-016kerberosinvaliduseruserdeleted)
* 4.5 [Kerberos Not Available](#kerberos-not-available)
* 4.5.1 [RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidServerTicket](#rqsrs-016kerberoskerberosnotavailableinvalidserverticket)
* 4.5.2 [RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidClientTicket](#rqsrs-016kerberoskerberosnotavailableinvalidclientticket)
* 4.5.3 [RQ.SRS-016.Kerberos.KerberosNotAvailable.ValidTickets](#rqsrs-016kerberoskerberosnotavailablevalidtickets)
* 4.6 [Kerberos Restarted](#kerberos-restarted)
* 4.6.1 [RQ.SRS-016.Kerberos.KerberosServerRestarted](#rqsrs-016kerberoskerberosserverrestarted)
* 4.7 [Performance](#performance)
* 4.7.1 [RQ.SRS-016.Kerberos.Performance](#rqsrs-016kerberosperformance)
* 4.8 [Parallel Requests processing](#parallel-requests-processing)
* 4.8.1 [RQ.SRS-016.Kerberos.Parallel](#rqsrs-016kerberosparallel)
* 4.8.2 [RQ.SRS-016.Kerberos.Parallel.ValidRequests.KerberosAndNonKerberos](#rqsrs-016kerberosparallelvalidrequestskerberosandnonkerberos)
* 4.8.3 [RQ.SRS-016.Kerberos.Parallel.ValidRequests.SameCredentials](#rqsrs-016kerberosparallelvalidrequestssamecredentials)
* 4.8.4 [RQ.SRS-016.Kerberos.Parallel.ValidRequests.DifferentCredentials](#rqsrs-016kerberosparallelvalidrequestsdifferentcredentials)
* 4.8.5 [RQ.SRS-016.Kerberos.Parallel.ValidInvalid](#rqsrs-016kerberosparallelvalidinvalid)
* 4.8.6 [RQ.SRS-016.Kerberos.Parallel.Deletion](#rqsrs-016kerberosparalleldeletion)
* 5 [References](#references)
## Revision History
This document is stored in an electronic form using [Git] source control management software
hosted in a [GitLab Repository].
All the updates are tracked using the [Git]'s [Revision History].
## Introduction
This document specifies the behavior for authenticating existing users using [Kerberos] authentication protocol.
Existing [ClickHouse] users, that are properly configured, have an ability to authenticate using [Kerberos]. Kerberos authentication is only supported for HTTP requests, and users configured to authenticate via Kerberos cannot be authenticated by any other means of authentication.
In order to use Kerberos authentication, Kerberos needs to be properly configured in the environment: Kerberos server must be present and user's and server's credentials must be set up. Configuring the Kerberos environment is outside the scope of this document.
## Terminology
* **Principal** -
A unique identity that uses [Kerberos].
* **Realm** -
A logical group of resources and identities that use [Kerberos].
* **Ticket** -
An encrypted block of data that authenticates principal.
* **Credentials** -
A Kerberos ticket and a session key.
* **Kerberized request** -
A HTTP query to ClickHouse server, which uses GSS [SPNEGO] and [Kerberos] to authenticate client.
* **Unkerberized request** -
A HTTP query to ClickHouse server, which uses any other mean of authentication than GSS [SPNEGO] or [Kerberos].
For a more detailed descriprion, visit [Kerberos terminology].
## Requirements
### Generic
#### RQ.SRS-016.Kerberos
version: 1.0
[ClickHouse] SHALL support user authentication using [Kerberos] server.
### Configuration
#### RQ.SRS-016.Kerberos.Configuration.MultipleAuthMethods
version: 1.0
[ClickHouse] SHALL generate an exception and TERMINATE in case some user in `users.xml` has a `<kerberos>` section specified alongside with any other authentication method's section, e.g. `ldap`, `password`.
#### RQ.SRS-016.Kerberos.Configuration.KerberosNotEnabled
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication in case user is properly configured for using Kerberos, but Kerberos itself is not enabled in `config.xml`. For example:
```xml
<yandex>
<!- ... -->
<kerberos />
</yandex>
```
```xml
<yandex>
<!- ... -->
<kerberos>
<principal>HTTP/clickhouse.example.com@EXAMPLE.COM</principal>
</kerberos>
</yandex>
```
```xml
<yandex>
<!- ... -->
<kerberos>
<realm>EXAMPLE.COM</realm>
</kerberos>
</yandex>
```
#### RQ.SRS-016.Kerberos.Configuration.MultipleKerberosSections
version: 1.0
[ClickHouse] SHALL disable [Kerberos] and reject [Kerberos] authentication in case multiple `kerberos` sections are present in `config.xml`.
#### RQ.SRS-016.Kerberos.Configuration.WrongUserRealm
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if user's realm specified in `users.xml` doesn't match the realm of the principal trying to authenticate.
#### RQ.SRS-016.Kerberos.Configuration.PrincipalAndRealmSpecified
version: 1.0
[ClickHouse] SHALL generate an exception and disable [Kerberos] in case both `realm` and `principal` sections are defined in `config.xml`.
#### RQ.SRS-016.Kerberos.Configuration.MultiplePrincipalSections
version: 1.0
[ClickHouse] SHALL generate an exception and disable [Kerberos] in case multiple `principal` sections are specified inside `kerberos` section in `config.xml`.
#### RQ.SRS-016.Kerberos.Configuration.MultipleRealmSections
version: 1.0
[ClickHouse] SHALL generate an exception and disable [Kerberos] in case multiple `realm` sections are specified inside `kerberos` section in `config.xml`.
### Valid User
#### RQ.SRS-016.Kerberos.ValidUser.XMLConfiguredUser
version: 1.0
[ClickHouse] SHALL accept [Kerberos] authentication for a user that is configured in `users.xml` and has [Kerberos] enabled, i.e.:
```xml
<yandex>
<!- ... -->
<users>
<!- ... -->
<my_user>
<!- ... -->
<kerberos>
<realm>EXAMPLE.COM</realm>
</kerberos>
</my_user>
</users>
</yandex>
```
#### RQ.SRS-016.Kerberos.ValidUser.RBACConfiguredUser
version: 1.0
[ClickHouse] SHALL accept [Kerberos] authentication if user is configured to authenticate via [Kerberos] using SQL queries
```sql
CREATE USER my_user IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM'
```
or
```sql
CREATE USER my_user IDENTIFIED WITH kerberos
```
#### RQ.SRS-016.Kerberos.ValidUser.KerberosNotConfigured
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if username is valid but [ClickHouse] user is not configured to be authenticated using [Kerberos].
### Invalid User
#### RQ.SRS-016.Kerberos.InvalidUser
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if name of the principal attempting to authenticate does not translate to a valid [ClickHouse] username configured in `users.xml` or via SQL workflow.
#### RQ.SRS-016.Kerberos.InvalidUser.UserDeleted
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if [ClickHouse] user was removed from the database using an SQL query.
### Kerberos Not Available
#### RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidServerTicket
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if [ClickHouse] user is configured to be authenticated using [Kerberos] and [Kerberos] server is unavailable, but [ClickHouse] doesn't have a valid Kerberos ticket or the ticket is expired.
#### RQ.SRS-016.Kerberos.KerberosNotAvailable.InvalidClientTicket
version: 1.0
[ClickHouse] SHALL reject [Kerberos] authentication if [ClickHouse] user is configured to to be authenticated using [Kerberos] and [Kerberos] server is unavailable, but the client doesn't have a valid Kerberos ticket or the ticket is expired.
#### RQ.SRS-016.Kerberos.KerberosNotAvailable.ValidTickets
version: 1.0
[ClickHouse] SHALL accept [Kerberos] authentication if no [Kerberos] server is reachable, but [ClickHouse] is configured to use valid credentials and [ClickHouse] has already processed some valid kerberized request (so it was granted a ticket), and the client has a valid ticket as well.
### Kerberos Restarted
#### RQ.SRS-016.Kerberos.KerberosServerRestarted
version: 1.0
[ClickHouse] SHALL accept [Kerberos] authentication if [Kerberos] server was restarted.
### Performance
#### RQ.SRS-016.Kerberos.Performance
version: 1.0
[ClickHouse]'s performance for [Kerberos] authentication SHALL be comparable to regular authentication.
### Parallel Requests processing
#### RQ.SRS-016.Kerberos.Parallel
version: 1.0
[ClickHouse] SHALL support parallel authentication using [Kerberos].
#### RQ.SRS-016.Kerberos.Parallel.ValidRequests.KerberosAndNonKerberos
version: 1.0
[ClickHouse] SHALL support processing of simultaneous kerberized (for users configured to authenticate via [Kerberos]) and non-kerberized (for users configured to authenticate with any other means) requests.
#### RQ.SRS-016.Kerberos.Parallel.ValidRequests.SameCredentials
version: 1.0
[ClickHouse] SHALL support processing of simultaneously sent [Kerberos] requests under the same credentials.
#### RQ.SRS-016.Kerberos.Parallel.ValidRequests.DifferentCredentials
version: 1.0
[ClickHouse] SHALL support processing of simultaneously sent [Kerberos] requests under different credentials.
#### RQ.SRS-016.Kerberos.Parallel.ValidInvalid
version: 1.0
[ClickHouse] SHALL support parallel authentication of users using [Kerberos] server, some of which are valid and some invalid. Valid users' authentication should not be affected by invalid users' attempts.
#### RQ.SRS-016.Kerberos.Parallel.Deletion
version: 1.0
[ClickHouse] SHALL not crash when two or more [Kerberos] users are simultaneously deleting one another.
## References
* **ClickHouse:** https://clickhouse.tech
* **Gitlab Repository:** https://gitlab.com/altinity-qa/documents/qa-srs016-clickhouse-kerberos-authentication/-/blob/master/QA_SRS016_ClickHouse_Kerberos_Authentication.md
* **Revision History:** https://gitlab.com/altinity-qa/documents/qa-srs016-clickhouse-kerberos-authentication/-/commits/master/QA_SRS016_ClickHouse_Kerberos_Authentication.md
* **Git:** https://git-scm.com/
* **Kerberos terminology:** https://web.mit.edu/kerberos/kfw-4.1/kfw-4.1/kfw-4.1-help/html/kerberos_terminology.htm
[Kerberos]: https://en.wikipedia.org/wiki/Kerberos_(protocol)
[SPNEGO]: https://en.wikipedia.org/wiki/SPNEGO
[ClickHouse]: https://clickhouse.tech
[GitLab]: https://gitlab.com
[GitLab Repository]: https://gitlab.com/altinity-qa/documents/qa-srs016-clickhouse-kerberos-authentication/-/blob/master/QA_SRS016_ClickHouse_Kerberos_Authentication.md
[Revision History]: https://gitlab.com/altinity-qa/documents/qa-srs016-clickhouse-kerberos-authentication/-/commits/master/QA_SRS016_ClickHouse_Kerberos_Authentication.md
[Git]: https://git-scm.com/
[Kerberos terminology]: https://web.mit.edu/kerberos/kfw-4.1/kfw-4.1/kfw-4.1-help/html/kerberos_terminology.htm
''')

View File

@ -0,0 +1,225 @@
from testflows.core import *
from testflows.asserts import error
from contextlib import contextmanager
import xml.etree.ElementTree as xmltree
import time
import uuid
def getuid():
return str(uuid.uuid1()).replace('-', '_')
def xml_append(root, tag, text=Null):
element = xmltree.Element(tag)
if text:
element.text = text
root.append(element)
def xml_write(data, filename):
strdata = xmltree.tostring(data)
with open(filename, "wb") as f:
f.write(strdata)
def xml_parse_file(filename):
return xmltree.parse(filename).getroot()
def create_default_config(filename):
contents = ""
if "kerberos_users.xml" in filename:
contents = "<yandex><users><kerberos_user><kerberos><realm>EXAMPLE.COM" \
"</realm></kerberos></kerberos_user></users></yandex>"
elif "kerberos.xml" in filename:
contents = "<yandex><kerberos><realm>EXAMPLE.COM</realm></kerberos></yandex>"
with open(filename, "w") as f:
f.write(contents)
def test_select_query(node, krb_auth=True, req="SELECT currentUser()"):
""" Helper forming a HTTP query to ClickHouse server
"""
if krb_auth:
return f"echo '{req}' | curl --negotiate -u : 'http://{node.name}:8123/' --data-binary @-"
else:
return f"echo '{req}' | curl 'http://{node.name}:8123/' --data-binary @-"
@TestStep(Given)
def kinit_no_keytab(self, node, principal="kerberos_user", lifetime_option="-l 10:00"):
""" Helper for obtaining Kerberos ticket for client
"""
try:
node.cmd("echo pwd | kinit admin/admin")
node.cmd(f"kadmin -w pwd -q \"add_principal -pw pwd {principal}\"")
node.cmd(f"echo pwd | kinit {lifetime_option} {principal}")
yield
finally:
node.cmd("kdestroy")
@TestStep(Given)
def create_server_principal(self, node):
""" Helper for obtaining Kerberos ticket for server
"""
try:
node.cmd("echo pwd | kinit admin/admin")
node.cmd(f"kadmin -w pwd -q \"add_principal -randkey HTTP/docker-compose_{node.name}_1.docker-compose_default\"")
node.cmd(f"kadmin -w pwd -q \"ktadd -k /etc/krb5.keytab HTTP/docker-compose_{node.name}_1.docker-compose_default\"")
yield
finally:
node.cmd("kdestroy")
node.cmd("rm /etc/krb5.keytab")
@TestStep(Given)
def save_file_state(self, node, filename):
""" Save current file and then restore it, restarting the node
"""
try:
with When("I save file state"):
with open(filename, 'r') as f:
a = f.read()
yield
finally:
with Finally("I restore initial state"):
with open(filename, 'w') as f:
f.write(a)
node.restart()
@TestStep(Given)
def temp_erase(self, node, filename=None):
""" Temporary erasing config file and restarting the node
"""
if filename is None:
filename = f"kerberos/configs/{node.name}/config.d/kerberos.xml"
with When("I save file state"):
with open(filename, 'r') as f:
a = f.read()
try:
with Then("I overwrite file to be dummy"):
with open(filename, 'w') as f:
f.write("<yandex></yandex>\n")
node.restart()
yield
finally:
with Finally("I restore initial file state"):
with open(filename, 'w') as f:
f.write(a)
node.restart()
def restart(node, config_path, safe=False, timeout=60):
"""Restart ClickHouse server and wait for config to be reloaded.
"""
filename = '/etc/clickhouse-server/config.xml' if 'config.d' in config_path else '/etc/clickhouse-server/users.xml'
with When("I restart ClickHouse server node"):
with node.cluster.shell(node.name) as bash:
bash.expect(bash.prompt)
with By("closing terminal to the node to be restarted"):
bash.close()
with And("getting current log size"):
logsize = \
node.command("stat --format=%s /var/log/clickhouse-server/clickhouse-server.log").output.split(" ")[0].strip()
with And("restarting ClickHouse server"):
node.restart(safe=safe)
with Then("tailing the log file from using previous log size as the offset"):
bash.prompt = bash.__class__.prompt
bash.open()
bash.send(f"tail -c +{logsize} -f /var/log/clickhouse-server/clickhouse-server.log")
with And("waiting for config reload message in the log file"):
bash.expect(
f"ConfigReloader: Loaded config '{filename}', performed update on configuration",
timeout=timeout)
@TestStep
def check_wrong_config(self, node, client, config_path, modify_file, log_error="", output="",
tail=120, timeout=60, healthy_on_restart=True):
"""Check that ClickHouse errors when trying to load invalid configuration file.
"""
preprocessed_name = "config.xml" if "config.d" in config_path else "users.xml"
full_config_path = "/etc/clickhouse-server/config.d/kerberos.xml" if "config.d" in config_path else "/etc/clickhouse-server/users.d/kerberos-users.xml"
uid = getuid()
try:
with Given("I save config file to restore it later"):
with open(config_path, 'r') as f:
initial_contents = f.read()
with And("I prepare the error log by writing empty lines into it"):
node.command("echo -e \"%s\" > /var/log/clickhouse-server/clickhouse-server.err.log" % ("-\\n" * tail))
with When("I modify xml file"):
root = xml_parse_file(config_path)
root = modify_file(root)
root.append(xmltree.fromstring(f"<comment>{uid}</comment>"))
config_contents = xmltree.tostring(root, encoding='utf8', method='xml').decode('utf-8')
command = f"cat <<HEREDOC > {full_config_path}\n{config_contents}\nHEREDOC"
node.command(command, steps=False, exitcode=0)
# time.sleep(1)
with Then(f"{preprocessed_name} should be updated", description=f"timeout {timeout}"):
started = time.time()
command = f"cat /var/lib/clickhouse/preprocessed_configs/{preprocessed_name} | grep {uid} > /dev/null"
while time.time() - started < timeout:
exitcode = node.command(command, steps=False).exitcode
if exitcode == 0:
break
time.sleep(1)
assert exitcode == 0, error()
with When("I restart ClickHouse to apply the config changes"):
if output:
node.restart(safe=False, wait_healthy=True)
else:
node.restart(safe=False, wait_healthy=False)
if output != "":
with Then(f"check {output} is in output"):
time.sleep(5)
started = time.time()
while time.time() - started < timeout:
kinit_no_keytab(node=client)
create_server_principal(node=node)
r = client.cmd(test_select_query(node=node), no_checks=True)
if output in r.output:
assert True, error()
break
time.sleep(1)
else:
assert False, error()
finally:
with Finally("I restore original config"):
with By("restoring the (correct) config file"):
with open(config_path, 'w') as f:
f.write(initial_contents)
with And("restarting the node"):
node.restart(safe=False)
if log_error != "":
with Then("error log should contain the expected error message"):
started = time.time()
command = f"tail -n {tail} /var/log/clickhouse-server/clickhouse-server.err.log | grep \"{log_error}\""
while time.time() - started < timeout:
exitcode = node.command(command, steps=False).exitcode
if exitcode == 0:
break
time.sleep(1)
assert exitcode == 0, error()

View File

@ -0,0 +1,163 @@
from testflows.core import *
from kerberos.tests.common import *
from kerberos.requirements.requirements import *
import time
import datetime
import itertools
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Configuration_KerberosNotEnabled("1.0")
)
def kerberos_not_enabled(self):
"""ClickHouse SHALL reject Kerberos authentication if user is properly configured for Kerberos,
but Kerberos itself is not enabled in config.xml.
"""
ch_nodes = self.context.ch_nodes
config_path = f"kerberos/configs/{ch_nodes[0].name}/config.d/kerberos.xml"
def modify_file(root):
return xmltree.fromstring("<yandex></yandex>")
check_wrong_config(node=ch_nodes[0], client=ch_nodes[2], config_path=config_path, modify_file=modify_file,
output="Kerberos is not enabled")
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Configuration_MultipleKerberosSections("1.0")
)
def multiple_kerberos(self):
"""ClickHouse SHALL disable Kerberos authentication if more than one kerberos sections specified in config.xml.
"""
ch_nodes = self.context.ch_nodes
config_path = f"kerberos/configs/{ch_nodes[0].name}/config.d/kerberos.xml"
def modify_file(root):
second_section = "<kerberos><realm>EXAM.COM</realm></kerberos>"
root.append(xmltree.fromstring(second_section))
return root
check_wrong_config(node=ch_nodes[0], client=ch_nodes[2], config_path=config_path, modify_file=modify_file,
log_error="Multiple kerberos sections are not allowed", healthy_on_restart=False)
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Configuration_WrongUserRealm("1.0")
)
def wrong_user_realm(self):
"""ClickHouse SHALL reject Kerberos authentication if user's realm specified in users.xml
doesn't match the realm of the principal trying to authenticate.
"""
ch_nodes = self.context.ch_nodes
config_path = f"kerberos/configs/{ch_nodes[0].name}/users.d/kerberos-users.xml"
def modify_file(root):
krb = root.find('users').find('kerberos_user')
krb.find('kerberos').find('realm').text = "OTHER.COM"
return root
check_wrong_config(node=ch_nodes[0], client=ch_nodes[2], config_path=config_path, modify_file=modify_file,
output="Authentication failed")
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Configuration_MultipleAuthMethods("1.0")
)
def multiple_auth_methods(self):
"""ClickHouse SHALL reject Kerberos authentication if other
auth method is specified for user alongside with Kerberos.
"""
ch_nodes = self.context.ch_nodes
config_path = f"kerberos/configs/{ch_nodes[0].name}/users.d/kerberos-users.xml"
def modify_file(root):
krb = root.find('users').find('kerberos_user')
xml_append(krb, 'password', 'qwerty')
return root
check_wrong_config(node=ch_nodes[0], client=ch_nodes[2], config_path=config_path, modify_file=modify_file,
log_error="More than one field of", healthy_on_restart=False)
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Configuration_PrincipalAndRealmSpecified("1.0")
)
def principal_and_realm_specified(self):
"""ClickHouse SHALL drop an exception if both realm and principal fields are specified in config.xml.
"""
ch_nodes = self.context.ch_nodes
config_path = f"kerberos/configs/{ch_nodes[0].name}/config.d/kerberos.xml"
def modify_file(root):
krb = root.find('kerberos')
xml_append(krb, 'principal', 'HTTP/srv1@EXAMPLE.COM')
return root
check_wrong_config(node=ch_nodes[0], client=ch_nodes[2], config_path=config_path, modify_file=modify_file,
log_error="Realm and principal name cannot be specified simultaneously",
output="Kerberos is not enabled")
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Configuration_MultipleRealmSections("1.0")
)
def multiple_realm(self):
"""ClickHouse SHALL throw an exception and disable Kerberos if more than one realm is specified in config.xml.
"""
ch_nodes = self.context.ch_nodes
config_path = f"kerberos/configs/{ch_nodes[0].name}/config.d/kerberos.xml"
def modify_file(root):
krb = root.find('kerberos')
xml_append(krb, 'realm', 'EXAM.COM')
return root
check_wrong_config(node=ch_nodes[0], client=ch_nodes[2], config_path=config_path, modify_file=modify_file,
log_error="Multiple realm sections are not allowed")
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Configuration_MultiplePrincipalSections("1.0")
)
def multiple_principal(self):
"""ClickHouse SHALL throw an exception and disable Kerberos if more than one principal is specified in config.xml.
"""
ch_nodes = self.context.ch_nodes
config_path = f"kerberos/configs/{ch_nodes[0].name}/config.d/kerberos.xml"
def modify_file(root):
krb = root.find('kerberos')
krb.remove(krb.find('realm'))
xml_append(krb, 'principal', 'HTTP/s1@EXAMPLE.COM')
xml_append(krb, 'principal', 'HTTP/s2@EXAMPLE.COM')
return root
check_wrong_config(node=ch_nodes[0], client=ch_nodes[2], config_path=config_path, modify_file=modify_file,
log_error="Multiple principal sections are not allowed")
@TestFeature
def config(self):
"""Perform ClickHouse Kerberos authentication testing for incorrect configuration files
"""
self.context.ch_nodes = [self.context.cluster.node(f"clickhouse{i}") for i in range(1, 4)]
self.context.krb_server = self.context.cluster.node("kerberos")
self.context.clients = [self.context.cluster.node(f"krb-client{i}") for i in range(1, 6)]
for scenario in loads(current_module(), Scenario, Suite):
Scenario(run=scenario, flags=TE)

View File

@ -0,0 +1,332 @@
from testflows.core import *
from kerberos.tests.common import *
from kerberos.requirements.requirements import *
import time
import datetime
import itertools
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_ValidUser_XMLConfiguredUser("1.0")
)
def xml_configured_user(self):
"""ClickHouse SHALL accept Kerberos authentication for valid XML-configured user
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for client"):
kinit_no_keytab(node=ch_nodes[2])
with And("kinit for server"):
create_server_principal(node=ch_nodes[0])
with When("I attempt to authenticate"):
r = ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]))
with Then(f"I expect 'kerberos_user'"):
assert r.output == "kerberos_user", error()
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_ValidUser_RBACConfiguredUser("1.0")
)
def rbac_configured_user(self):
"""ClickHouse SHALL accept Kerberos authentication for valid RBAC-configured user
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for client"):
kinit_no_keytab(node=ch_nodes[2], principal="krb_rbac")
with And("kinit for server"):
create_server_principal(node=ch_nodes[0])
with When("I create a RBAC user"):
ch_nodes[0].query("CREATE USER krb_rbac IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM'")
with When("I attempt to authenticate"):
r = ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]))
with Then("I restore server original state"):
ch_nodes[0].query("DROP USER krb_rbac")
with Finally("I expect 'krb_rbac'"):
assert r.output == "krb_rbac", error()
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_KerberosNotAvailable_InvalidServerTicket("1.0")
)
def invalid_server_ticket(self):
"""ClickHouse SHALL reject Kerberos authentication no Kerberos server is reachable
and CH-server has no valid ticket (or the existing ticket is outdated).
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for client"):
kinit_no_keytab(node=ch_nodes[2])
with And("setting up server principal"):
create_server_principal(node=ch_nodes[0])
with And("I kill kerberos-server"):
self.context.krb_server.stop()
with When("I attempt to authenticate as kerberos_user"):
r = ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]))
with Then("I start kerberos server again"):
self.context.krb_server.start()
ch_nodes[2].cmd("kdestroy")
while True:
kinit_no_keytab(node=ch_nodes[2])
if ch_nodes[2].cmd(test_select_query(node=ch_nodes[0])).output == "kerberos_user":
break
ch_nodes[2].cmd("kdestroy")
with And("I expect the user to be default"):
assert r.output == "default", error()
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_KerberosNotAvailable_InvalidClientTicket("1.0")
)
def invalid_client_ticket(self):
"""ClickHouse SHALL reject Kerberos authentication no Kerberos server is reachable
and client has no valid ticket (or the existing ticket is outdated).
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for client"):
kinit_no_keytab(node=ch_nodes[2], lifetime_option="-l 00:00:05")
with And("setting up server principal"):
create_server_principal(node=ch_nodes[0])
with And("I kill kerberos-server"):
self.context.krb_server.stop()
with And("I wait until client ticket is expired"):
time.sleep(10)
with When("I attempt to authenticate as kerberos_user"):
r = ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]))
with Then("I expect the user to be default"):
assert r.output == "default", error()
with Finally("I start kerberos server again"):
self.context.krb_server.start()
ch_nodes[2].cmd("kdestroy")
while True:
kinit_no_keytab(node=ch_nodes[2])
if ch_nodes[2].cmd(test_select_query(node=ch_nodes[0])).output == "kerberos_user":
break
ch_nodes[2].cmd("kdestroy")
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_KerberosNotAvailable_ValidTickets("1.0")
)
def kerberos_unreachable_valid_tickets(self):
"""ClickHouse SHALL accept Kerberos authentication if no Kerberos server is reachable
but both CH-server and client have valid tickets.
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for client"):
kinit_no_keytab(node=ch_nodes[2])
with And("setting up server principal"):
create_server_principal(node=ch_nodes[0])
with And("make sure server obtained ticket"):
ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]))
with And("I kill kerberos-server"):
self.context.krb_server.stop()
with When("I attempt to authenticate as kerberos_user"):
r = ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]))
with Then("I expect the user to be default"):
assert r.output == "kerberos_user", error()
with Finally("I start kerberos server again"):
self.context.krb_server.start()
ch_nodes[2].cmd("kdestroy")
while True:
kinit_no_keytab(node=ch_nodes[2])
if ch_nodes[2].cmd(test_select_query(node=ch_nodes[0])).output == "kerberos_user":
break
ch_nodes[2].cmd("kdestroy")
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_ValidUser_KerberosNotConfigured("1.0")
)
def kerberos_not_configured(self):
"""ClickHouse SHALL reject Kerberos authentication if user is not a kerberos-auth user.
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for client"):
kinit_no_keytab(node=ch_nodes[2], principal="unkerberized")
with And('Kinit for server'):
create_server_principal(node=ch_nodes[0])
with By("I add non-Kerberos user to ClickHouse"):
ch_nodes[0].query("CREATE USER unkerberized IDENTIFIED WITH plaintext_password BY 'qwerty'")
with When("I attempt to authenticate"):
r = ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]), no_checks=True)
with Then("I expect authentication failure"):
assert "Authentication failed" in r.output, error()
with Finally("I drop the user"):
ch_nodes[0].query("DROP USER unkerberized")
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_KerberosServerRestarted("1.0")
)
def kerberos_server_restarted(self):
"""ClickHouse SHALL accept Kerberos authentication if Kerberos server was restarted.
"""
ch_nodes = self.context.ch_nodes
krb_server = self.context.krb_server
with Given("I obtain keytab for user"):
kinit_no_keytab(node=ch_nodes[2])
with And("I create server principal"):
create_server_principal(node=ch_nodes[0])
with And("I obtain server ticket"):
ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]), no_checks=True)
with By("I dump, restart and restore kerberos server"):
krb_server.cmd("kdb5_util dump dump.dmp", shell_command="/bin/sh")
krb_server.restart()
krb_server.cmd("kdb5_util load dump.dmp", shell_command="/bin/sh")
with When("I attempt to authenticate"):
r = ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]))
with And("I wait for kerberos to be healthy"):
ch_nodes[2].cmd("kdestroy")
while True:
kinit_no_keytab(node=ch_nodes[2])
if ch_nodes[2].cmd(test_select_query(node=ch_nodes[0])).output == "kerberos_user":
break
with Then(f"I expect kerberos_user"):
assert r.output == "kerberos_user", error()
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_InvalidUser("1.0")
)
def invalid_user(self):
"""ClickHouse SHALL reject Kerberos authentication for invalid principal
"""
ch_nodes = self.context.ch_nodes
with Given("I obtain keytab for invalid user"):
kinit_no_keytab(node=ch_nodes[2], principal="invalid")
with And("I create server principal"):
create_server_principal(node=ch_nodes[0])
with When("I attempt to authenticate"):
r = ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]), no_checks=True)
with Then(f"I expect default"):
assert "Authentication failed: password is incorrect or there is no user with such name" in r.output, error()
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_InvalidUser_UserDeleted("1.0")
)
def user_deleted(self):
"""ClickHouse SHALL reject Kerberos authentication if Kerberos user was deleted prior to query.
"""
ch_nodes = self.context.ch_nodes
with Given("I obtain keytab for a user"):
kinit_no_keytab(node=ch_nodes[2], principal="krb_rbac")
with And("I create server principal"):
create_server_principal(node=ch_nodes[0])
with And("I create and then delete kerberized user"):
ch_nodes[0].query("CREATE USER krb_rbac IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM'")
ch_nodes[0].query("DROP USER krb_rbac")
with When("I attempt to authenticate"):
r = ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]), no_checks=True)
with Then(f"I expect error"):
assert "Authentication failed: password is incorrect or there is no user with such name" in r.output, error()
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Performance("1.0")
)
def authentication_performance(self):
"""ClickHouse's performance for Kerberos authentication SHALL shall be comparable to regular authentication.
"""
ch_nodes = self.context.ch_nodes
with Given("I obtain keytab for a user"):
kinit_no_keytab(node=ch_nodes[2])
with And("I create server principal"):
create_server_principal(node=ch_nodes[0])
with And("I create a password-identified user"):
ch_nodes[0].query("CREATE USER pwd_user IDENTIFIED WITH plaintext_password BY 'pwd'")
with When("I measure kerberos auth time"):
start_time_krb = time.time()
for i in range(100):
ch_nodes[2].cmd(test_select_query(node=ch_nodes[0]))
krb_time = (time.time() - start_time_krb) / 100
with And("I measure password auth time"):
start_time_usual = time.time()
for i in range(100):
ch_nodes[2].cmd(f"echo 'SELECT 1' | curl 'http://pwd_user:pwd@clickhouse1:8123/' -d @-")
usual_time = (time.time() - start_time_usual) / 100
with Then("measuring the performance compared to password auth"):
metric("percentage_improvement", units="%", value=100*(krb_time - usual_time)/usual_time)
with Finally("I drop pwd_user"):
ch_nodes[0].query("DROP USER pwd_user")
@TestFeature
def generic(self):
"""Perform ClickHouse Kerberos authentication testing
"""
self.context.ch_nodes = [self.context.cluster.node(f"clickhouse{i}") for i in range(1, 4)]
self.context.krb_server = self.context.cluster.node("kerberos")
self.context.clients = [self.context.cluster.node(f"krb-client{i}") for i in range(1, 6)]
for scenario in loads(current_module(), Scenario, Suite):
Scenario(run=scenario, flags=TE)

View File

@ -0,0 +1,204 @@
from testflows.core import *
from kerberos.tests.common import *
from kerberos.requirements.requirements import *
from multiprocessing.dummy import Pool
import time
import datetime
import itertools
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Parallel_ValidRequests_SameCredentials("1.0")
)
def valid_requests_same_credentials(self):
"""ClickHouse should be able to process parallel requests sent under the same credentials.
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for clients"):
kinit_no_keytab(node=ch_nodes[1])
kinit_no_keytab(node=ch_nodes[2])
with And('create server principal'):
create_server_principal(node=ch_nodes[0])
def helper(cmd):
return cmd(test_select_query(node=ch_nodes[0]))
for i in range(15):
pool = Pool(2)
tasks = []
with When("I try simultaneous authentication"):
tasks.append(pool.apply_async(helper, (ch_nodes[1].cmd, )))
tasks.append(pool.apply_async(helper, (ch_nodes[2].cmd, )))
tasks[0].wait(timeout=200)
tasks[1].wait(timeout=200)
with Then(f"I expect requests to success"):
assert tasks[0].get(timeout=300).output == "kerberos_user", error()
assert tasks[1].get(timeout=300).output == "kerberos_user", error()
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Parallel_ValidRequests_DifferentCredentials("1.0")
)
def valid_requests_different_credentials(self):
"""ClickHouse should be able to process parallel requests by different users.
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for clients"):
kinit_no_keytab(node=ch_nodes[1], principal="krb1")
kinit_no_keytab(node=ch_nodes[2], principal="krb2")
with And("create server principal"):
create_server_principal(node=ch_nodes[0])
def helper(cmd):
return cmd(test_select_query(node=ch_nodes[0]))
for i in range(15):
pool = Pool(2)
tasks = []
with And("add 2 kerberos users via RBAC"):
ch_nodes[0].query("CREATE USER krb1 IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM'")
ch_nodes[0].query("CREATE USER krb2 IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM'")
with When("I try simultaneous authentication for valid and invalid"):
tasks.append(pool.apply_async(helper, (ch_nodes[1].cmd, )))
tasks.append(pool.apply_async(helper, (ch_nodes[2].cmd, )))
tasks[0].wait(timeout=200)
tasks[1].wait(timeout=200)
with Then(f"I expect have auth failure"):
assert tasks[1].get(timeout=300).output == "krb2", error()
assert tasks[0].get(timeout=300).output == "krb1", error()
with Finally("I make sure both users are removed"):
ch_nodes[0].query("DROP USER krb1", no_checks=True)
ch_nodes[0].query("DROP USER krb2", no_checks=True)
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Parallel_ValidInvalid("1.0")
)
def valid_invalid(self):
"""Valid users' Kerberos authentication should not be affected by invalid users' attempts.
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for clients"):
kinit_no_keytab(node=ch_nodes[2])
kinit_no_keytab(node=ch_nodes[1], principal="invalid_user")
with And('create server principal'):
create_server_principal(node=ch_nodes[0])
def helper(cmd):
return cmd(test_select_query(node=ch_nodes[0]), no_checks=True)
for i in range(15):
pool = Pool(2)
tasks = []
with When("I try simultaneous authentication for valid and invalid"):
tasks.append(pool.apply_async(helper, (ch_nodes[1].cmd, ))) # invalid
tasks.append(pool.apply_async(helper, (ch_nodes[2].cmd, ))) # valid
with Then(f"I expect have auth failure"):
assert tasks[1].get(timeout=300).output == "kerberos_user", error()
assert tasks[0].get(timeout=300).output != "kerberos_user", error()
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Parallel_Deletion("1.0")
)
def deletion(self):
"""ClickHouse SHALL NOT crash when 2 Kerberos users are simultaneously deleting one another.
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for clients"):
kinit_no_keytab(node=ch_nodes[1], principal="krb1")
kinit_no_keytab(node=ch_nodes[2], principal="krb2")
with And("create server principal"):
create_server_principal(node=ch_nodes[0])
def helper(cmd, todel):
return cmd(test_select_query(node=ch_nodes[0], req=f"DROP USER {todel}"), no_checks=True)
for i in range(15):
pool = Pool(2)
tasks = []
with And("add 2 kerberos users via RBAC"):
ch_nodes[0].query("CREATE USER krb1 IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM'")
ch_nodes[0].query("CREATE USER krb2 IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM'")
ch_nodes[0].query("GRANT ACCESS MANAGEMENT ON *.* TO krb1")
ch_nodes[0].query("GRANT ACCESS MANAGEMENT ON *.* TO krb2")
with When("I try simultaneous authentication for valid and invalid"):
tasks.append(pool.apply_async(helper, (ch_nodes[1].cmd, "krb2")))
tasks.append(pool.apply_async(helper, (ch_nodes[2].cmd, "krb1")))
tasks[0].wait(timeout=200)
tasks[1].wait(timeout=200)
with Then(f"I check CH is alive"):
assert ch_nodes[0].query("SELECT 1").output == "1", error()
with Finally("I make sure both users are removed"):
ch_nodes[0].query("DROP USER krb1", no_checks=True)
ch_nodes[0].query("DROP USER krb2", no_checks=True)
@TestScenario
@Requirements(
RQ_SRS_016_Kerberos_Parallel_ValidRequests_KerberosAndNonKerberos("1.0")
)
def kerberos_and_nonkerberos(self):
"""ClickHouse SHALL support processing of simultaneous kerberized and non-kerberized requests.
"""
ch_nodes = self.context.ch_nodes
with Given("kinit for clients"):
kinit_no_keytab(node=ch_nodes[2])
with And('create server principal'):
create_server_principal(node=ch_nodes[0])
def helper(cmd, krb_auth):
return cmd(test_select_query(node=ch_nodes[0], krb_auth=krb_auth), no_checks=True)
for i in range(15):
pool = Pool(2)
tasks = []
with When("I try simultaneous authentication for valid and invalid"):
tasks.append(pool.apply_async(helper, (ch_nodes[1].cmd, False))) # non-kerberos
tasks.append(pool.apply_async(helper, (ch_nodes[2].cmd, True))) # kerberos
with Then(f"I expect have auth failure"):
assert tasks[1].get(timeout=300).output == "kerberos_user", error()
assert tasks[0].get(timeout=300).output == "default", error()
@TestFeature
@Requirements(
RQ_SRS_016_Kerberos_Parallel("1.0")
)
def parallel(self):
"""Perform ClickHouse Kerberos authentication testing for incorrect configuration files
"""
self.context.ch_nodes = [self.context.cluster.node(f"clickhouse{i}") for i in range(1, 4)]
self.context.krb_server = self.context.cluster.node("kerberos")
self.context.clients = [self.context.cluster.node(f"krb-client{i}") for i in range(1, 6)]
for scenario in loads(current_module(), Scenario, Suite):
Scenario(run=scenario, flags=TE)

View File

@ -468,7 +468,7 @@ version: 1.0
the following RBAC command
```sql
CREATE USER name IDENTIFIED WITH ldap_server BY 'server_name'
CREATE USER name IDENTIFIED WITH ldap SERVER 'server_name'
```
#### RQ.SRS-007.LDAP.Configuration.User.Syntax

View File

@ -903,7 +903,7 @@ RQ_SRS_007_LDAP_Configuration_User_RBAC = Requirement(
'the following RBAC command\n'
'\n'
'```sql\n'
"CREATE USER name IDENTIFIED WITH ldap_server BY 'server_name'\n"
"CREATE USER name IDENTIFIED WITH ldap SERVER 'server_name'\n"
'```\n'
'\n'
),
@ -1841,7 +1841,7 @@ version: 1.0
the following RBAC command
```sql
CREATE USER name IDENTIFIED WITH ldap_server BY 'server_name'
CREATE USER name IDENTIFIED WITH ldap SERVER 'server_name'
```
#### RQ.SRS-007.LDAP.Configuration.User.Syntax

View File

@ -248,7 +248,7 @@ def add_users_identified_with_ldap(*users):
try:
with Given("I create users"):
for user in users:
node.query(f"CREATE USER '{user['username']}' IDENTIFIED WITH ldap_server BY '{user['server']}'")
node.query(f"CREATE USER '{user['username']}' IDENTIFIED WITH ldap SERVER '{user['server']}'")
yield
finally:
with Finally("I remove users"):

View File

@ -18,6 +18,7 @@ def regression(self, local, clickhouse_binary_path, stress=None, parallel=None):
Feature(test=load("ldap.regression", "regression"))(**args)
Feature(test=load("rbac.regression", "regression"))(**args)
Feature(test=load("aes_encryption.regression", "regression"))(**args)
# Feature(test=load("kerberos.regression", "regression"))(**args)
if main():
regression()