diff --git a/api/libsphinxclient/sphinxclient.c b/api/libsphinxclient/sphinxclient.c index c59ed20a76..fe4ed8f116 100644 --- a/api/libsphinxclient/sphinxclient.c +++ b/api/libsphinxclient/sphinxclient.c @@ -72,6 +72,7 @@ #define MAX_REQS 32 #define CONNECT_TIMEOUT_MSEC 1000 #define MAX_PACKET_LEN (8*1024*1024) +#define SPHINX_CLIENT_VERSION 2 enum { @@ -91,6 +92,13 @@ enum VER_COMMAND_STATUS = 0x101 }; +enum +{ + AUTH_NO = 1, + AUTH_SHA1 = 2, + AUTH_SHA256 = 3 +}; + ////////////////////////////////////////////////////////////////////////// struct st_filter @@ -125,6 +133,8 @@ struct st_override const unsigned int * uint_values; }; +#define SHA1_SIZE 20 +#define AUTH_HEADER_SIZE (SHA1_SIZE+1) struct st_sphinx_client { @@ -183,6 +193,8 @@ struct st_sphinx_client int outer_limit; sphinx_bool has_outer; + sphinx_bool has_user; + unsigned char user_token[SHA1_SIZE]; int num_reqs; int req_lens [ MAX_REQS ]; @@ -206,6 +218,8 @@ static const char * strchain ( sphinx_client * client, const char * s ); static void unchain ( sphinx_client * client, const void * ptr ); static void unchain_all ( sphinx_client * client ); +static void sphinx_add_auth ( sphinx_client * client, char ** pp ); + sphinx_client * sphinx_create ( sphinx_bool copy_args ) { sphinx_client * client; @@ -272,6 +286,9 @@ sphinx_client * sphinx_create ( sphinx_bool copy_args ) client->outer_limit = 0; client->has_outer = SPH_FALSE; + client->has_user = SPH_FALSE; + memset ( client->user_token, 0, sizeof ( client->user_token ) ); + client->num_reqs = 0; client->response_len = 0; client->response_buf = NULL; @@ -338,6 +355,9 @@ void sphinx_cleanup ( sphinx_client * client ) client->num_results = 0; safe_free ( client->response_buf ); + + client->has_user = SPH_FALSE; + memset ( client->user_token, 0, sizeof ( client->user_token ) ); } @@ -1687,7 +1707,7 @@ static int net_connect_get ( sphinx_client * client ) sock_set_blocking ( sock ); // now send major client protocol version - my_proto = htonl ( 1 ); + my_proto = htonl ( SPHINX_CLIENT_VERSION ); if ( !net_write ( sock, (char*)&my_proto, sizeof(my_proto), client ) ) { sock_close ( sock ); @@ -1924,7 +1944,7 @@ static void net_get_response ( int fd, sphinx_client * client ) sphinx_bool sphinx_open ( sphinx_client * client ) { - char buf[16], *pbuf; + char buf[16+AUTH_HEADER_SIZE], *pbuf; if ( client->sock>=0 ) { @@ -1937,6 +1957,8 @@ sphinx_bool sphinx_open ( sphinx_client * client ) return SPH_FALSE; pbuf = buf; + + sphinx_add_auth ( client, &pbuf ); send_word ( &pbuf, SEARCHD_COMMAND_PERSIST ); send_word ( &pbuf, 0 ); // dummy version send_int ( &pbuf, 4 ); // dummy body len @@ -1988,7 +2010,7 @@ static void * sphinx_malloc ( int len, sphinx_client * client ) sphinx_result * sphinx_run_queries ( sphinx_client * client ) { int i, j, k, l, fd, len, nreqs, id64; - char req_header[32], *req, *p, *pmax; + char req_header[32+AUTH_HEADER_SIZE], *req, *p, *pmax; sphinx_result * res; union un_attr_value * pval; @@ -2014,6 +2036,7 @@ sphinx_result * sphinx_run_queries ( sphinx_client * client ) len += client->req_lens[i]; req = req_header; + sphinx_add_auth ( client, &req ); send_word ( &req, SEARCHD_COMMAND_SEARCH ); send_word ( &req, client->ver_search ); send_int ( &req, len ); @@ -2235,7 +2258,7 @@ const char * sphinx_get_string ( sphinx_result * result, int match, int attr ) ////////////////////////////////////////////////////////////////////////// -static sphinx_bool net_simple_query ( sphinx_client * client, char * buf, int req_len ) +static sphinx_bool net_simple_query ( sphinx_client * client, char * buf, int buf_len ) { int fd; @@ -2246,7 +2269,7 @@ static sphinx_bool net_simple_query ( sphinx_client * client, char * buf, int re return SPH_FALSE; } - if ( !net_write ( fd, buf, 8+req_len, client ) ) + if ( !net_write ( fd, buf, buf_len, client ) ) { free ( buf ); return SPH_FALSE; @@ -2322,7 +2345,7 @@ char ** sphinx_build_excerpts ( sphinx_client * client, int num_docs, const char for ( i=0; istate[0] = 0x67452301; + p->state[1] = 0xEFCDAB89; + p->state[2] = 0x98BADCFE; + p->state[3] = 0x10325476; + p->state[4] = 0xC3D2E1F0; + memset ( p->count, 0, sizeof ( p->count ) ); +} + +static void sha1_update ( const unsigned char * data, int len, struct sha1_st * p ) +{ + int i, j; + + j = ( p->count[0] >> 3 ) & 63; + p->count[0] += ( len << 3 ); + if ( p->count[0] < (unsigned int)( len << 3 ) ) + p->count[1]++; + p->count[1] += ( len >> 29 ); + if ( ( j + len ) > 63 ) + { + i = 64 - j; + memcpy ( &p->buffer[j], data, i ); + sha1_transform ( p->buffer, p ); + for ( ; i + 63 < len; i += 64 ) + sha1_transform ( data + i, p ); + j = 0; + } else + { + i = 0; + } + memcpy ( &p->buffer[j], &data[i], len - i ); +} + +static void sha1_final ( unsigned char * digest, struct sha1_st * p ) +{ + int i; + unsigned char finalcount[8]; + + for ( i = 0; i < 8; ++i ) + finalcount[i] = (unsigned char)( ( p->count[( i >= 4 ) ? 0 : 1] >> ( ( 3 - ( i & 3 ) ) * 8 ) ) + & 255 ); // endian independent + sha1_update ( (const unsigned char*)"\200", 1, p ); // add padding + while ( ( p->count[0] & 504 ) != 448 ) + sha1_update ( (const unsigned char*)"\0", 1, p ); + sha1_update ( finalcount, 8, p ); // should cause a SHA1_Transform() + for ( i = 0; i < SHA1_SIZE; ++i ) + digest[i] = (unsigned char)( ( p->state[i >> 2] >> ( ( 3 - ( i & 3 ) ) * 8 ) ) & 255 ); +} + +void sha1_transform ( const unsigned char * buf, struct sha1_st * p ) +{ + int i; + unsigned int a, b, c, d, e, t; + unsigned int block[16]; + + a = p->state[0]; + b = p->state[1]; + c = p->state[2]; + d = p->state[3]; + e = p->state[4]; + memset ( block, 0, sizeof ( block ) ); // initial conversion to big-endian units + for ( i = 0; i < 64; i++ ) + block[i >> 2] += buf[i] << ( ( 3 - ( i & 3 ) ) * 8 ); + for ( int i = 0; i < 80; i++ ) // do hashing rounds + { +#define LROT( value, bits ) ( ( ( value ) << ( bits ) ) | ( ( value ) >> ( 32 - ( bits ) ) ) ) + if ( i >= 16 ) + block[i & 15] = LROT ( + block[( i + 13 ) & 15] ^ block[( i + 8 ) & 15] ^ block[( i + 2 ) & 15] ^ block[i & 15], 1 ); + + if ( i < 20 ) + e += ( ( b & ( c ^ d ) ) ^ d ) + 0x5A827999; + else if ( i < 40 ) + e += ( b ^ c ^ d ) + 0x6ED9EBA1; + else if ( i < 60 ) + e += ( ( ( b | c ) & d ) | ( b & c ) ) + 0x8F1BBCDC; + else + e += ( b ^ c ^ d ) + 0xCA62C1D6; + + e += block[i & 15] + LROT ( a, 5 ); + t = e; + e = d; + d = c; + c = LROT ( b, 30 ); + b = a; + a = t; + } + p->state[0] += a; // save state + p->state[1] += b; + p->state[2] += c; + p->state[3] += d; + p->state[4] += e; +} + +sphinx_bool sphinx_set_user ( sphinx_client * client, const char * user, const char * password ) +{ + struct sha1_st * hash; + + // check args + if ( !client || !user || !password ) + { + if ( !user ) set_error ( client, "invalid arguments (user must not be empty)" ); + else if ( !password ) set_error ( client, "invalid arguments (password must not be empty)" ); + return SPH_FALSE; + } + + hash = malloc ( sizeof(struct sha1_st) ); + if ( !hash ) + { + set_error ( client, "malloc() failed (bytes=%d)", sizeof(struct sha1_st) ); + return SPH_FALSE; + } + sha1_init ( hash ); + sha1_update ( (const unsigned char *)password, strlen ( password ), hash ); + sha1_final ( client->user_token, hash ); + + sha1_init ( hash ); + sha1_update ( (const unsigned char *)user, strlen ( user ), hash ); + sha1_update ( client->user_token, sizeof ( client->user_token ), hash ); + sha1_final ( client->user_token, hash ); + + client->has_user = SPH_TRUE; + + free ( hash ); + + return SPH_TRUE; +} + +void sphinx_add_auth ( sphinx_client * client, char ** pp ) +{ + unsigned char * p = (unsigned char*)*pp; + if ( client->has_user ) + { + p[0] = AUTH_SHA1; + memcpy ( p+1, client->user_token, SHA1_SIZE ); + *pp += AUTH_HEADER_SIZE; + } else + { + p[0] = AUTH_NO; + *pp += 1; + } +} \ No newline at end of file diff --git a/api/libsphinxclient/sphinxclient.h b/api/libsphinxclient/sphinxclient.h index e98ff69ff3..043ea525cd 100644 --- a/api/libsphinxclient/sphinxclient.h +++ b/api/libsphinxclient/sphinxclient.h @@ -255,6 +255,7 @@ sphinx_keyword_info * sphinx_build_keywords ( sphinx_client * client, const c char ** sphinx_status ( sphinx_client * client, int * num_rows, int * num_cols ); char ** sphinx_status_extended ( sphinx_client * client, int * num_rows, int * num_cols, int local ); void sphinx_status_destroy ( char ** status, int num_rows, int num_cols ); +sphinx_bool sphinx_set_user ( sphinx_client * client, const char * user, const char * password ); ///////////////////////////////////////////////////////////////////////////// diff --git a/api/libsphinxclient/test.c b/api/libsphinxclient/test.c index 2f991070b2..9e11cfdcba 100644 --- a/api/libsphinxclient/test.c +++ b/api/libsphinxclient/test.c @@ -155,10 +155,11 @@ void test_excerpt ( sphinx_client * client ) g_failed += ( res==NULL ); if ( !g_smoke ) die ( "query failed: %s", sphinx_error(client) ); + } else + { + for ( i=0; i_mbenc = ""; $this->_arrayresult = false; $this->_timeout = 0; + + $this->user = ''; + $this->user_token = ''; } function __destruct() @@ -584,8 +596,18 @@ function SetConnectTimeout ( $timeout ) } - function _Send ( $handle, $data, $length ) + function _Send ( $handle, $data, $length, $add_auth ) { + if ( $add_auth ) + { + // auth data head + $auth_head = $this->GetUserData(); + $length += strlen ( $auth_head ); + + $auth_head .= $data; + $data = $auth_head; + } + if ( feof($handle) || fwrite ( $handle, $data, $length ) !== $length ) { $this->_error = 'connection unexpectedly closed (timed out?)'; @@ -666,7 +688,7 @@ function _Connect () // this is a subtle part. we must do it before (!) reading back from searchd. // because otherwise under some conditions (reported on FreeBSD for instance) // TCP stack could throttle write-write-read pattern because of Nagle. - if ( !$this->_Send ( $fp, pack ( "N", 1 ), 4 ) ) + if ( !$this->_Send ( $fp, pack ( "N", 2 ), 4, false ) ) { fclose ( $fp ); $this->_error = "failed to send client protocol version"; @@ -1294,7 +1316,7 @@ function RunQueries () $len = 8+strlen($req); $req = pack ( "nnNNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, 0, $nreqs ) . $req; // add header - if ( !( $this->_Send ( $fp, $req, $len+8 ) ) || + if ( !( $this->_Send ( $fp, $req, $len+8, true ) ) || !( $response = $this->_GetResponse ( $fp, VER_COMMAND_SEARCH ) ) ) { $this->_MBPop (); @@ -1577,7 +1599,7 @@ function BuildExcerpts ( $docs, $index, $words, $opts=array() ) $len = strlen($req); $req = pack ( "nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len ) . $req; // add header - if ( !( $this->_Send ( $fp, $req, $len+8 ) ) || + if ( !( $this->_Send ( $fp, $req, $len+8, true ) ) || !( $response = $this->_GetResponse ( $fp, VER_COMMAND_EXCERPT ) ) ) { $this->_MBPop (); @@ -1647,7 +1669,7 @@ function BuildKeywords ( $query, $index, $hits ) $len = strlen($req); $req = pack ( "nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len ) . $req; // add header - if ( !( $this->_Send ( $fp, $req, $len+8 ) ) || + if ( !( $this->_Send ( $fp, $req, $len+8, true ) ) || !( $response = $this->_GetResponse ( $fp, VER_COMMAND_KEYWORDS ) ) ) { $this->_MBPop (); @@ -1781,7 +1803,7 @@ function UpdateAttributes ( $index, $attrs, $values, $type=SPH_UPDATE_INT, $igno $len = strlen($req); $req = pack ( "nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len ) . $req; // add header - if ( !$this->_Send ( $fp, $req, $len+8 ) ) + if ( !$this->_Send ( $fp, $req, $len+8, true ) ) { $this->_MBPop (); return -1; @@ -1815,7 +1837,7 @@ function Open() // command, command version = 0, body length = 4, body = 1 $req = pack ( "nnNN", SEARCHD_COMMAND_PERSIST, 0, 4, 1 ); - if ( !$this->_Send ( $fp, $req, 12 ) ) + if ( !$this->_Send ( $fp, $req, 12, true ) ) return false; $this->_socket = $fp; @@ -1852,7 +1874,7 @@ function Status ($session=false) } $req = pack ( "nnNN", SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, 4, $session?0:1 ); // len=4, body=1 - if ( !( $this->_Send ( $fp, $req, 12 ) ) || + if ( !( $this->_Send ( $fp, $req, 12, true ) ) || !( $response = $this->_GetResponse ( $fp, VER_COMMAND_STATUS ) ) ) { $this->_MBPop (); @@ -1888,7 +1910,7 @@ function FlushAttributes () } $req = pack ( "nnN", SEARCHD_COMMAND_FLUSHATTRS, VER_COMMAND_FLUSHATTRS, 0 ); // len=0 - if ( !( $this->_Send ( $fp, $req, 8 ) ) || + if ( !( $this->_Send ( $fp, $req, 8, true ) ) || !( $response = $this->_GetResponse ( $fp, VER_COMMAND_FLUSHATTRS ) ) ) { $this->_MBPop (); @@ -1904,6 +1926,35 @@ function FlushAttributes () $this->_MBPop (); return $tag; } + + ////////////////////////////////////////////////////////////////////////// + // user + ////////////////////////////////////////////////////////////////////////// + + function SetUser ($user, $password) + { + $this->user = $user; + + $password_hash = hash ( "sha256", $password, true ); + $pwd_hash = hash ( "sha256", $user . $password_hash, true ); + $this->user_token = $pwd_hash; + } + + function GetUserData () + { + $data = ''; + if ( $this->user=='' ) + { + $data .= pack ( "C", AUTH_NO ); + } else + { + $data .= pack ( "C", AUTH_SHA256 ); + $data .= $this->user_token; + } + + return $data; + } + } // diff --git a/manual/References.md b/manual/References.md index 4e29146424..63aabd9f61 100755 --- a/manual/References.md +++ b/manual/References.md @@ -604,7 +604,7 @@ spelldump [options] [result] [locale-name] A comprehensive alphabetical list of keywords currently reserved in Manticore SQL syntax (thus, they cannot be used as identifiers). ``` -AND, AS, BY, COLUMNARSCAN, DISTINCT, DIV, DOCIDINDEX, EXPLAIN, FACET, FALSE, FORCE, FROM, IGNORE, IN, INDEXES, INNER, IS, JOIN, KNN, LEFT, LIMIT, MOD, NOT, NO_COLUMNARSCAN, NO_DOCIDINDEX, NO_SECONDARYINDEX, NULL, OFFSET, ON, OR, ORDER, RELOAD, SECONDARYINDEX, SELECT, SYSFILTERS, TRUE +AND, AS, BY, COLUMNARSCAN, DISTINCT, DIV, DOCIDINDEX, EXPLAIN, FACET, FALSE, FORCE, FROM, IGNORE, IN, INDEXES, INNER, IS, JOIN, KNN, LEFT, LIMIT, MOD, NOT, NO_COLUMNARSCAN, NO_DOCIDINDEX, NO_SECONDARYINDEX, NULL, OFFSET, ON, OR, ORDER, RELOAD, SECONDARYINDEX, SELECT, SYSFILTERS, TOKEN, TRUE ``` ## Documentation for old Manticore versions diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 548bfaef40..313e612dd7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -174,7 +174,8 @@ set ( SEARCHD_H searchdaemon.h searchdconfig.h searchdddl.h searchdexpr.h search compressed_zlib_mysql.h sphinxql_debug.h stackmock.h searchdssl.h digest_sha1.h client_session.h compressed_zstd_mysql.h docs_collector.h index_rotator.h config_reloader.h searchdhttp.h timeout_queue.h netpoll.h pollable_event.h netfetch.h searchdbuddy.h sphinxql_second.h sphinxql_extra.h sphinxjsonquery.h - frontendschema.h debug_cmds.h dynamic_idx.h sphinxexcerpt.h) + frontendschema.h debug_cmds.h dynamic_idx.h sphinxexcerpt.h + auth/auth.h auth/auth_common.h auth/auth_proto_http.h auth/auth_proto_mysql.h auth/auth_proto_api.h digest_sha256.h auth/auth_perms.h) # add the extra targets in the case we want on-the-fly grammar compiler @@ -255,11 +256,15 @@ add_library ( lsearchd OBJECT searchdha.cpp http/http_parser.c searchdhttp.cpp netreceive_http.cpp netreceive_ql.cpp query_status.cpp sphinxql_debug.cpp sphinxql_second.cpp stackmock.cpp docs_collector.cpp index_rotator.cpp config_reloader.cpp netpoll.cpp pollable_event.cpp netfetch.cpp searchdbuddy.cpp searchdhttpcompat.cpp sphinxql_extra.cpp searchdreplication.cpp sphinxjsonquery.cpp - frontendschema.cpp compressed_http.cpp debug_cmds.cpp jsonqueryfilter.cpp dynamic_idx.cpp sphinxexcerpt.cpp searchdexpr.cpp) + frontendschema.cpp compressed_http.cpp debug_cmds.cpp jsonqueryfilter.cpp dynamic_idx.cpp sphinxexcerpt.cpp searchdexpr.cpp + auth/auth.cpp auth/auth_common.cpp auth/auth_proto_http.cpp auth/auth_proto_mysql.cpp auth/auth_proto_api.cpp auth/auth_perms.cpp) + target_sources ( lsearchd PUBLIC ${SEARCHD_SRCS_TESTABLE} ${SEARCHD_H} ${SEARCHD_BISON} ${SEARCHD_FLEX} ) add_library ( digest_sha1 digest_sha1.cpp ) +add_library ( digest_sha256 digest_sha256.cpp ) target_link_libraries ( digest_sha1 PRIVATE lextra ) -target_link_libraries ( lsearchd PUBLIC digest_sha1 lextra nlohmann_json::nlohmann_json ) +target_link_libraries ( digest_sha256 PRIVATE lextra ) +target_link_libraries ( lsearchd PUBLIC digest_sha1 digest_sha256 lextra nlohmann_json::nlohmann_json ) target_link_libraries ( lsearchd INTERFACE Boost::filesystem ) function (stackmock processors compiler versions config values) @@ -332,9 +337,12 @@ if (WITH_SSL) target_link_libraries ( searchd_ssl PRIVATE OpenSSL::Crypto ) target_link_libraries ( digest_sha1 PRIVATE OpenSSL::SSL ) target_link_libraries ( digest_sha1 PRIVATE OpenSSL::Crypto ) + target_link_libraries ( digest_sha256 PRIVATE OpenSSL::SSL ) + target_link_libraries ( digest_sha256 PRIVATE OpenSSL::Crypto ) if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") target_link_options ( searchd_ssl INTERFACE $<${ONLYGNUCLANGC_CXX}:-Wl,--exclude-libs,libssl.a,--exclude-libs,libcrypto.a> ) target_link_options ( digest_sha1 INTERFACE $<${ONLYGNUCLANGC_CXX}:-Wl,--exclude-libs,libssl.a,--exclude-libs,libcrypto.a> ) + target_link_options ( digest_sha256 INTERFACE $<${ONLYGNUCLANGC_CXX}:-Wl,--exclude-libs,libssl.a,--exclude-libs,libcrypto.a> ) endif() target_compile_options ( searchd_ssl PRIVATE "$<$:-Wno-deprecated-declarations>" ) include ( CheckFunctionExists ) diff --git a/src/auth/auth.cpp b/src/auth/auth.cpp new file mode 100644 index 0000000000..c82384d5b3 --- /dev/null +++ b/src/auth/auth.cpp @@ -0,0 +1,908 @@ +// +// Copyright (c) 2017-2024, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#include "std/base64.h" +#include "searchdha.h" +#include "dynamic_idx.h" +#include "searchdsql.h" +#include "docs_collector.h" +#include "client_session.h" + +#include "auth_common.h" +#include "auth_proto_mysql.h" +#include "auth_proto_http.h" +#include "auth_proto_api.h" +#include "auth.h" + +static CSphMutex g_tAuthLock; +static AuthUsersPtr_t g_tAuth; + +static CSphString g_sAuthFile; + +///////////////////////////////////////////////////////////////////////////// +/// auth load + +static void AddBuddyToken ( const AuthUserCred_t * pSrcBuddy, AuthUsers_t & tDst ); + +void AuthConfigure ( const CSphConfigSection & hSearchd ) +{ + CSphString sFile = hSearchd.GetStr ( "auth_user_file" ); + if ( sFile.IsEmpty() ) + return; + + g_sAuthFile = RealPath ( sFile ); + CSphString sError; + AuthUsersMutablePtr_t tAuth = ReadAuthFile ( g_sAuthFile, sError ); + if ( !tAuth ) + sphFatal ( "%s", sError.cstr() ); + + AddBuddyToken ( nullptr, *tAuth ); + + ScopedMutex_t tLock ( g_tAuthLock ); + g_tAuth = std::move ( tAuth ); +} + +bool IsAuthEnabled() +{ + if ( !g_tAuth ) + return false; + + ScopedMutex_t tLock ( g_tAuthLock ); + return g_tAuth->m_hUserToken.GetLength(); +} + +CSphString GetBuddyToken() +{ + AuthUsersPtr_t pUsers = GetAuth(); + const AuthUserCred_t * pBuddy = pUsers->m_hUserToken ( GetAuthBuddyName() ); + if ( !pBuddy ) + return CSphString(); + return pBuddy->m_sRawBearerSha256; +} + +const CSphString CreateSessionToken() +{ + CSphString sToken; + + if ( !IsAuthEnabled() ) + return sToken; + + AuthUsersPtr_t pUsers = GetAuth(); + AuthUserCred_t * pAuth = pUsers->m_hUserToken ( session::GetUser() ); + if ( !pAuth ) + return sToken; + + sToken = pAuth->m_sRawBearerSha256; + return sToken; +} + +const AuthUsersPtr_t GetAuth() +{ + ScopedMutex_t tLock ( g_tAuthLock ); + return g_tAuth; +} + +AuthUsersMutablePtr_t CopyAuth() +{ + AuthUsersPtr_t tAuth = GetAuth(); + if ( tAuth ) + return std::make_unique( *tAuth ); + else + return std::make_unique(); +} + +bool AuthReload ( CSphString & sError ) +{ + if ( !IsAuthEnabled() ) + { + sError = "authorization disabled, can not reload"; + return false; + } + + AuthUsersMutablePtr_t tAuth = ReadAuthFile ( g_sAuthFile, sError ); + if ( !tAuth ) + return false; + + ScopedMutex_t tLock ( g_tAuthLock ); + const AuthUserCred_t * pSrcBuddy = g_tAuth->m_hUserToken ( GetAuthBuddyName() ); + AddBuddyToken ( pSrcBuddy, *tAuth ); + g_tAuth = std::move ( tAuth ); + + return true; +} + +void AddBuddyToken ( const AuthUserCred_t * pSrcBuddy, AuthUsers_t & tAuth ) +{ + AuthUserCred_t tBuddy; + if ( !pSrcBuddy ) + { + tBuddy.m_sUser = GetAuthBuddyName(); + + SHA1_c tSaltHash; + tSaltHash.Init(); + DWORD uScramble[4] = { sphRand(), sphRand(), sphRand(), sphRand() }; + tSaltHash.Update ( (const BYTE *)&uScramble, sizeof ( uScramble ) ); + int64_t iDT = sphMicroTimer(); + tSaltHash.Update ( (const BYTE *)&iDT, sizeof ( iDT ) ); + tSaltHash.Update ( (const BYTE *)tBuddy.m_sUser.cstr(), tBuddy.m_sUser.Length() ); + tBuddy.m_sSalt = BinToHex ( tSaltHash.FinalHash() ); + + SHA1_c tSaltPwd1; + tSaltPwd1.Init(); + tSaltPwd1.Update ( (const BYTE *)tBuddy.m_sSalt.cstr(), tBuddy.m_sSalt.Length() ); + tSaltPwd1.Update ( (const BYTE *)tBuddy.m_sUser.cstr(), tBuddy.m_sUser.Length() ); + tBuddy.m_tPwdSha1 = tSaltPwd1.FinalHash(); + + std::unique_ptr pPwd256 { CreateSHA256() }; + pPwd256->Init(); + pPwd256->Update ( (const BYTE *)tBuddy.m_sSalt.cstr(), tBuddy.m_sSalt.Length() ); + pPwd256->Update ( (const BYTE *)tBuddy.m_sUser.cstr(), tBuddy.m_sUser.Length() ); + tBuddy.m_sPwdSha256 = BinToHex ( pPwd256->FinalHash() ); + + std::unique_ptr pToken { CreateSHA256() }; + pToken->Init(); + pToken->Update ( (const BYTE *)tBuddy.m_sSalt.cstr(), tBuddy.m_sSalt.Length() ); + pToken->Update ( (const BYTE *)tBuddy.m_sPwdSha256.cstr(), tBuddy.m_sPwdSha256.Length() ); + tBuddy.m_sRawBearerSha256 = tBuddy.m_sBearerSha256 = BinToHex ( pToken->FinalHash() ); + + pSrcBuddy = &tBuddy; + } + + tAuth.m_hUserToken.Add ( *pSrcBuddy, pSrcBuddy->m_sUser ); + tAuth.m_hHttpToken2User.Add ( pSrcBuddy->m_sUser, pSrcBuddy->m_sRawBearerSha256 ); + tAuth.m_hApiToken2User.Add ( pSrcBuddy->m_sUser, pSrcBuddy->m_sBearerSha256 ); +} + +class DynamicAuthIndex_c : public GenericTableIndex_c +{ +public: + DynamicAuthIndex_c ( bool bForceFinalize ) + { + m_bForceFinalize = bForceFinalize; + } + virtual ~DynamicAuthIndex_c() = default; + const CSphSchema & GetMatchSchema () const final { return m_tSchema; } + +private: + void SetSorterStuff ( CSphMatch * pMatch ) const final + { + m_pMatch = pMatch; + m_pUsers = GetAuth(); + Start(); + } + Str_t GetErrors () const final { return FromStr ( m_sError ); } + +protected: + CSphString m_sError; + mutable CSphMatch * m_pMatch = nullptr; + + virtual void Start() const = 0; + + mutable AuthUsersPtr_t m_pUsers; +}; + +class AuthUsersIndex_c final : public DynamicAuthIndex_c +{ +public: + AuthUsersIndex_c ( bool bForceFinalize ); + +private: + bool FillNextMatch () const final; + void Start() const override; + + mutable SmallStringHash_T::Iterator_c m_tItUser; + +public: + enum class SchemaColumn_e + { + Id = 0, + Username = 1, + Salt = 2, + Hashes = 3, + Hash_SHA1 = 4, + Hash_SHA256 = 5, + Hash_Bearer = 6, + + Total + }; + + static const char * GetColumnName ( SchemaColumn_e eName ); +}; + +class AuthPermsIndex_c final : public DynamicAuthIndex_c +{ +public: + AuthPermsIndex_c ( bool bForceFinalize ); + +private: + bool FillNextMatch () const final; + void Start() const override; + + mutable SmallStringHash_T::Iterator_c m_tItPerm; + mutable int m_iItemPerm = 0; + +public: + enum class SchemaColumn_e + { + Id = 0, + Username = 1, + Action = 2, + Target = 3, + Allow = 4, + Budget = 5, + }; + + static const char * GetColumnName ( SchemaColumn_e eName ); +}; + +AuthUsersIndex_c::AuthUsersIndex_c ( bool bForceFinalize ) + : DynamicAuthIndex_c ( bForceFinalize ) +{ + m_tSchema.AddAttr ( CSphColumnInfo ( GetColumnName ( SchemaColumn_e::Id ), SPH_ATTR_BIGINT ), true ); + m_tSchema.AddAttr ( CSphColumnInfo ( GetColumnName ( SchemaColumn_e::Username ), SPH_ATTR_STRINGPTR ), true ); + m_tSchema.AddAttr ( CSphColumnInfo ( GetColumnName ( SchemaColumn_e::Salt ), SPH_ATTR_STRINGPTR ), true ); + m_tSchema.AddAttr ( CSphColumnInfo ( GetColumnName ( SchemaColumn_e::Hashes ), SPH_ATTR_JSON_PTR ), true ); +} + +const char * AuthUsersIndex_c::GetColumnName ( SchemaColumn_e eName ) +{ + switch ( eName ) + { + case SchemaColumn_e::Id: return sphGetDocidName(); + case SchemaColumn_e::Username: return "username"; + case SchemaColumn_e::Salt: return "salt"; + case SchemaColumn_e::Hashes: return "hashes"; + case SchemaColumn_e::Hash_SHA1: return "password_sha1_no_salt"; + case SchemaColumn_e::Hash_SHA256: return "password_sha256"; + case SchemaColumn_e::Hash_Bearer: return "bearer_sha256"; + + default: + break; + } + + return nullptr; +} + +void AuthUsersIndex_c::Start() const +{ + m_tItUser = m_pUsers->m_hUserToken.begin(); +} + +static void PutString ( const CSphString & sVal, const CSphAttrLocator & tLoc, CSphMatch * m_pMatch ) +{ + int iLen = sVal.Length(); + + BYTE * pData = nullptr; + m_pMatch->SetAttr ( tLoc, (SphAttr_t)sphPackPtrAttr ( iLen, &pData ) ); + if ( iLen ) + memcpy ( pData, sVal.cstr(), iLen ); +} + +static void PutString ( const char * sVal, const CSphAttrLocator & tLoc, CSphMatch * m_pMatch ) +{ + int iLen = ( sVal ? strlen ( sVal ) : 0 ); + + BYTE * pData = nullptr; + m_pMatch->SetAttr ( tLoc, (SphAttr_t)sphPackPtrAttr ( iLen, &pData ) ); + if ( iLen ) + memcpy ( pData, sVal, iLen ); +} + +static void PutBjson ( const VecTraits_T & dBson, const CSphAttrLocator & tLoc, CSphMatch * m_pMatch ) +{ + BYTE * pData = nullptr; + SphAttr_t uValue = (SphAttr_t)sphPackPtrAttr ( dBson.GetLength(), &pData ); + if ( dBson.GetLength() ) + memcpy ( pData, dBson.Begin(), dBson.GetLength() ); + m_pMatch->SetAttr ( tLoc, uValue ); +} + +static SphAttr_t Hash ( const AuthUserCred_t & tUser ) +{ + SphAttr_t tID = sphFNV64 ( tUser.m_sUser.cstr(), tUser.m_sUser.Length(), SPH_FNV64_SEED ); + tID = abs ( tID ); + + return tID; +} + +static SphAttr_t Hash ( const CSphString & sUser, const UserPerm_t & tPerm ) +{ + SphAttr_t tID = SPH_FNV64_SEED; + tID = sphFNV64 ( sUser.cstr(), sUser.Length(), tID ); + tID = sphFNV64 ( &tPerm.m_eAction, 1, tID ); + tID = sphFNV64 ( tPerm.m_sTarget.cstr(), tPerm.m_sTarget.Length(), tID ); + tID = sphFNV64 ( &tPerm.m_bAllow, 1, tID ); + tID = sphFNV64 ( tPerm.m_sBudget.cstr(), tPerm.m_sBudget.Length(), tID ); + tID = abs ( tID ); + + return tID; +} + +bool AuthUsersIndex_c::FillNextMatch () const +{ + if ( m_tItUser!=m_pUsers->m_hUserToken.end() ) + { + const AuthUserCred_t & tUser = m_tItUser->second; + + // docid + m_pMatch->SetAttr ( m_tSchema.GetAttr ( (int)SchemaColumn_e::Id ).m_tLocator, Hash ( tUser ) ); + // username + PutString ( tUser.m_sUser, m_tSchema.GetAttr ( (int)SchemaColumn_e::Username ).m_tLocator, m_pMatch ); + // salt + PutString ( BinToHex ( (const BYTE *)tUser.m_sSalt.scstr(), tUser.m_sSalt.Length() ), m_tSchema.GetAttr ( (int)SchemaColumn_e::Salt ).m_tLocator, m_pMatch ); + // hashes + CSphVector dHashes; + { + bson::Root_c tHashes ( dHashes ); + + tHashes.AddString ( GetColumnName ( SchemaColumn_e::Hash_SHA1 ), BinToHex ( (const BYTE *)tUser.m_tPwdSha1.data(), tUser.m_tPwdSha1.size() ).cstr() ); + tHashes.AddString ( GetColumnName ( SchemaColumn_e::Hash_SHA256 ), BinToHex ( (const BYTE *)tUser.m_sPwdSha256.cstr(), tUser.m_sPwdSha256.Length() ).cstr() ); + tHashes.AddString ( GetColumnName ( SchemaColumn_e::Hash_Bearer ), BinToHex ( (const BYTE *)tUser.m_sBearerSha256.cstr(), tUser.m_sBearerSha256.Length() ).cstr() ); + } + PutBjson ( dHashes, m_tSchema.GetAttr ( (int)SchemaColumn_e::Hashes ).m_tLocator, m_pMatch ); + + ++m_tItUser; + return true; + } + return false; +} + +const char * AuthPermsIndex_c::GetColumnName ( SchemaColumn_e eName ) +{ + switch ( eName ) + { + case SchemaColumn_e::Id: return sphGetDocidName(); + case SchemaColumn_e::Username: return "username"; + case SchemaColumn_e::Action: return "action"; + case SchemaColumn_e::Target: return "target"; + case SchemaColumn_e::Allow: return "allow"; + case SchemaColumn_e::Budget: return "budget"; + + default: + break; + } + + return nullptr; +} + +AuthPermsIndex_c::AuthPermsIndex_c ( bool bForceFinalize ) + : DynamicAuthIndex_c ( bForceFinalize ) +{ + m_tSchema.AddAttr ( CSphColumnInfo ( GetColumnName ( SchemaColumn_e::Id ), SPH_ATTR_BIGINT ), true ); + m_tSchema.AddAttr ( CSphColumnInfo ( GetColumnName ( SchemaColumn_e::Username ), SPH_ATTR_STRINGPTR ), true ); + m_tSchema.AddAttr ( CSphColumnInfo ( GetColumnName ( SchemaColumn_e::Action ), SPH_ATTR_STRINGPTR ), true ); + m_tSchema.AddAttr ( CSphColumnInfo ( GetColumnName ( SchemaColumn_e::Target ), SPH_ATTR_STRINGPTR ), true ); + m_tSchema.AddAttr ( CSphColumnInfo ( GetColumnName ( SchemaColumn_e::Allow ), SPH_ATTR_BOOL ), true ); + m_tSchema.AddAttr ( CSphColumnInfo ( GetColumnName ( SchemaColumn_e::Budget ), SPH_ATTR_JSON_PTR ), true ); +} + +void AuthPermsIndex_c::Start() const +{ + m_tItPerm = m_pUsers->m_hUserPerms.begin(); + m_iItemPerm = 0; +} + +bool AuthPermsIndex_c::FillNextMatch () const +{ + while ( m_tItPerm!=m_pUsers->m_hUserPerms.end() ) + { + // skip empty perms vector or switch to next perms vector + if ( m_iItemPerm>=m_tItPerm->second.GetLength() ) + { + m_iItemPerm = 0; + ++m_tItPerm; + continue; + } + + const CSphString & sUser = m_tItPerm->first; + const UserPerm_t & tPerm = m_tItPerm->second[m_iItemPerm]; + + // docid + m_pMatch->SetAttr ( m_tSchema.GetAttr ( (int)SchemaColumn_e::Id ).m_tLocator, Hash ( sUser, tPerm ) ); + // username + PutString ( sUser, m_tSchema.GetAttr ( (int)SchemaColumn_e::Username ).m_tLocator, m_pMatch ); + // action + PutString ( GetActionName ( tPerm.m_eAction ), m_tSchema.GetAttr ( (int)SchemaColumn_e::Action).m_tLocator, m_pMatch ); + // target + PutString ( tPerm.m_sTarget, m_tSchema.GetAttr ( (int)SchemaColumn_e::Target ).m_tLocator, m_pMatch ); + // allow + m_pMatch->SetAttr ( m_tSchema.GetAttr ( (int)SchemaColumn_e::Allow ).m_tLocator, (SphAttr_t)tPerm.m_bAllow ); + // budget + CSphVector dBudget; + if ( !tPerm.m_sBudget.IsEmpty() ) + { + bson::BsonContainer_c tBudget ( tPerm.m_sBudget.cstr() ); + tBudget.BsonToBson ( dBudget ); + } + PutBjson ( dBudget, m_tSchema.GetAttr ( (int)SchemaColumn_e::Budget ).m_tLocator, m_pMatch ); + + m_iItemPerm++; + + return true; + } + + return false; +} + +static ServedIndexRefPtr_c MakeDynamicAuthIndex ( const CSphString & sName, bool bForceFinalize ) +{ + if ( sName==GetIndexNameAuthUsers() ) + return MakeServed ( new AuthUsersIndex_c ( bForceFinalize ) ); + else if ( sName==GetIndexNameAuthPerms() ) + return MakeServed ( new AuthPermsIndex_c ( bForceFinalize ) ); + else + return nullptr; +} + +ServedIndexRefPtr_c MakeDynamicAuthIndex ( const CSphString & sName, StringBuilder_c & sError ) +{ + ServedIndexRefPtr_c pIndex = MakeDynamicAuthIndex ( sName, false ); + if ( !pIndex ) + sError << "no such table " << sName; + return pIndex; +} + +///////////////////////////////////////////////////////////////////////////// +// DELETE + +static void DeleteUser ( AuthUsersMutablePtr_t & tAuth, const CSphString & sUserName ) +{ + const auto & tUser = tAuth->m_hUserToken[sUserName]; + tAuth->m_hHttpToken2User.Delete ( tUser.m_sRawBearerSha256 ); + tAuth->m_hApiToken2User.Delete ( tUser.m_sBearerSha256 ); + tAuth->m_hUserPerms.Delete ( sUserName ); + tAuth->m_hUserToken.Delete ( sUserName ); +} + +static int DeleteUsers ( const VecTraits_T & dSrcDocs, CSphString & sError ) +{ + CSphFixedVector < std::pair > dDocsDelete ( dSrcDocs.GetLength() ); + ARRAY_FOREACH ( i, dSrcDocs ) + dDocsDelete[i].first = dSrcDocs[i]; + dDocsDelete.Sort(); + int iDocsCount = sphUniq ( dDocsDelete.Begin(), dDocsDelete.GetLength() ); + + AuthUsersMutablePtr_t tAuth = CopyAuth(); + for ( const auto & tItem : tAuth->m_hUserToken ) + { + SphAttr_t tUserHash = Hash ( tItem.second ); + auto * pDoc = dDocsDelete.BinarySearch ( ([](const auto & tItem ) { return tItem.first; }), tUserHash ); + if ( pDoc ) + pDoc->second = tItem.first; + } + + int iDeleted = 0; + for ( int i=0; i & dDocs, CSphString & sError ) +{ + int iDeleted = 0; + + AuthUsersMutablePtr_t tAuth = CopyAuth(); + for ( auto & tPerms : tAuth->m_hUserPerms ) + { + ARRAY_FOREACH ( iPerm, tPerms.second ) + { + SphAttr_t tPermHash = Hash ( tPerms.first, tPerms.second[iPerm] ); + if ( !dDocs.Contains ( tPermHash ) ) + continue; + + tPerms.second.Remove ( iPerm-- ); + iDeleted++; + } + } + + if ( iDeleted ) + { + ScopedMutex_t tLock ( g_tAuthLock ); + if ( !SaveAuthFile ( *tAuth.get(), g_sAuthFile, sError ) ) + return 0; + + g_tAuth = std::move ( tAuth ); + } + + return iDeleted; +} + + +int DeleteAuthDocuments ( const CSphString & sName, const SqlStmt_t & tStmt, CSphString & sError ) +{ + cServedIndexRefPtr_c pIndex { MakeDynamicAuthIndex ( sName, true ) }; + if ( !pIndex ) + { + sError.SetSprintf ( "no such table %s", sName.cstr() ); + return 0; + } + + DocsCollector_c tDocs { tStmt.m_tQuery, tStmt.m_bJson, sName, pIndex, &sError }; + auto dDocs = tDocs.GetValuesSlice(); + if ( !sError.IsEmpty() || dDocs.IsEmpty() ) + return 0; + + if ( sName==GetIndexNameAuthUsers() ) + return DeleteUsers ( dDocs, sError ); + else + return DeletePerms ( dDocs, sError ); +} + +///////////////////////////////////////////////////////////////////////////// +// INSERT + +static int GetSchemaColumn ( const char * sDstColname, const StrVec_t & dSchema, CSphString & sError ) +{ + int iSrcCol = dSchema.GetFirst ( [&] ( const auto & sSrcColName ) { return sSrcColName==sDstColname; } ); + if ( iSrcCol==-1 ) + sError.SetSprintf ( "missed column %s", sDstColname ); + + return iSrcCol; +} + + +static int GetSchemaColumn ( const AuthUsersIndex_c::SchemaColumn_e eCol, const StrVec_t & dSchema, CSphString & sError ) +{ + return GetSchemaColumn ( AuthUsersIndex_c::GetColumnName ( eCol ), dSchema, sError ); +} + +static int GetSchemaColumn ( const AuthPermsIndex_c::SchemaColumn_e eCol, const StrVec_t & dSchema, CSphString & sError ) +{ + return GetSchemaColumn ( AuthPermsIndex_c::GetColumnName ( eCol ), dSchema, sError ); +} + + +static bool SetUserMember ( const AuthUsersIndex_c::SchemaColumn_e eCol, const SqlInsert_t & tVal, int iRow, AuthUserCred_t & tUser, CSphString & sError ) +{ + switch ( eCol ) + { + case AuthUsersIndex_c::SchemaColumn_e::Username: + tUser.m_sUser = tVal.m_sVal; + if ( tUser.m_sUser.IsEmpty() ) + { + sError.SetSprintf ( "empty 'username' at %d document", iRow ); + return false; + } + return true; + + case AuthUsersIndex_c::SchemaColumn_e::Salt: + tUser.m_sSalt = ReadHex ( FromStr ( tVal.m_sVal ), HASH20_SIZE, sError ); + return true; + + case AuthUsersIndex_c::SchemaColumn_e::Hashes: + { + if ( tVal.m_sVal.IsEmpty() ) + { + sError.SetSprintf ( "empty 'hashes' at %d document", iRow ); + return false; + } + CSphVector tRawBson; + if ( !sphJsonParse ( tRawBson, const_cast( tVal.m_sVal.cstr() ), false, false, false, sError ) ) + { + sError.SetSprintf ( "can not read user 'hashes' at %d document, error: %s", iRow, sError.cstr() ); + return false; + } + + bson::Bson_c tBsonSrc ( tRawBson ); + if ( tBsonSrc.IsEmpty() || !tBsonSrc.IsAssoc() ) + { + sError.SetSprintf ( "can not read user 'hashes' at %d document, error: wrong json", iRow ); + return false; + } + + CSphString sSha1 = ReadHex ( "password_sha1_no_salt", HASH20_SIZE, tBsonSrc, sError ); + memcpy ( tUser.m_tPwdSha1.data(), sSha1.cstr(), HASH20_SIZE ); + tUser.m_sPwdSha256 = ReadHex ( "password_sha256", HASH256_SIZE, tBsonSrc, sError ); + tUser.m_sBearerSha256 = ReadHex ( "bearer_sha256", HASH256_SIZE, tBsonSrc, sError ); + tUser.m_sRawBearerSha256 = bson::String ( tBsonSrc.ChildByName ( "bearer_sha256" ) ); + + return sError.IsEmpty(); + } + + default: + break; + } + + sError.SetSprintf ( "unknown column %s", AuthUsersIndex_c::GetColumnName ( eCol ) ); + return false; +} + +static CSphFixedVector< int > CheckInsertUsers ( const SqlStmt_t & tStmt, CSphString & sError ) +{ + const int iExp = tStmt.m_iSchemaSz; + const int iAttrsCount = (int)AuthUsersIndex_c::SchemaColumn_e::Hashes; + CSphFixedVector< int > dMapping ( iAttrsCount ); + + if ( tStmt.m_dInsertSchema.GetLength() ) + { + if ( tStmt.m_dInsertSchema.GetLength()!=iAttrsCount ) + { + sError.SetSprintf ( "column count does not match schema (expected %d, got %d)", tStmt.m_dInsertSchema.GetLength(), iAttrsCount ); + return dMapping; + } + // id calculated from the content + if ( tStmt.m_dInsertSchema.Contains ( AuthUsersIndex_c::GetColumnName ( AuthUsersIndex_c::SchemaColumn_e::Id ) ) ) + { + sError.SetSprintf ( "can not set id column" ); + return dMapping; + } + dMapping[0] = GetSchemaColumn ( AuthUsersIndex_c::SchemaColumn_e::Username, tStmt.m_dInsertSchema, sError ); + dMapping[1] = GetSchemaColumn ( AuthUsersIndex_c::SchemaColumn_e::Salt, tStmt.m_dInsertSchema, sError ); + dMapping[2] = GetSchemaColumn ( AuthUsersIndex_c::SchemaColumn_e::Hashes, tStmt.m_dInsertSchema, sError ); + if ( !sError.IsEmpty() ) + return dMapping; + + } else + { + int iGot = tStmt.m_dInsertValues.GetLength(); + if ( iAttrsCount!=tStmt.m_iSchemaSz ) + { + sError.SetSprintf ( "column count does not match schema (expected %d, got %d)", iAttrsCount, iGot ); + return dMapping; + } + if ( ( iGot % iExp )!=0 ) + { + sError.SetSprintf ( "column count does not match value count (expected %d, got %d)", iExp, iGot ); + return dMapping; + } + + dMapping[0] = 0; + dMapping[1] = 1; + dMapping[2] = 2; + } + + return dMapping; +} + +static bool InsertUsers ( const SqlStmt_t & tStmt, bool bReplace, AuthUsersMutablePtr_t & tAuth, CSphVector & dIds, CSphString & sError ) +{ + CSphFixedVector< int > dMapping = CheckInsertUsers ( tStmt, sError ); + if ( !sError.IsEmpty() ) + return false; + + for ( int iRow=0; iRowm_hUserToken ( tUser.m_sUser ); + if ( pCurUser && !bReplace ) + { + sError.SetSprintf ( "duplicate user '%s', use REPLACE", tUser.m_sUser.cstr() ); + return false; + } else if ( pCurUser ) + { + DeleteUser ( tAuth, tUser.m_sUser ); + } + // FIXME!!! add salts collision check + AddUser ( tUser, tAuth ); + dIds.Add ( Hash ( tUser ) ); + } + + return true; +} + +static bool SetPermMember ( const AuthPermsIndex_c::SchemaColumn_e eCol, const SqlInsert_t & tVal, int iRow, UserPerm_t & tPerm, CSphString & sUser, CSphString & sError ) +{ + switch ( eCol ) + { + case AuthPermsIndex_c::SchemaColumn_e::Username: + sUser = tVal.m_sVal; + if ( sUser.IsEmpty() ) + { + sError.SetSprintf ( "empty 'username' at %d document", iRow ); + return false; + } + return true; + + case AuthPermsIndex_c::SchemaColumn_e::Action: + tPerm.m_eAction = ReadAction ( FromStr ( tVal.m_sVal ) ); + if ( tPerm.m_eAction==AuthAction_e::UNKNOWN ) + { + sError.SetSprintf ( "unknown action '%s' at %d document", tVal.m_sVal.cstr(), iRow ); + return false; + } + return true; + + case AuthPermsIndex_c::SchemaColumn_e::Target: + tPerm.m_sTarget = tVal.m_sVal; + tPerm.m_sTarget.Trim(); + tPerm.m_bTargetWildcard = HasWildcard ( tPerm.m_sTarget.cstr() ); + tPerm.m_bTargetWildcardAll = ( tPerm.m_sTarget=="*" ); + if ( tPerm.m_sTarget.IsEmpty() ) + { + sError.SetSprintf ( "empty 'target' at %d document", iRow ); + return false; + } + return true; + + case AuthPermsIndex_c::SchemaColumn_e::Allow: + { + int64_t iVal = tVal.GetValueInt(); + if ( iVal!=0 && iVal!=1 ) + { + sError.SetSprintf ( "wrong allowed '" INT64_FMT "' at %d document", iVal, iRow ); + return false; + } + tPerm.m_bAllow = ( iVal==1 ); + return true; + } + + case AuthPermsIndex_c::SchemaColumn_e::Budget: + { + if ( !tVal.m_sVal.IsEmpty() ) + { + CSphVector tRawBson; + if ( !sphJsonParse ( tRawBson, const_cast( tVal.m_sVal.cstr() ), false, false, false, sError ) ) + { + sError.SetSprintf ( "can not read user 'budget' at %d document, error: %s", iRow, sError.cstr() ); + return false; + } + } + tPerm.m_sBudget = tVal.m_sVal; + + return true; + } + + default: + break; + } + + sError.SetSprintf ( "unknown column %s", AuthPermsIndex_c::GetColumnName ( eCol ) ); + return false; +} + +static CSphFixedVector< int > CheckInsertPerms ( const SqlStmt_t & tStmt, CSphString & sError ) +{ + const int iExp = tStmt.m_iSchemaSz; + // attributes without ID + const int iAttrsCount = (int)AuthPermsIndex_c::SchemaColumn_e::Budget; + CSphFixedVector< int > dMapping ( iAttrsCount ); + + if ( tStmt.m_dInsertSchema.GetLength() ) + { + if ( tStmt.m_dInsertSchema.GetLength()!=iAttrsCount ) + { + sError.SetSprintf ( "column count does not match schema (expected %d, got %d)", tStmt.m_dInsertSchema.GetLength(), iAttrsCount ); + return dMapping; + } + // id calculated from the content + if ( tStmt.m_dInsertSchema.Contains ( AuthPermsIndex_c::GetColumnName ( AuthPermsIndex_c::SchemaColumn_e::Id ) ) ) + { + sError.SetSprintf ( "can not set id column" ); + return dMapping; + } + dMapping[0] = GetSchemaColumn ( AuthPermsIndex_c::SchemaColumn_e::Username, tStmt.m_dInsertSchema, sError ); + dMapping[1] = GetSchemaColumn ( AuthPermsIndex_c::SchemaColumn_e::Action, tStmt.m_dInsertSchema, sError ); + dMapping[2] = GetSchemaColumn ( AuthPermsIndex_c::SchemaColumn_e::Target, tStmt.m_dInsertSchema, sError ); + dMapping[3] = GetSchemaColumn ( AuthPermsIndex_c::SchemaColumn_e::Allow, tStmt.m_dInsertSchema, sError ); + dMapping[4] = GetSchemaColumn ( AuthPermsIndex_c::SchemaColumn_e::Budget, tStmt.m_dInsertSchema, sError ); + if ( !sError.IsEmpty() ) + return dMapping; + } else + { + int iGot = tStmt.m_dInsertValues.GetLength(); + if ( iAttrsCount!=tStmt.m_iSchemaSz ) + { + sError.SetSprintf ( "column count does not match schema (expected %d, got %d)", iAttrsCount, iGot ); + return dMapping; + } + if ( ( iGot % iExp )!=0 ) + { + sError.SetSprintf ( "column count does not match value count (expected %d, got %d)", iExp, iGot ); + return dMapping; + } + + dMapping[0] = 0; + dMapping[1] = 1; + dMapping[2] = 2; + dMapping[3] = 3; + dMapping[4] = 4; + } + + return dMapping; +} + +static bool InsertPerms ( const SqlStmt_t & tStmt, bool bReplace, AuthUsersMutablePtr_t & tAuth, CSphVector & dIds, CSphString & sError ) +{ + CSphFixedVector< int > dMapping = CheckInsertPerms ( tStmt, sError ); + if ( !sError.IsEmpty() ) + return false; + + sph::StringSet hUsers; + + for ( int iRow=0; iRowm_hUserPerms.AddUnique ( sUser ); + + int iReplaced = dPerms.GetFirst ( [&] ( const UserPerm_t & tItem ) { return ( Hash ( sUser, tItem )==tPermHash ); }); + if ( iReplaced!=-1 && !bReplace ) + { + sError.SetSprintf ( "duplicate perms for user '%s', use REPLACE", sUser.cstr() ); + return false; + } else if ( iReplaced!=-1 ) + { + dPerms.Remove ( iReplaced ); + } + + dPerms.Add ( tPerm ); + hUsers.Add ( sUser ); + dIds.Add ( tPermHash ); + } + + for ( const auto & tUser : hUsers ) + SortUserPerms ( tAuth->m_hUserPerms[ tUser.first ] ); + + return true; +} + +bool InsertAuthDocuments ( const SqlStmt_t & tStmt, CSphString & sError ) +{ + CSphVector dIds; + dIds.Reserve ( tStmt.m_iRowsAffected ); + + bool bReplace = ( tStmt.m_eStmt==STMT_REPLACE ); + AuthUsersMutablePtr_t tAuth = CopyAuth(); + + bool bInserted = ( tStmt.m_sIndex==GetIndexNameAuthUsers() ? InsertUsers ( tStmt, bReplace, tAuth, dIds, sError ) : InsertPerms ( tStmt, bReplace, tAuth, dIds, sError ) ); + + if ( bInserted && dIds.GetLength() ) + { + ScopedMutex_t tLock ( g_tAuthLock ); + if ( !SaveAuthFile ( *tAuth.get(), g_sAuthFile, sError ) ) + return 0; + + g_tAuth = std::move ( tAuth ); + + auto * pSession = session::GetClientSession(); + pSession->m_dLastIds.SwapData ( dIds ); + } + + return bInserted; +} \ No newline at end of file diff --git a/src/auth/auth.h b/src/auth/auth.h new file mode 100644 index 0000000000..c6334a611f --- /dev/null +++ b/src/auth/auth.h @@ -0,0 +1,31 @@ +// +// Copyright (c) 2017-2024, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#pragma once + +#include "sphinxstd.h" +#include "sphinxutils.h" +#include "searchdaemon.h" + +void AuthConfigure ( const CSphConfigSection & hSearchd ); +bool IsAuthEnabled(); + +struct AgentConn_t; +void SetSessionAuth ( CSphVector & dRemotes ); +void SetAuth ( const CSphString & sUser, CSphVector & dRemotes ); +void SetAuth ( const CSphString & sUser, AgentConn_t * pAgent ); +bool AuthReload ( CSphString & sError ); + +ServedIndexRefPtr_c MakeDynamicAuthIndex ( const CSphString & sName, StringBuilder_c & sError ); + +int DeleteAuthDocuments ( const CSphString & sName, const SqlStmt_t & tStmt, CSphString & sError ); +bool InsertAuthDocuments ( const SqlStmt_t & tStmt, CSphString & sError ); diff --git a/src/auth/auth_common.cpp b/src/auth/auth_common.cpp new file mode 100644 index 0000000000..3f53fe2b6b --- /dev/null +++ b/src/auth/auth_common.cpp @@ -0,0 +1,433 @@ +// +// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#include "sphinxstd.h" +#include "sphinxutils.h" +#include "fileutils.h" +#include "std/base64.h" +#include "std/fnv64.h" +#include "sphinxjson.h" +#include + +#include "auth_perms.h" +#include "auth_common.h" + +static const CSphString g_sBuddyName ( "system.buddy" ); +static CSphString g_sPrefixAuth ( "system.auth_" ); +static CSphString g_sIndexNameAuthUsers ( "system.auth_users" ); +static CSphString g_sIndexNameAuthPerms ( "system.auth_permissions" ); + +const CSphString & GetPrefixAuth() +{ + return g_sPrefixAuth; +} + +const CSphString & GetIndexNameAuthUsers() +{ + return g_sIndexNameAuthUsers; +} + +const CSphString & GetIndexNameAuthPerms() +{ + return g_sIndexNameAuthPerms; +} + +const CSphString & GetAuthBuddyName() +{ + return g_sBuddyName; +} + +static void FromString ( Str_t sSrc, CSphString & sRes ) +{ + // expects hash in form FIPS-180-1, that is: + // 45f44fd2db02b08b4189abf21e90edd712c9616d + // i.e. 40 symbols hex hash in small letters, space + + ByteBlob_t dBuf ( sRes ); + for ( int i=0; i dRawJson ( iSize + 2 ); + auto iRead = (int64_t)sphReadThrottled ( tReader.GetFD(), dRawJson.begin(), iSize ); + if ( iRead!=iSize ) + { + sError.SetSprintf ( "can not read users from the'%s', error: wrong size %d(%d)", sFile.cstr(), (int)iRead, iSize ); + return nullptr; + } + if ( tReader.GetErrorFlag() ) + { + sError.SetSprintf ( "can not read users from the '%s', error: %s", sFile.cstr(), tReader.GetErrorMessage().cstr() ); + return nullptr; + } + + dRawJson[iSize] = '\0'; // safe gap + dRawJson[iSize+1] = '\0'; + + CSphVector tRawBson; + if ( !sphJsonParse ( tRawBson, dRawJson.begin(), false, false, false, sError ) ) + { + sError.SetSprintf ( "can not read users from the '%s', error: %s", sFile.cstr(), sError.cstr() ); + return nullptr; + } + + bson::Bson_c tBsonSrc ( tRawBson ); + if ( tBsonSrc.IsEmpty() || !tBsonSrc.IsAssoc() ) + { + sError.SetSprintf ( "can not read users from the '%s', error: wrong json", sFile.cstr() ); + return nullptr; + } + + if ( !ReadUsers ( sFile, tBsonSrc, tAuth, sError ) ) + return nullptr; + if ( !ReadPerms ( sFile, tBsonSrc, tAuth, sError ) ) + return nullptr; + + return tAuth; +} + +void AddUser ( const AuthUserCred_t & tEntry, AuthUsersMutablePtr_t & tAuth ) +{ + tAuth->m_hUserToken.Add ( tEntry, tEntry.m_sUser ); + tAuth->m_hHttpToken2User.Add ( tEntry.m_sUser, tEntry.m_sRawBearerSha256 ); + tAuth->m_hApiToken2User.Add ( tEntry.m_sUser, tEntry.m_sBearerSha256 ); +} + +bool ReadUsers ( const CSphString & sFile, bson::Bson_c & tBson, AuthUsersMutablePtr_t & tAuth, CSphString & sError ) +{ + sph::StringSet hSalts; + + bson::Bson_c ( tBson.ChildByName ( "users" ) ).ForEach ( [&] ( const bson::NodeHandle_t & tHandle ) + { + bson::Bson_c tNode { tHandle }; + + AuthUserCred_t tEntry; + tEntry.m_sUser = bson::String ( tNode.ChildByName ( "username" ) ); + tEntry.m_sSalt = ReadHex ( "salt", HASH20_SIZE, tNode, sError ); + + bson::Bson_c tHashes { tNode.ChildByName ( "hashes" ) }; + + CSphString sSha1 = ReadHex ( "password_sha1_no_salt", HASH20_SIZE, tHashes, sError ); + memcpy ( tEntry.m_tPwdSha1.data(), sSha1.cstr(), HASH20_SIZE ); + tEntry.m_sPwdSha256 = ReadHex ( "password_sha256", HASH256_SIZE, tHashes, sError ); + tEntry.m_sBearerSha256 = ReadHex ( "bearer_sha256", HASH256_SIZE, tHashes, sError ); + tEntry.m_sRawBearerSha256 = bson::String ( tHashes.ChildByName ( "bearer_sha256" ) ); + + if ( !sError.IsEmpty() ) + return; + + if ( tNode.HasError() ) + { + sError.SetSprintf ( "can not read users from the '%s', error: %s", sFile.cstr(), tNode.Error() ); + return; + } + if ( tHashes.HasError() ) + { + sError.SetSprintf ( "can not read users from the '%s', error: %s", sFile.cstr(), tHashes.Error() ); + return; + } + if ( tAuth->m_hUserToken ( tEntry.m_sUser ) ) + { + sError.SetSprintf ( "can not read users from the '%s', error: multiple user %s entries", sFile.cstr(), tEntry.m_sUser.cstr() ); + return; + } + if ( hSalts [ tEntry.m_sSalt ] ) + { + sError.SetSprintf ( "can not read users from the '%s', error: multiple salt entries '%s'", sFile.cstr(), BinToHex ( (const BYTE *)tEntry.m_sSalt.cstr(), tEntry.m_sSalt.Length() ).cstr() ); + return; + } + if ( tAuth->m_hHttpToken2User ( tEntry.m_sRawBearerSha256 ) ) + { + sError.SetSprintf ( "can not read users from the '%s', error: multiple bearer entries '%s'", sFile.cstr(), tEntry.m_sRawBearerSha256.cstr() ); + return; + } + + AddUser ( tEntry, tAuth ); + hSalts.Add ( tEntry.m_sSalt ); + }); + if ( !sError.IsEmpty() ) + return false; + + // should fail daemon start if file provided but no users read + if ( tAuth->m_hUserToken.IsEmpty() ) + { + sError.SetSprintf ( "no users read from the file '%s'", sFile.cstr() ); + return false; + } + + return true; +} + +AuthAction_e ReadAction ( Str_t sAction ) +{ + if ( StrEq ( sAction, "read" ) ) + return AuthAction_e::READ; + else if ( StrEq ( sAction, "write" ) ) + return AuthAction_e::WRITE; + else if ( StrEq ( sAction, "schema" ) ) + return AuthAction_e::SCHEMA; + else if ( StrEq ( sAction, "replication" ) ) + return AuthAction_e::REPLICATION; + else if ( StrEq ( sAction, "admin" ) ) + return AuthAction_e::ADMIN; + else + return AuthAction_e::UNKNOWN; +} + +const char * GetActionName ( AuthAction_e eAction ) +{ + switch ( eAction ) + { + case AuthAction_e::READ: return "read"; + case AuthAction_e::WRITE: return "write"; + case AuthAction_e::SCHEMA: return "schema"; + case AuthAction_e::REPLICATION: return "replication"; + case AuthAction_e::ADMIN: return "admin"; + default: + return "unknown"; + break; + } +} + +struct CmpPerm_fn +{ + static inline bool IsLess ( const UserPerm_t & tA, const UserPerm_t & tB ) + { + // 1nd - actions + if ( tA.m_eAction!=tB.m_eAction ) + return ( tA.m_eActionm_hUserToken ( sUser ); + if ( !pUser ) + { + sError.SetSprintf ( "can not read users from the '%s', permission for unknown user: %s", sFile.cstr(), sUser.cstr() ); + return; + } + + UserPerms_t & dPerms = tAuth->m_hUserPerms.AddUnique ( sUser ); + dPerms.Add ( tPerm ); + }); + + if ( !sError.IsEmpty() ) + return false; + + for ( auto & tPerms : tAuth->m_hUserPerms ) + SortUserPerms ( tPerms.second ); + + return true; +} + +static CSphString WriteJson ( const AuthUsers_t & tAuth ) +{ + JsonEscapedBuilder tWriter; + tWriter.StartBlock ( nullptr, "{", "\n}" ); + { + tWriter.StartBlock ( ",\n", "\"users\":[\n", "\n]" ); + for ( const auto & tItUser : tAuth.m_hUserToken ) + { + const AuthUserCred_t & tUser = tItUser.second; + if ( tUser.m_sUser==GetAuthBuddyName() ) + continue; + + tWriter.StartBlock ( ",\n", "{\n", "\n}" ); + tWriter.NamedString ( "username", tUser.m_sUser ); + tWriter.NamedString ( "salt", BinToHex ( (const BYTE *)tUser.m_sSalt.scstr(), tUser.m_sSalt.Length() ) ); + { + tWriter.StartBlock ( ",\n", "\"hashes\":{\n", "\n}" ); + tWriter.NamedString ( "password_sha1_no_salt", BinToHex ( tUser.m_tPwdSha1.data(), tUser.m_tPwdSha1.size() ) ); + tWriter.NamedString ( "password_sha256", BinToHex ( (const BYTE *)tUser.m_sPwdSha256.scstr(), tUser.m_sPwdSha256.Length() ) ); + tWriter.NamedString ( "bearer_sha256", BinToHex ( (const BYTE *)tUser.m_sBearerSha256.scstr(), tUser.m_sBearerSha256.Length() ) ); + tWriter.FinishBlock ( true ); + } + tWriter.FinishBlock ( true ); + } + tWriter.FinishBlock ( true ); + + tWriter.StartBlock ( ",\n", ",\"permissions\":[\n", "\n]" ); + for ( const auto & dPerms : tAuth.m_hUserPerms ) + { + if ( dPerms.first==GetAuthBuddyName() ) + continue; + + for ( const auto & tPerm : dPerms.second ) + { + tWriter.StartBlock ( ",\n", "{\n", "\n}" ); + tWriter.NamedString ( "username", dPerms.first ); + tWriter.NamedString ( "action", GetActionName ( tPerm.m_eAction ) ); + tWriter.NamedString ( "target", tPerm.m_sTarget ); + tWriter.NamedVal ( "allow", tPerm.m_bAllow ); + if ( !tPerm.m_sBudget.IsEmpty() ) + { + tWriter.Named ( "budget" ); + tWriter.AppendRawChunk ( FromStr ( tPerm.m_sBudget ) ); + } + tWriter.FinishBlock ( true ); + } + } + tWriter.FinishBlock ( true ); + } + tWriter.FinishBlock ( true ); + + return CSphString ( tWriter ); +} + +bool SaveAuthFile ( const AuthUsers_t & tAuth, const CSphString & sFile, CSphString & sError ) +{ + CSphString sAuth = WriteJson ( tAuth ); + + CSphString sNewFile; + sNewFile.SetSprintf ( "%s.new", sFile.cstr() ); + + CSphWriter tWriter; + if ( !tWriter.OpenFile ( sNewFile, sError ) ) + return false; + + tWriter.PutBytes ( sAuth.cstr(), sAuth.Length() ); + tWriter.CloseFile(); + if ( tWriter.IsError() ) + return false; + + if ( sph::rename ( sNewFile.cstr(), sFile.cstr() ) ) + { + sError.SetSprintf ( "failed to rename (src=%s, dst=%s, errno=%d, error=%s)", sNewFile.cstr(), sFile.cstr(), errno, strerrorm(errno) ); + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/auth/auth_common.h b/src/auth/auth_common.h new file mode 100644 index 0000000000..a19b57d9f0 --- /dev/null +++ b/src/auth/auth_common.h @@ -0,0 +1,69 @@ +// +// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#pragma once + +#include "sphinxstd.h" +#include "std/openhash.h" +#include "digest_sha1.h" +#include "digest_sha256.h" +#include "sphinxjson.h" + +#include "auth_perms.h" + +struct AuthUserCred_t +{ + CSphString m_sUser; + + CSphString m_sSalt; + HASH20_t m_tPwdSha1; + CSphString m_sPwdSha256; + CSphString m_sBearerSha256; + + CSphString m_sRawBearerSha256; +}; + +class AuthUsers_t +{ +public: + SmallStringHash_T m_hUserToken; + SmallStringHash_T m_hApiToken2User; + SmallStringHash_T m_hHttpToken2User; + + SmallStringHash_T m_hUserPerms; + + AuthUsers_t() = default; + ~AuthUsers_t() = default; + AuthUsers_t ( AuthUsers_t && ) noexcept = default; + AuthUsers_t & operator= ( AuthUsers_t && ) noexcept = default; + AuthUsers_t ( const AuthUsers_t & ) noexcept = default; + AuthUsers_t & operator= ( const AuthUsers_t & ) noexcept = default; +}; + +using AuthUsersPtr_t = std::shared_ptr; +using AuthUsersMutablePtr_t = std::unique_ptr; + +const AuthUsersPtr_t GetAuth(); +AuthUsersMutablePtr_t CopyAuth(); + +AuthUsersMutablePtr_t ReadAuthFile ( const CSphString & sFile, CSphString & sError ); +bool SaveAuthFile ( const AuthUsers_t & tAuth, const CSphString & sFile, CSphString & sError ); + +const CSphString & GetPrefixAuth(); +const CSphString & GetIndexNameAuthUsers(); +const CSphString & GetIndexNameAuthPerms(); +const CSphString & GetAuthBuddyName(); + +void AddUser ( const AuthUserCred_t & tEntry, AuthUsersMutablePtr_t & tAuth ); +CSphString ReadHex ( Str_t sRaw, int iHashLen, CSphString & sError ); +CSphString ReadHex ( const char * sName, int iHashLen, const bson::Bson_c & tNode, CSphString & sError ); +void SortUserPerms ( UserPerms_t & dPerms ); \ No newline at end of file diff --git a/src/auth/auth_perms.cpp b/src/auth/auth_perms.cpp new file mode 100644 index 0000000000..8f1769b150 --- /dev/null +++ b/src/auth/auth_perms.cpp @@ -0,0 +1,62 @@ +// +// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#include "sphinxstd.h" + +#include "auth_common.h" +#include "auth.h" + +#include "auth_perms.h" + +static bool ProcessPerms ( const UserPerms_t * pPerms, AuthAction_e eAction, const CSphString & sTarget, bool bAllowEmpty ) +{ + bool bGotEmptyTarget = sTarget.IsEmpty(); + bool bHasAllow = false; + + for ( const auto & tPerm : *pPerms ) + { + if ( tPerm.m_eAction!=eAction ) + continue; + + if ( !tPerm.m_bTargetWildcard && tPerm.m_sTarget==sTarget ) + return tPerm.m_bAllow; + + if ( tPerm.m_bTargetWildcard && !bGotEmptyTarget && ( tPerm.m_bTargetWildcardAll || sphWildcardMatch ( sTarget.cstr(), tPerm.m_sTarget.cstr() ) ) ) + return tPerm.m_bAllow; + + // actions without target denny only in case no allow rules found + // the resolution goes at the next consequent statements + if ( bAllowEmpty && bGotEmptyTarget ) + { + if ( tPerm.m_bTargetWildcardAll ) + return tPerm.m_bAllow; + + bHasAllow |= tPerm.m_bAllow; + } + } + + return ( bAllowEmpty && bHasAllow ); +} + +bool CheckPerms ( const CSphString & sUser, AuthAction_e eAction, const CSphString & sTarget, bool bAllowEmpty, CSphString & sError ) +{ + assert ( IsAuthEnabled() ); + + AuthUsersPtr_t pUsers = GetAuth(); + const UserPerms_t * pPerms = pUsers->m_hUserPerms ( sUser ); + if ( pPerms && pPerms->GetLength() && ProcessPerms ( pPerms, eAction, sTarget, bAllowEmpty ) ) + return true; + + //sError.SetSprintf ( "Permission denied for user '%s', action(%s) to '%s'", sUser.cstr(), GetActionName ( eAction ), sTarget.scstr() ); // !COMMIT + sError.SetSprintf ( "Permission denied for user '%s'", sUser.cstr() ); + return false; +} diff --git a/src/auth/auth_perms.h b/src/auth/auth_perms.h new file mode 100644 index 0000000000..ad8d3b9877 --- /dev/null +++ b/src/auth/auth_perms.h @@ -0,0 +1,44 @@ +// +// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#pragma once + +#include "sphinxstd.h" + +enum class AuthAction_e +{ + READ, + WRITE, + SCHEMA, + REPLICATION, + ADMIN, + + UNKNOWN +}; + +struct UserPerm_t +{ + AuthAction_e m_eAction; + CSphString m_sTarget; + bool m_bAllow = false; + CSphString m_sBudget; + + bool m_bTargetWildcard = false; + bool m_bTargetWildcardAll = false; +}; + +// FIME!!! add offset for each action inside perms vector +using UserPerms_t = CSphVector; + +bool CheckPerms ( const CSphString & sUser, AuthAction_e eAction, const CSphString & sTarget, bool bAllowEmpty, CSphString & sError ); +AuthAction_e ReadAction ( Str_t sAction ); +const char * GetActionName ( AuthAction_e eAction ); diff --git a/src/auth/auth_proto_api.cpp b/src/auth/auth_proto_api.cpp new file mode 100644 index 0000000000..9e5b4819ed --- /dev/null +++ b/src/auth/auth_proto_api.cpp @@ -0,0 +1,123 @@ +// +// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#include "searchdha.h" + +#include "auth_common.h" +#include "auth_proto_api.h" +#include "auth.h" + +///////////////////////////////////////////////////////////////////////////// +/// API + +int GetApiTokenSize ( ApiAuth_e eType ) +{ + switch ( eType ) + { + case ApiAuth_e::SHA1: return HASH20_SIZE; break; + case ApiAuth_e::SHA256: return HASH256_SIZE; break; + + case ApiAuth_e::NO_AUTH: + default: + return 0; + } +} + +bool CheckAuth ( ApiAuth_e eType, const VecTraits_T & dToken, CSphString & sUser, CSphString & sError ) +{ + sphLogDebug ( "%d, token(%d):0x%s", (int)eType, dToken.GetLength(), BinToHex ( dToken.Begin(), dToken.GetLength() ).cstr() ); + + if ( !IsAuthEnabled() ) + return true; + + CSphString sToken; + sToken.SetBinary ( (const char *)dToken.Begin(), dToken.GetLength() ); + AuthUsersPtr_t pUsers = GetAuth(); + const CSphString * pUser = pUsers->m_hApiToken2User ( sToken ); + if ( !pUser ) + { + sError.SetSprintf ( "Access denied for token '%s'", BinToHex ( dToken.Begin(), dToken.GetLength() ).cstr() ); + return false; + } + + sphLogDebug ( "user %s passed", pUser->cstr() ); + + sUser = *pUser; + return true; +} + +static void SetAuth ( const char * sBearerSha256, int iLen, AgentConn_t * pAgent ) +{ + assert ( sBearerSha256 && iLen>0 ); + + pAgent->m_tAuthToken.m_eType = ApiAuth_e::SHA256; + pAgent->m_tAuthToken.m_dToken.Reset ( iLen ); + memcpy ( pAgent->m_tAuthToken.m_dToken.Begin(), sBearerSha256, iLen ); +} + + +void SetAuth ( const CSphString & sUser, AgentConn_t * pAgent ) +{ + if ( sUser.IsEmpty() || !pAgent ) + return; + + if ( !IsAuthEnabled() ) + return; + + AuthUsersPtr_t pUsers = GetAuth(); + const AuthUserCred_t * pUser = pUsers->m_hUserToken ( sUser ); + if ( !pUser ) + return; + + SetAuth ( pUser->m_sBearerSha256.cstr(), pUser->m_sBearerSha256.Length(), pAgent ); +} + +void SetAuth ( const CSphString & sUser, CSphVector & dRemotes ) +{ + if ( sUser.IsEmpty() || dRemotes.IsEmpty() ) + return; + + if ( !IsAuthEnabled() ) + return; + + AuthUsersPtr_t pUsers = GetAuth(); + const AuthUserCred_t * pUser = pUsers->m_hUserToken ( sUser ); + if ( !pUser ) + return; + + // FIXME!!! replace by FixedVector + int iTokenLen = pUser->m_sBearerSha256.Length(); + const char * sBearerSha256 = pUser->m_sBearerSha256.cstr(); + for ( auto & tClient : dRemotes ) + SetAuth ( sBearerSha256, iTokenLen, tClient ); +} + +void SetSessionAuth ( CSphVector & dRemotes ) +{ + SetAuth ( session::GetUser(), dRemotes ); +} + +bool ApiCheckPerms ( const CSphString & sUser, AuthAction_e eAction, const CSphString & sTarget, ISphOutputBuffer & tOut ) +{ + if ( !IsAuthEnabled() ) + { + sphLogDebug ( "no users found in config, permission granted" ); + return true; + } + + CSphString sError; + if ( CheckPerms ( sUser, eAction, sTarget, false, sError ) ) + return true; + + SendErrorReply ( tOut, "%s", sError.cstr() ); + return false; +} \ No newline at end of file diff --git a/src/auth/auth_proto_api.h b/src/auth/auth_proto_api.h new file mode 100644 index 0000000000..6f4a95a365 --- /dev/null +++ b/src/auth/auth_proto_api.h @@ -0,0 +1,34 @@ +// +// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#pragma once + +#include "sphinxstd.h" +#include "auth_perms.h" + +// API +enum class ApiAuth_e : BYTE +{ + NO_AUTH = 1, + SHA1, + SHA256 +}; +int GetApiTokenSize ( ApiAuth_e eType ); +bool CheckAuth ( ApiAuth_e eType, const VecTraits_T & dToken, CSphString & sUser, CSphString & sError ); + +struct ApiAuthToken_t +{ + ApiAuth_e m_eType = ApiAuth_e::NO_AUTH; + CSphFixedVector m_dToken { 0 }; +}; + +bool ApiCheckPerms ( const CSphString & sUser, AuthAction_e eAction, const CSphString & sTarget, ISphOutputBuffer & tOut ); \ No newline at end of file diff --git a/src/auth/auth_proto_http.cpp b/src/auth/auth_proto_http.cpp new file mode 100644 index 0000000000..0024542b8b --- /dev/null +++ b/src/auth/auth_proto_http.cpp @@ -0,0 +1,148 @@ +// +// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#include "std/base64.h" +#include "searchdhttp.h" + +#include "auth_common.h" +#include "auth_proto_http.h" +#include "auth.h" + +///////////////////////////////////////////////////////////////////////////// +/// HTTP + +static bool CheckPwd ( const AuthUserCred_t & tEntry, const CSphString & sPwd ) +{ + HASH256_t tPwdHash = CalcBinarySHA2 ( sPwd.cstr(), sPwd.Length() ); + + int iCmp = memcmp ( tEntry.m_sPwdSha256.cstr(), tPwdHash.data(), tPwdHash.size() ); + return ( iCmp==0 ); +} + +static bool FailAuth ( HttpProcessResult_t & tRes, CSphVector & dReply ) +{ + tRes.m_eReplyHttpCode = EHTTP_STATUS::_401; + tRes.m_bSkipBuddy = true; + sphHttpErrorReply ( dReply, tRes.m_eReplyHttpCode, tRes.m_sError.cstr() ); + return false; +} + +static Str_t GetTokenFromAuth ( const CSphString & sSrcAuth, int iPrefixLen ) +{ + int iSrcAuthLen = sSrcAuth.Length(); + const char * sCur = sSrcAuth.cstr() + iPrefixLen; + const char * sEnd = sSrcAuth.cstr() + iSrcAuthLen; + while ( sCur & dReply, CSphString & sUser ) +{ + CSphVector dSrcUserPwd; + DecodeBinBase64 ( sSrcUserPwd, dSrcUserPwd ); + + int iDel = dSrcUserPwd.GetFirst ( []( BYTE c ) { return c==':'; } ); + if ( iDel==-1 ) + { + tRes.m_sError = "Failed to find user:password delimiter"; + return FailAuth ( tRes, dReply ); + } + + sUser.SetBinary ( (const char *)dSrcUserPwd.Begin(), iDel ); + CSphString sPwd; + sPwd.SetBinary ( (const char *)dSrcUserPwd.Begin() + iDel + 1, dSrcUserPwd.GetLength()-iDel-1 ); + + AuthUsersPtr_t pUsers = GetAuth(); + const AuthUserCred_t * pUser = pUsers->m_hUserToken ( sUser ); + if ( !pUser || sPwd.IsEmpty() ) + { + tRes.m_sError.SetSprintf ( "Access denied for user '%s' (using password: NO)", sUser.cstr() ); + return FailAuth ( tRes, dReply ); + } + + if ( !CheckPwd ( *pUser, sPwd ) ) + { + tRes.m_sError.SetSprintf ( "Access denied for user '%s' (using password: YES)", sUser.cstr() ); + return FailAuth ( tRes, dReply ); + } else + { + return true; + } +} + +static bool CheckAuthBearer ( const Str_t & sToken, HttpProcessResult_t & tRes, CSphVector & dReply, CSphString & sUser ) +{ + AuthUsersPtr_t pUsers = GetAuth(); + const CSphString * pUser = pUsers->m_hHttpToken2User ( sToken ); + if ( !pUser ) + { + tRes.m_sError.SetSprintf ( "Access denied for token '%s'", sToken.first ); + return FailAuth ( tRes, dReply ); + } + + sUser = *pUser; + return true; +} + + +bool CheckAuth ( const SmallStringHash_T & hOptions, HttpProcessResult_t & tRes, CSphVector & dReply, CSphString & sUser ) +{ + if ( !IsAuthEnabled() ) + { + sphLogDebug ( "no users found in config, access granted" ); + return true; + } + + const CSphString * pSrcAuth = hOptions ( "authorization" ); + if ( !pSrcAuth ) + { + tRes.m_eReplyHttpCode = EHTTP_STATUS::_401; + tRes.m_bSkipBuddy = true; + tRes.m_sError = "Authorization header field missed"; + sphHttpErrorReply ( dReply, tRes.m_eReplyHttpCode, tRes.m_sError.cstr(), R"(WWW-Authenticate: Basic realm="Manticore daemon", charset="UTF-8")" ); + return false; + } + + const char sAuthPrefixBasic[] = "Basic"; + const char sAuthPrefixBearer[] = "Bearer"; + const bool bBasicToken = ( pSrcAuth->Begins ( sAuthPrefixBasic ) ); + const bool bBearerToken = ( !bBasicToken && pSrcAuth->Begins ( sAuthPrefixBearer ) ); + if ( !bBasicToken && !bBearerToken ) + { + tRes.m_sError = "Only Basic and Bearer authorization supported"; + return FailAuth ( tRes, dReply ); + } + + Str_t sAuthTail = GetTokenFromAuth ( *pSrcAuth, ( bBasicToken ? sizeof( sAuthPrefixBasic )-1 : sizeof( sAuthPrefixBearer )-1 ) ); + if ( bBasicToken ) + return CheckAuthBasic ( sAuthTail, tRes, dReply, sUser ); + else + return CheckAuthBearer ( sAuthTail, tRes, dReply, sUser ); +} + +bool HttpCheckPerms ( const CSphString & sUser, AuthAction_e eAction, const CSphString & sTarget, EHTTP_STATUS & eReplyHttpCode, CSphString & sError, CSphVector & dReply ) +{ + if ( !IsAuthEnabled() ) + { + sphLogDebug ( "no users found in config, permission granted" ); + return true; + } + + if ( CheckPerms ( sUser, eAction, sTarget, false, sError ) ) + return true; + + eReplyHttpCode = EHTTP_STATUS::_403; + sphHttpErrorReply ( dReply, eReplyHttpCode, sError.cstr() ); + return false; +} \ No newline at end of file diff --git a/src/auth/auth_proto_http.h b/src/auth/auth_proto_http.h new file mode 100644 index 0000000000..352209fd8c --- /dev/null +++ b/src/auth/auth_proto_http.h @@ -0,0 +1,25 @@ +// +// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#pragma once + +#include "sphinxstd.h" +#include "auth_perms.h" + +// HTTP +struct HttpProcessResult_t; + +bool CheckAuth ( const SmallStringHash_T & hOptions, HttpProcessResult_t & tRes, CSphVector & dReply, CSphString & sUser ); +CSphString GetBuddyToken(); +const CSphString CreateSessionToken(); + +bool HttpCheckPerms ( const CSphString & sUser, AuthAction_e eAction, const CSphString & sTarget, EHTTP_STATUS & eReplyHttpCode, CSphString & sError, CSphVector & dReply ); diff --git a/src/auth/auth_proto_mysql.cpp b/src/auth/auth_proto_mysql.cpp new file mode 100644 index 0000000000..ba5c7832f8 --- /dev/null +++ b/src/auth/auth_proto_mysql.cpp @@ -0,0 +1,305 @@ +// +// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#include "sphinxutils.h" + +#include "searchdsql.h" +#include "searchdaemon.h" +#include "auth_common.h" +#include "auth_proto_mysql.h" +#include "auth.h" + +static void Crypt ( BYTE * pBuf, const BYTE * pS1, const BYTE * pS2, int iLen ) +{ + const BYTE * pEnd = pS1 + iLen; + while ( pS1 & dClientHash ) +{ + const HASH20_t & tSha1 = tEntry.m_tPwdSha1; + sphLogDebug ( "sha1:(%d)0x%s", (int)tSha1.size(), BinToHex ( tSha1 ).cstr() ); + + HASH20_t tSha2 = CalcBinarySHA1 ( tSha1.data(), tSha1.size() ); + sphLogDebug ( "sha2:(%d)0x%s", (int)tSha2.size(), BinToHex ( tSha2 ).cstr() ); + + SHA1_c tSaltSha2Calc; + tSaltSha2Calc.Init(); + tSaltSha2Calc.Update ( (const BYTE *)tSalt.m_dScramble.Begin(), tSalt.m_dScramble.GetLength()-1 ); + tSaltSha2Calc.Update ( tSha2.data(), tSha2.size() ); + HASH20_t tSha3 = tSaltSha2Calc.FinalHash(); + + sphLogDebug ( "sha3:(%d)0x%s", (int)tSha3.size(), BinToHex ( tSha3 ).cstr() ); + + CSphFixedVector dRes ( tSha1.size() ); + Crypt ( dRes.Begin(), tSha3.data(), tSha1.data(), dRes.GetLength() ); + + sphLogDebug ( "buf:(%d)0x%s", (int)dRes.GetLength(), BinToHex ( dRes.Begin(), dRes.GetLength() ).cstr() ); + + int iCmp = memcmp ( dRes.Begin(), dClientHash.Begin(), Min ( dClientHash.GetLength(), dRes.GetLength() ) ); + sphLogDebug ( "pwd:(%d) matched(%d):0x%s", dClientHash.GetLength(), iCmp, BinToHex ( dClientHash.Begin(), dClientHash.GetLength() ).cstr() ); + return ( iCmp==0 ); +} + +bool CheckAuth ( const MySQLAuth_t & tAuth, const CSphString & sUser, const VecTraits_T & dClientHash, CSphString & sError ) +{ + sphLogDebug ( "auth raw:(%d)0x%s", dClientHash.GetLength(), BinToHex ( dClientHash.Begin(), dClientHash.GetLength() ).cstr() ); + + if ( !IsAuthEnabled() ) + { + sphLogDebug ( "no users found in config, user '%s' access granted", sUser.cstr() ); + return true; + } + + AuthUsersPtr_t pUsers = GetAuth(); + const AuthUserCred_t * pUser = pUsers->m_hUserToken ( sUser ); + if ( !pUser || dClientHash.IsEmpty() ) + { + sError.SetSprintf ( "Access denied for user '%s' (using password: NO)", sUser.cstr() ); + return false; + } + + if ( !CheckPwd ( tAuth, *pUser, dClientHash ) ) + { + sError.SetSprintf ( "Access denied for user '%s' (using password: YES)", sUser.cstr() ); + return false; + } else + { + return true; + } +} + +bool SqlCheckPerms ( const CSphString & sUser, const CSphVector & dStmt, CSphString & sError ) +{ + if ( !IsAuthEnabled() ) + { + sphLogDebug ( "no users found in config, permission granted" ); + return true; + } + + if ( !dStmt.GetLength() ) + { + sphLogDebug ( "empty statements, permission granted" ); + return true; + } + + const SqlStmt_t & tStmt = dStmt[0]; + + // handle SQL query + switch ( tStmt.m_eStmt ) + { + case STMT_PARSE_ERROR: return true; + + case STMT_SELECT: + case STMT_CALL: + case STMT_DESCRIBE: + case STMT_SHOW_CREATE_TABLE: + case STMT_SHOW_INDEX_STATUS: + case STMT_SHOW_AGENT_STATUS: + case STMT_SHOW_FEDERATED_INDEX_STATUS: + case STMT_FACET: + case STMT_SHOW_INDEX_SETTINGS: + case STMT_EXPLAIN: + case STMT_SHOW_TABLE_INDEXES: + return CheckPerms ( sUser, AuthAction_e::READ, ( tStmt.m_sIndex.IsEmpty() ? tStmt.m_tQuery.m_sIndexes : tStmt.m_sIndex ), false, sError ); + + // special read actions without index + case STMT_SHOW_WARNINGS: + case STMT_SHOW_META: + case STMT_SHOW_TABLES: + case STMT_SELECT_COLUMNS: + case STMT_SHOW_PROFILE: + case STMT_SHOW_PLAN: + case STMT_SHOW_SCROLL: + return CheckPerms ( sUser, AuthAction_e::READ, tStmt.m_sIndex, true, sError ); + + case STMT_INSERT: + case STMT_REPLACE: + case STMT_DELETE: + case STMT_UPDATE: + case STMT_ATTACH_INDEX: + case STMT_FLUSH_RTINDEX: + case STMT_FLUSH_RAMCHUNK: + case STMT_TRUNCATE_RTINDEX: + case STMT_OPTIMIZE_INDEX: + case STMT_FLUSH_INDEX: + case STMT_ALTER_KLIST_TARGET: + case STMT_FREEZE: + case STMT_UNFREEZE: + return CheckPerms ( sUser, AuthAction_e::WRITE, tStmt.m_sIndex, false, sError ); + + // special write actions without index + case STMT_SET: + case STMT_BEGIN: + case STMT_COMMIT: + case STMT_ROLLBACK: + case STMT_SYSFILTERS: + case STMT_KILL: + case STMT_FLUSH_HOSTNAMES: + case STMT_FLUSH_LOGS: + return CheckPerms ( sUser, AuthAction_e::WRITE, tStmt.m_sIndex, true, sError ); + + case STMT_SHOW_STATUS: + case STMT_CREATE_TABLE: + case STMT_CREATE_TABLE_LIKE: + case STMT_DROP_TABLE: + case STMT_CREATE_FUNCTION: + case STMT_DROP_FUNCTION: + case STMT_SHOW_VARIABLES: + case STMT_SHOW_COLLATION: + case STMT_SHOW_CHARACTER_SET: + case STMT_ALTER_ADD: + case STMT_ALTER_DROP: + case STMT_ALTER_MODIFY: + case STMT_SHOW_DATABASES: + case STMT_CREATE_PLUGIN: + case STMT_DROP_PLUGIN: + case STMT_SHOW_PLUGINS: + case STMT_SHOW_THREADS: + case STMT_ALTER_RECONFIGURE: + case STMT_RELOAD_PLUGINS: + case STMT_RELOAD_INDEX: + case STMT_RELOAD_INDEXES: + case STMT_ALTER_INDEX_SETTINGS: + case STMT_JOIN_CLUSTER: + case STMT_CLUSTER_CREATE: + case STMT_CLUSTER_DELETE: + case STMT_CLUSTER_ALTER_ADD: + case STMT_CLUSTER_ALTER_DROP: + case STMT_CLUSTER_ALTER_UPDATE: + case STMT_IMPORT_TABLE: + case STMT_SHOW_SETTINGS: + case STMT_ALTER_REBUILD_SI: + case STMT_SHOW_LOCKS: + return CheckPerms ( sUser, AuthAction_e::SCHEMA, tStmt.m_sIndex, false, sError ); + + case STMT_RELOAD_AUTH: + case STMT_DEBUG: + case STMT_SHOW_USERS: + case STMT_SHOW_PERMISSIONS: + case STMT_SHOW_TOKEN: + return CheckPerms ( sUser, AuthAction_e::ADMIN, tStmt.m_sIndex, true, sError ); + + default: + break; + } + + // FIXME!!! skip buddy for that failed query + sError.SetSprintf ( "Permission denied for user '%s'", sUser.cstr() ); + return false; +} + +void HandleMysqlShowPerms ( RowBuffer_i & tOut ) +{ + tOut.HeadBegin (); + tOut.HeadColumn ( "Username" ); + tOut.HeadColumn ( "action" ); + tOut.HeadColumn ( "Target" ); + tOut.HeadColumn ( "Allow" ); + tOut.HeadColumn ( "Budget" ); + if ( !tOut.HeadEnd () ) + return; + + if ( IsAuthEnabled() ) + { + AuthUsersPtr_t pUsers = GetAuth(); + for ( const auto & tUser : pUsers->m_hUserPerms ) + { + for ( const auto & tPerm : tUser.second ) + { + tOut.PutString ( tUser.first ); + tOut.PutString ( GetActionName ( tPerm.m_eAction ) ); + tOut.PutString ( tPerm.m_sTarget ); + tOut.PutString ( tPerm.m_bAllow ? "true" : "false" ); + tOut.PutString ( tPerm.m_sBudget ); + if ( !tOut.Commit () ) + return; + } + } + } + + tOut.Eof ( false ); +} + +void HandleMysqlShowUsers ( RowBuffer_i & tOut ) +{ + tOut.HeadBegin (); + tOut.HeadColumn ( "Username" ); + if ( !tOut.HeadEnd () ) + return; + + if ( IsAuthEnabled() ) + { + AuthUsersPtr_t pUsers = GetAuth(); + for ( const auto & tUser : pUsers->m_hUserToken ) + { + tOut.PutString ( tUser.first ); + if ( !tOut.Commit () ) + return; + } + } + + tOut.Eof ( false ); +} + +void HandleMysqlShowToken ( const CSphString & sUser, RowBuffer_i & tOut ) +{ + tOut.HeadBegin (); + tOut.HeadColumn ( "Username" ); + tOut.HeadColumn ( "Token" ); + if ( !tOut.HeadEnd () ) + return; + + if ( IsAuthEnabled() ) + { + AuthUsersPtr_t pUsers = GetAuth(); + const AuthUserCred_t * pUser = pUsers->m_hUserToken ( sUser ); + + if ( pUser ) + { + tOut.PutString ( sUser ); + tOut.PutString ( pUser->m_sRawBearerSha256 ); + + if ( !tOut.Commit () ) + return; + } + } + + tOut.Eof ( false ); + +} \ No newline at end of file diff --git a/src/auth/auth_proto_mysql.h b/src/auth/auth_proto_mysql.h new file mode 100644 index 0000000000..0eb46681f2 --- /dev/null +++ b/src/auth/auth_proto_mysql.h @@ -0,0 +1,33 @@ +// +// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2011-2016, Andrew Aksyonoff +// Copyright (c) 2011-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#pragma once + +#include "sphinxstd.h" +#include "auth_perms.h" + +struct MySQLAuth_t +{ + static constexpr BYTE SCRAMBLE_LENGTH = 20; + CSphFixedVector m_dScramble { SCRAMBLE_LENGTH + 1 }; +}; + +// SphixnQL +MySQLAuth_t GetMySQLAuth(); +bool CheckAuth ( const MySQLAuth_t & tAuth, const CSphString & sUser, const VecTraits_T & dClientHash, CSphString & sError ); + +struct SqlStmt_t; +bool SqlCheckPerms ( const CSphString & sUser, const CSphVector & dStmt, CSphString & sError ); + +void HandleMysqlShowPerms ( RowBuffer_i & tOut ); +void HandleMysqlShowUsers ( RowBuffer_i & tOut ); +void HandleMysqlShowToken ( const CSphString & sUser, RowBuffer_i & tOut ); \ No newline at end of file diff --git a/src/digest_sha1.cpp b/src/digest_sha1.cpp index 71cc179582..864e01631b 100644 --- a/src/digest_sha1.cpp +++ b/src/digest_sha1.cpp @@ -223,6 +223,14 @@ CSphString CalcSHA1 ( const void * pData, int iLen ) return BinToHex ( dHashValue ); } +HASH20_t CalcBinarySHA1 ( const void* pData, int iLen ) +{ + SHA1_c dHasher; + dHasher.Init(); + dHasher.Update ( (const BYTE*)pData, iLen ); + return dHasher.FinalHash(); +} + bool CalcSHA1 ( const CSphString & sFileName, CSphString & sRes, CSphString & sError ) { CSphAutofile tFile ( sFileName, SPH_O_READ, sError, false ); @@ -321,8 +329,7 @@ CSphString TaggedHash20_t::ToFIPS () const return sResult.cstr(); } -namespace { -inline BYTE hex_char ( unsigned char c ) noexcept +BYTE HexChar ( unsigned char c ) noexcept { if ( c>=0x30 && c<=0x39 ) return c - '0'; @@ -331,7 +338,6 @@ inline BYTE hex_char ( unsigned char c ) noexcept assert ( false && "broken hex num - expected digits and a..f letters in the num" ); return 0; } -} // de-serialize from FIPS, returns len of parsed chunk of sFIPS or -1 on error int TaggedHash20_t::FromFIPS ( const char * sFIPS ) @@ -346,8 +352,8 @@ int TaggedHash20_t::FromFIPS ( const char * sFIPS ) for ( auto i = 0; i& dHash ); CSphString BinToHex ( const HASH20_t& dHash ); -CSphString BinToHexx ( const BYTE* pHash, int iLen ); +CSphString BinToHex ( const BYTE* pHash, int iLen ); CSphString CalcSHA1 ( const void* pData, int iLen ); bool CalcSHA1 ( const CSphString& sFileName, CSphString& sRes, CSphString& sError ); +HASH20_t CalcBinarySHA1 ( const void * pData, int iLen ); +BYTE HexChar ( unsigned char c ) noexcept; diff --git a/src/digest_sha256.cpp b/src/digest_sha256.cpp new file mode 100644 index 0000000000..224c197663 --- /dev/null +++ b/src/digest_sha256.cpp @@ -0,0 +1,79 @@ +// +// Copyright (c) 2017-2024, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2001-2016, Andrew Aksyonoff +// Copyright (c) 2008-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + + +#include "sphinxstd.h" +#include "fileio.h" +#include "fileutils.h" +#include "indexsettings.h" +#include "digest_sha1.h" + +#include "digest_sha256.h" + +#if WITH_SSL +#define USE_SHA1_FROM_OPENSSL +#include +#endif + +////////////////////////////////////////////////////////////////////////// +// SHA256 digests +////////////////////////////////////////////////////////////////////////// + +class SHA256_c : public SHA256_i +{ +public: + void Init() final + { + m_pCtx = EVP_MD_CTX_create(); + EVP_DigestInit_ex ( m_pCtx, EVP_sha256(), nullptr ); + } + + void Update ( const BYTE * data, int len ) final + { + EVP_DigestUpdate ( m_pCtx, data, len ); + } + + void Final ( HASH256_t & tDigest ) final + { + unsigned int uLen = HASH256_SIZE; + EVP_DigestFinal_ex ( m_pCtx, tDigest.data(), &uLen ); + EVP_MD_CTX_destroy ( m_pCtx ); + } + + HASH256_t FinalHash() final + { + HASH256_t dDigest {}; + Final ( dDigest ); + return dDigest; + } + +private: + EVP_MD_CTX * m_pCtx = nullptr; +}; + +SHA256_i * CreateSHA256() +{ + return new SHA256_c(); +} + +HASH256_t CalcBinarySHA2 ( const void * pData, int iLen ) +{ + SHA256_c dHasher; + dHasher.Init(); + dHasher.Update ( (const BYTE*)pData, iLen ); + return dHasher.FinalHash(); +} + +CSphString BinToHex ( const HASH256_t & dHash ) +{ + return BinToHex ( dHash.data(), dHash.size() ); +} \ No newline at end of file diff --git a/src/digest_sha256.h b/src/digest_sha256.h new file mode 100644 index 0000000000..7fec9096dc --- /dev/null +++ b/src/digest_sha256.h @@ -0,0 +1,35 @@ +// +// Copyright (c) 2017-2024, Manticore Software LTD (https://manticoresearch.com) +// Copyright (c) 2001-2016, Andrew Aksyonoff +// Copyright (c) 2008-2016, Sphinx Technologies Inc +// All rights reserved +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License. You should have +// received a copy of the GPL license along with this program; if you +// did not, you can find it at http://www.gnu.org/ +// + +#pragma once + +#include "sphinxstd.h" +#include + +/// SHA256 digests +static constexpr int HASH256_SIZE = 32; +using HASH256_t = std::array; + +class SHA256_i +{ +public: + virtual ~SHA256_i() = default; + virtual void Init() = 0; + virtual void Update ( const BYTE * pData, int iLen ) = 0; + virtual void Final ( HASH256_t & tDigest ) = 0; + virtual HASH256_t FinalHash() = 0; +}; + +SHA256_i * CreateSHA256(); +HASH256_t CalcBinarySHA2 ( const void * pData, int iLen ); +CSphString BinToHex ( const HASH256_t & dHash ); + diff --git a/src/dynamic_idx.cpp b/src/dynamic_idx.cpp index 8c1111c3c2..dc3c50365e 100644 --- a/src/dynamic_idx.cpp +++ b/src/dynamic_idx.cpp @@ -479,22 +479,9 @@ class FeederSchema_c : public RowBuffer_i void Add ( BYTE ) override {} }; -class GenericTableIndex_c : public CSphIndexStub -{ -public: - GenericTableIndex_c () - : CSphIndexStub ( "dynamic", nullptr ) - {} - - bool MultiQuery ( CSphQueryResult & , const CSphQuery & , const VecTraits_T &, const CSphMultiQueryArgs & ) const final; - -private: - bool MultiScan ( CSphQueryResult & tResult, const CSphQuery & tQuery, const VecTraits_T & dSorters, const CSphMultiQueryArgs & tArgs ) const; - - virtual void SetSorterStuff ( CSphMatch * pMatch ) const = 0; - virtual bool FillNextMatch () const = 0; - virtual Str_t GetErrors() const = 0; -}; +GenericTableIndex_c::GenericTableIndex_c() + : CSphIndexStub ( "dynamic", nullptr ) +{} bool GenericTableIndex_c::MultiQuery ( CSphQueryResult & tResult, const CSphQuery & tQuery, const VecTraits_T & dAllSorters, const CSphMultiQueryArgs &tArgs ) const @@ -669,7 +656,8 @@ bool GenericTableIndex_c::MultiScan ( CSphQueryResult & tResult, const CSphQuery SwitchProfile ( pProfiler, SPH_QSTATE_FINALIZE ); // do final expression calculations - if ( tCtx.m_dCalcFinal.GetLength () ) + // even if no final stage calc to finilize doc collector + if ( tCtx.m_dCalcFinal.GetLength() || m_bForceFinalize ) { DynMatchProcessor_c tFinal ( tArgs.m_iTag, tCtx ); dSorters.Apply ( [&] ( ISphMatchSorter * p ) { p->Finalize ( tFinal, false, tArgs.m_bFinalizeSorters ); } ); @@ -780,7 +768,7 @@ Str_t DynamicIndexSchema_c::GetErrors () const return FromStr ( m_tFeeder.GetError() ); } -static ServedIndexRefPtr_c MakeServed ( CSphIndex* pIndex ) +ServedIndexRefPtr_c MakeServed ( CSphIndex* pIndex ) { auto pServed = MakeServedIndex(); pServed->SetIdx ( std::unique_ptr ( pIndex ) ); diff --git a/src/dynamic_idx.h b/src/dynamic_idx.h index 356f82fad0..68d17fa005 100644 --- a/src/dynamic_idx.h +++ b/src/dynamic_idx.h @@ -23,3 +23,22 @@ ServedIndexRefPtr_c MakeDynamicIndex ( TableFeeder_fn fnFeed ); // schema of any generic table of columns as index with possibility to filter (full-scan) ServedIndexRefPtr_c MakeDynamicIndexSchema ( TableFeeder_fn fnFeed ); + +class GenericTableIndex_c : public CSphIndexStub +{ +public: + GenericTableIndex_c (); + bool MultiQuery ( CSphQueryResult & , const CSphQuery & , const VecTraits_T &, const CSphMultiQueryArgs & ) const final; + +private: + bool MultiScan ( CSphQueryResult & tResult, const CSphQuery & tQuery, const VecTraits_T & dSorters, const CSphMultiQueryArgs & tArgs ) const; + + virtual void SetSorterStuff ( CSphMatch * pMatch ) const = 0; + virtual bool FillNextMatch () const = 0; + virtual Str_t GetErrors() const = 0; + +protected: + bool m_bForceFinalize = false; +}; + +ServedIndexRefPtr_c MakeServed ( CSphIndex * pIndex ); \ No newline at end of file diff --git a/src/netreceive_api.cpp b/src/netreceive_api.cpp index 71cb489e3b..ce6c9340f4 100644 --- a/src/netreceive_api.cpp +++ b/src/netreceive_api.cpp @@ -11,6 +11,7 @@ // #include "netreceive_api.h" +#include "auth/auth.h" extern int g_iClientTimeoutS; // from searchd.cpp extern volatile bool g_bMaintenance; @@ -52,9 +53,9 @@ void ApiServe ( std::unique_ptr pBuf ) } auto uHandshake = tIn.GetDword(); sphLogDebugv ( "conn %s(%d): got handshake, major v.%d", sClientIP, iCID, uHandshake ); - if ( uHandshake!=SPHINX_CLIENT_VERSION && uHandshake!=0x01000000UL ) + if ( uHandshake!=SPHINX_CLIENT_VERSION && uHandshake!=1 && uHandshake!=0x01000000UL ) { - sphLogDebugv ( "conn %s(%d): got handshake, major v.%d", sClientIP, iCID, uHandshake ); + sphLogDebugv ( "conn %s(%d): got invalid handshake, major v.%d", sClientIP, iCID, uHandshake ); return; } // legacy client - sends us exactly 4 bytes of handshake, so we have to flush our handshake also before continue. @@ -132,11 +133,39 @@ void ApiServe ( std::unique_ptr pBuf ) iPconnIdleS = 0; + ApiAuth_e eApiAuth = ApiAuth_e::NO_AUTH; + CSphFixedVector dApiToken ( 0 ); + // can support old protocol version if daemon runs without auth + if ( uHandshake!=1 ) + { + eApiAuth = (ApiAuth_e)tIn.GetByte(); + int iApiTokenSize = GetApiTokenSize ( eApiAuth ); + dApiToken.Reset ( iApiTokenSize ); + if ( iApiTokenSize ) + tIn.GetBytes ( dApiToken.Begin(), iApiTokenSize ); + } + auto eCommand = (SearchdCommand_e) tIn.GetWord (); auto uVer = tIn.GetWord (); auto iReplySize = tIn.GetInt (); sphLogDebugv ( "read command %d, version %d, reply size %d", eCommand, uVer, iReplySize ); + CSphString sUser; + // need to pass by the only commands: + // - PING + // - PERSIST + if ( eCommand!=SEARCHD_COMMAND_PING && eCommand!=SEARCHD_COMMAND_PERSIST ) + { + CSphString sError; + if ( !CheckAuth ( eApiAuth, dApiToken, sUser, sError ) ) + { + SendErrorReply ( tOut, "%s", sError.cstr() ); + tOut.Flush(); // no need to check return code since we anyway break + break; + } + } + // should set client user to pass it further into distributed index + session::SetUser ( sUser ); bool bCheckLen = ( eCommand!= SEARCHD_COMMAND_CLUSTER ); bool bBadCommand = ( eCommand>=SEARCHD_COMMAND_WRONG ); @@ -185,7 +214,7 @@ void ApiServe ( std::unique_ptr pBuf ) { sphWarning ( "%s", g_sMaxedOutMessage.first ); { - auto tHdr = APIHeader ( tOut, SEARCHD_RETRY ); + auto tHdr = APIAnswer ( tOut, 0, SEARCHD_RETRY ); tOut.SendString ( g_sMaxedOutMessage ); } tOut.Flush(); // no need to check return code since we anyway break @@ -217,7 +246,7 @@ void ApiServe ( std::unique_ptr pBuf ) // Start Sphinx API command/request header -APIBlob_c APIHeader ( ISphOutputBuffer & dBuff, WORD uCommand, WORD uVer ) +static APIBlob_c APIHeader ( ISphOutputBuffer & dBuff, WORD uCommand, WORD uVer ) { dBuff.SendWord ( uCommand ); dBuff.SendWord ( uVer ); @@ -229,3 +258,14 @@ APIBlob_c APIAnswer ( ISphOutputBuffer & dBuff, WORD uVer, WORD uStatus ) { return APIHeader ( dBuff, uStatus, uVer ); } + +APIBlob_c APIHeader ( ISphOutputBuffer & dBuff, WORD uCommand, WORD uVer, const ApiAuthToken_t & tToken ) +{ + assert ( GetApiTokenSize ( tToken.m_eType )==tToken.m_dToken.GetLength() ); + + dBuff.SendByte ( (BYTE)tToken.m_eType ); + if ( tToken.m_dToken.GetLength() ) + dBuff.SendBytes ( tToken.m_dToken ); + + return APIHeader ( dBuff, uCommand, uVer ); +} diff --git a/src/netreceive_ql.cpp b/src/netreceive_ql.cpp index 99b8ba4213..133183c4ea 100644 --- a/src/netreceive_ql.cpp +++ b/src/netreceive_ql.cpp @@ -16,6 +16,7 @@ #include "compressed_zlib_mysql.h" #include "compressed_zstd_mysql.h" #include "searchdbuddy.h" +#include "auth/auth_proto_mysql.h" extern int g_iClientQlTimeoutS; // sec extern volatile bool g_bMaintenance; @@ -722,7 +723,6 @@ struct CLIENT // handshake package we send to client class HandshakeV10_c { - static constexpr BYTE AUTH_DATA_LEN = 21; const BYTE m_uVersion = 0x0A; // protocol version 10 const BYTE m_uCharSet = MYSQL_CHARSET::utf8_general_ci; const WORD m_uServerStatusFlag = MYSQL_FLAG::STATUS_AUTOCOMMIT; @@ -730,7 +730,7 @@ class HandshakeV10_c Str_t m_sVersionString; DWORD m_uConnID; - std::array m_sAuthData {}; + const MySQLAuth_t & m_tAuth; DWORD m_uCapabilities = CLIENT::CONNECT_WITH_DB | CLIENT::PROTOCOL_41 | CLIENT::RESERVED2 // deprecated @@ -741,8 +741,9 @@ class HandshakeV10_c | ( bSendOkInsteadofEOF ? CLIENT::DEPRECATE_EOF : 0 ); public: - explicit HandshakeV10_c( DWORD uConnID ) + explicit HandshakeV10_c( DWORD uConnID, const MySQLAuth_t & tAuth ) : m_uConnID ( uConnID ) + , m_tAuth ( tAuth ) { static bool bExtraCapabilitiesSet = false; static WORD uExtraCapabilities = 0; @@ -753,17 +754,6 @@ class HandshakeV10_c } m_uCapabilities |= uExtraCapabilities; - // fill scramble auth data (random) - DWORD i = 0; - DWORD uRand = sphRand() | 0x01010101; - for ( ; i < AUTH_DATA_LEN - sizeof ( DWORD ); i += sizeof ( DWORD ) ) - { - memcpy ( m_sAuthData.data() + i, &uRand, sizeof ( DWORD ) ); - uRand = sphRand() | 0x01010101; - } - if ( i < AUTH_DATA_LEN ) - memcpy ( m_sAuthData.data() + i, &uRand, AUTH_DATA_LEN - i ); - memset ( m_sAuthData.data() + AUTH_DATA_LEN - 1, 0, 1); // version string (plus 0-terminator) m_sVersionString = FromStr ( g_sMySQLVersion ); ++m_sVersionString.second; // encount also z-terminator @@ -793,7 +783,7 @@ class HandshakeV10_c constexpr int iFillerSize = 10; const std::array dFiller { 0 }; - sphLogDebugv ( "Sending handshake..." ); + sphLogDebug ( "Sending handshake..." ); SQLPacketHeader_c tHeader { tOut }; @@ -801,15 +791,15 @@ class HandshakeV10_c tOut.SendByte ( m_uVersion ); tOut.SendBytes ( m_sVersionString ); tOut.SendLSBDword ( m_uConnID ); - tOut.SendBytes ( m_sAuthData.data(), 8 ); + tOut.SendBytes ( m_tAuth.m_dScramble.Begin(), 8 ); tOut.SendByte ( 0 ); tOut.SendLSBWord ( m_uCapabilities & 0xFFFF ); tOut.SendByte ( m_uCharSet ); tOut.SendLSBWord ( m_uServerStatusFlag ); tOut.SendLSBWord ( m_uCapabilities >> 16 ); - tOut.SendByte ( AUTH_DATA_LEN ); + tOut.SendByte ( m_tAuth.m_dScramble.GetLength() ); tOut.SendBytes ( dFiller.data(), iFillerSize ); - tOut.SendBytes ( &m_sAuthData[8], AUTH_DATA_LEN - 8 ); + tOut.SendBytes ( &m_tAuth.m_dScramble[8], m_tAuth.m_dScramble.GetLength() - 8 ); tOut.SendBytes ( m_sAuthPluginName ); } }; @@ -818,7 +808,7 @@ class HandshakeV10_c class HandshakeResponse41 { CSphString m_sLoginUserName; - CSphString m_sAuthResponse; + CSphFixedVector m_dAuthResponse { 0 }; CSphString m_sDatabase; CSphString m_sClientPluginName; SmallStringHash_T m_hAttributes; @@ -829,7 +819,7 @@ class HandshakeResponse41 public: // see https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_response.html for ref - explicit HandshakeResponse41 ( AsyncNetInputBuffer_c& tRawIn, int iPacketLen ) + explicit HandshakeResponse41 ( AsyncNetInputBuffer_c & tRawIn, int iPacketLen ) { InputBuffer_c tIn { tRawIn.PopTail ( iPacketLen ) }; m_uCapabilities = tIn.GetLSBDword(); @@ -838,46 +828,54 @@ class HandshakeResponse41 m_uCharset = tIn.GetByte(); tIn.SetBufferPos ( tIn.GetBufferPos() + 23 ); - sphLogDebugv ( "HandshakeResponse41. PackedLen=%d, hasBytes=%d", iPacketLen, tIn.HasBytes() ); + sphLogDebug ( "HandshakeResponse41. PackedLen=%d, hasBytes=%d", iPacketLen, tIn.HasBytes() ); // ssl auth is finished here if ( tIn.HasBytes() <=0 ) return; // login name m_sLoginUserName = MysqlReadSzStr ( tIn ); - sphLogDebugv ( "User: %s", m_sLoginUserName.cstr() ); + sphLogDebug ( "User: %s", m_sLoginUserName.cstr() ); // auth + int iAuthLen = 0; if ( m_uCapabilities & CLIENT::PLUGIN_AUTH_LENENC_CLIENT_DATA ) - m_sAuthResponse = MysqlReadVlStr ( tIn ); + { + iAuthLen = MysqlReadPackedInt ( tIn ); + } else { - auto uLen = tIn.GetByte(); - m_sAuthResponse = tIn.GetRawString ( uLen ); + iAuthLen = tIn.GetByte(); } + m_dAuthResponse.Reset ( iAuthLen ); + if ( iAuthLen ) + tIn.GetBytes ( m_dAuthResponse.Begin(), iAuthLen ); // db name if ( m_uCapabilities & CLIENT::CONNECT_WITH_DB ) { m_sDatabase = MysqlReadSzStr ( tIn ); - sphLogDebugv ( "DB: %s", m_sDatabase.cstr() ); + sphLogDebug ( "DB: %s", m_sDatabase.cstr() ); } // db name if ( m_uCapabilities & CLIENT::PLUGIN_AUTH ) + { m_sClientPluginName = MysqlReadSzStr ( tIn ); + sphLogDebug ( "plugin: %s", m_sClientPluginName.cstr() ); + } // attributes if ( m_uCapabilities & CLIENT::CONNECT_ATTRS ) { auto iWatermark = MysqlReadPackedInt ( tIn ); - sphLogDebugv ( "%d bytes of attrs", (int) iWatermark ); + sphLogDebug ( "%d bytes of attrs", (int) iWatermark ); iWatermark = tIn.HasBytes() - iWatermark; while ( iWatermark < tIn.HasBytes() ) { auto sKey = MysqlReadVlStr ( tIn ); auto sVal = MysqlReadVlStr ( tIn ); - sphLogDebugv ( "%s: %s", sKey.cstr(), sVal.cstr() ); + sphLogDebug ( "%s: %s", sKey.cstr(), sVal.cstr() ); m_hAttributes.Add ( std::move ( sVal ), sKey ); } } @@ -921,6 +919,11 @@ class HandshakeResponse41 { return ( m_uCapabilities & CLIENT::DEPRECATE_EOF ) != 0; } + + const VecTraits_T & GetAuthResponce() const noexcept + { + return m_dAuthResponse; + } }; @@ -936,7 +939,7 @@ static bool LoopClientMySQL ( BYTE & uPacketID, int iPacketLen, QueryProfile_c * const BYTE uMysqlCmd = tIn.GetByte (); if ( uMysqlCmd!=MYSQL_COM_QUERY ) - sphLogDebugv ( "LoopClientMySQL command %d", uMysqlCmd ); + sphLogDebug ( "LoopClientMySQL command %d", uMysqlCmd ); if ( uMysqlCmd==MYSQL_COM_QUIT ) return false; @@ -975,7 +978,7 @@ static bool LoopClientMySQL ( BYTE & uPacketID, int iPacketLen, QueryProfile_c * myinfo::SetDescription ( CSphString ( tSrcQueryReference ), tSrcQueryReference.second ); // OPTIMIZE? could be huge, but string is hazard. AT_SCOPE_EXIT ( []() { myinfo::SetDescription ( {}, 0 ); } ); assert ( !tIn.GetError() ); - sphLogDebugv ( "LoopClientMySQL command %d, '%s'", uMysqlCmd, myinfo::UnsafeDescription().first ); + sphLogDebug ( "LoopClientMySQL command %d, '%s'", uMysqlCmd, myinfo::UnsafeDescription().first ); tSess.SetTaskState ( TaskState_e::QUERY ); SqlRowBuffer_c tRows ( &uPacketID, &tOut ); @@ -1010,7 +1013,7 @@ static bool LoopClientMySQL ( BYTE & uPacketID, int iPacketLen, QueryProfile_c * if ( uBytesConsumed pBuf ) /// So, no passive probing possible. // send handshake first tSess.SetTaskState ( TaskState_e::HANDSHAKE ); - HandshakeV10_c tHandshake ( iCID ); + + MySQLAuth_t tAuth = GetMySQLAuth(); + HandshakeV10_c tHandshake ( iCID, tAuth ); tHandshake.SetCanSsl ( CheckWeCanUseSSL() ); // fixme! SSL capability must be set only if keys are valid! tHandshake.SetCanZlib( bCanZlibCompression ); tHandshake.SetCanZstd( bCanZstdCompression ); @@ -1132,7 +1137,7 @@ void SqlServe ( std::unique_ptr pBuf ) // get next packet // we want interruptible calls here, so that shutdowns could be honored - sphLogDebugv ( "Receiving command... %d bytes in buf", pIn->HasBytes() ); + sphLogDebug ( "Receiving command... %d bytes in buf", pIn->HasBytes() ); // setup per-query profiling auto pProfile = session::StartProfiling ( SPH_QSTATE_TOTAL ); @@ -1168,7 +1173,7 @@ void SqlServe ( std::unique_ptr pBuf ) uPacketID = 1+(BYTE) ( uAddon >> 24 ); iChunkLen = ( uAddon & MAX_PACKET_LEN ); - sphLogDebugv ( "AsyncReadMySQLPacketHeader returned %d len...", iChunkLen ); + sphLogDebug ( "AsyncReadMySQLPacketHeader returned %d len...", iChunkLen ); iPacketLen += iChunkLen; if ( !bAuthed && ( uAddon == SPHINX_CLIENT_VERSION || uAddon == 0x01000000UL ) ) @@ -1220,7 +1225,17 @@ void SqlServe ( std::unique_ptr pBuf ) if ( tResponse.GetUsername() == "FEDERATED" ) session::SetFederatedUser(); + // should set client user to pass it further into distributed index session::SetUser ( tResponse.GetUsername() ); + + if ( !CheckAuth ( tAuth, tResponse.GetUsername(), tResponse.GetAuthResponce(), sError ) ) + { + LogNetError ( sError.cstr() ); + SendMysqlErrorPacket ( *pOut, uPacketID, FromStr ( sError ), EMYSQL_ERR::UNKNOWN_COM_ERROR ); + pOut->Flush (); + return; + } + SendMysqlOkPacket ( *pOut, uPacketID, session::IsAutoCommit(), session::IsInTrans ()); tSess.SetPersistent ( pOut->Flush () ); bAuthed = true; diff --git a/src/networking_daemon.cpp b/src/networking_daemon.cpp index 45ed7bb5e9..bd7b58658f 100644 --- a/src/networking_daemon.cpp +++ b/src/networking_daemon.cpp @@ -962,7 +962,8 @@ Proto_e AsyncNetInputBuffer_c::Probe() auto tBlob = Tail (); if ( tBlob.second >=4 ) { - if ( !memcmp (tBlob.first,"\0\0\0\1",4) ) + // check for head with SPHINX_CLIENT_VERSION (1 or 2) + if ( memcmp ( tBlob.first, "\0\0\0\1", 4 )==0 || memcmp ( tBlob.first, "\0\0\0\2", 4 )==0 ) { sBytes << "SphinxAPI, usual byte order"; eResult = Proto_e::SPHINX; diff --git a/src/query_status.cpp b/src/query_status.cpp index 8ac5989c10..9744b5db5e 100644 --- a/src/query_status.cpp +++ b/src/query_status.cpp @@ -381,7 +381,7 @@ void QueryStatus ( CSphVariant * v ) REQUIRES ( MainThread ) NetOutputBuffer_c tOut ( iSock ); tOut.SendDword ( SPHINX_CLIENT_VERSION ); { - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, ApiAuthToken_t() ); tOut.SendInt ( 1 ); // dummy body } tOut.Flush (); diff --git a/src/replication/api_command_cluster.cpp b/src/replication/api_command_cluster.cpp index 65647db1b0..452cfd63a6 100644 --- a/src/replication/api_command_cluster.cpp +++ b/src/replication/api_command_cluster.cpp @@ -112,6 +112,9 @@ void HandleAPICommandCluster ( ISphOutputBuffer & tOut, WORD uCommandVer, InputB if ( !bNodeVer && !CheckCommandVersion ( uCommandVer, VER_COMMAND_CLUSTER, tOut ) ) return; + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::REPLICATION, CSphString(), tOut ) ) + return; + if ( eClusterCmd!=E_CLUSTER::FILE_SEND ) sphLogDebugRpl ( "remote cluster command %d(%s), client %s", (int) eClusterCmd, szClusterCmd (eClusterCmd), szClient ); @@ -175,7 +178,7 @@ void HandleAPICommandCluster ( ISphOutputBuffer & tOut, WORD uCommandVer, InputB auto szError = TlsMsg::szError(); sphLogDebugRpl ( "remote cluster '%s' command %s(%d), client %s - %s", sCluster.scstr(), szClusterCmd ( eClusterCmd ), (int)eClusterCmd, szClient, szError ); - auto tReply = APIHeader ( tOut, SEARCHD_ERROR ); + auto tReply = APIAnswer ( tOut, 0, SEARCHD_ERROR ); tOut.SendString ( SphSprintf ( "[%s] %s", szIncomingIP(), szError ).cstr() ); ReportClusterError ( sCluster, szError, szClient, eClusterCmd ); diff --git a/src/replication/api_command_cluster.h b/src/replication/api_command_cluster.h index 345e88dabd..a108b08c48 100644 --- a/src/replication/api_command_cluster.h +++ b/src/replication/api_command_cluster.h @@ -91,6 +91,8 @@ struct CustomAgentData_T final: public DefaultQueryResult_t // base of API commands request and reply builders AgentConn_t* CreateAgentBase ( const AgentDesc_t& tDesc, int64_t iTimeoutMs ); +void SetAuth ( const CSphString & sUser, CSphVector & dRemotes ); +void SetAuth ( const CSphString & sUser, AgentConn_t * pAgent ); // set to true to see all proto exchanging in the log constexpr bool VERBOSE_LOG = false; @@ -116,19 +118,25 @@ class ClusterCommand_T: public RequestBuilder_i, public ReplyParser_i return pResult->m_tRequest; } - static AgentConn_t* CreateAgent ( const AgentDesc_t& tDesc, int64_t iTimeoutMs, const REQUEST& tReq ) + static AgentConn_t * CreateAgent ( const AgentDesc_t & tDesc, const CSphString & sUser, int64_t iTimeoutMs, const REQUEST & tReq ) { auto* pAgent = CreateAgentBase ( tDesc, iTimeoutMs ); pAgent->m_pResult = std::make_unique> ( tReq ); + SetAuth ( sUser, pAgent ); return pAgent; } - static VecRefPtrs_t MakeAgents ( const VecTraits_T& dDesc, int64_t iTimeout, const REQUEST& tReq ) + static VecRefPtrs_t MakeAgents ( const VecTraits_T & dDesc, const CSphString & sUser, int64_t iTimeout, const REQUEST & tReq ) { VecRefPtrs_t dNodes; dNodes.Resize ( dDesc.GetLength() ); ARRAY_FOREACH ( i, dDesc ) - dNodes[i] = CreateAgent ( dDesc[i], iTimeout, tReq ); + { + auto * pAgent = CreateAgentBase ( dDesc[i], iTimeout ); + pAgent->m_pResult = std::make_unique> ( tReq ); + dNodes[i] = pAgent; + } + SetAuth ( sUser, dNodes ); return dNodes; } @@ -146,22 +154,24 @@ class ClusterCommand_T: public RequestBuilder_i, public ReplyParser_i void BuildRequest ( const AgentConn_t& tAgent, ISphOutputBuffer& tOut ) const final { + //sphLogDebugRpl ( "%d, token(%d) %s", static_cast ( CMD ), tAgent.m_tAuthToken.m_dToken.GetLength(), BinToHex ( tAgent.m_tAuthToken.m_dToken.Begin(), tAgent.m_tAuthToken.m_dToken.GetLength() ).cstr() ); // !COMMIT + if ( CMD==E_CLUSTER::FILE_SEND ) { { - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_PERSIST ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_PERSIST, 0, ApiAuthToken_t() ); tOut.SendInt ( 1 ); // set persistent to 1 } } // API header - auto tReply = APIHeader ( tOut, SEARCHD_COMMAND_CLUSTER, VER_COMMAND_CLUSTER ); + auto tReply = APIHeader ( tOut, SEARCHD_COMMAND_CLUSTER, VER_COMMAND_CLUSTER, tAgent.m_tAuthToken ); tOut.SendWord ( static_cast ( CMD ) ); tOut << GetReq ( tAgent ); if ( CMD==E_CLUSTER::FILE_SEND ) { { - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_PERSIST ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_PERSIST, 0, ApiAuthToken_t() ); tOut.SendInt ( 0 ); // set persistent to 0 } } diff --git a/src/replication/cluster_delete.cpp b/src/replication/cluster_delete.cpp index 91c3abb105..f1758b8e3b 100644 --- a/src/replication/cluster_delete.cpp +++ b/src/replication/cluster_delete.cpp @@ -31,13 +31,13 @@ void ReceiveClusterDelete ( ISphOutputBuffer & tOut, InputBuffer_c & tBuf, CSphS ClusterDelete_c::BuildReply ( tOut ); } -void SendClusterDeleteToNodes ( const VecTraits_T& dNodes, const CSphString& sCluster ) +void SendClusterDeleteToNodes ( const VecTraits_T& dNodes, const CSphString & sCluster, const CSphString & sUser ) { if ( dNodes.IsEmpty() ) return; ClusterRequest_t tData { sCluster }; ClusterDelete_c tReq; - auto dAgents = tReq.MakeAgents ( GetDescAPINodes ( dNodes, Resolve_e::SLOW ), ReplicationTimeoutQuery(), tData ); + auto dAgents = tReq.MakeAgents ( GetDescAPINodes ( dNodes, Resolve_e::SLOW ), sUser, ReplicationTimeoutQuery(), tData ); PerformRemoteTasksWrap ( dAgents, tReq, tReq, true ); } diff --git a/src/replication/cluster_delete.h b/src/replication/cluster_delete.h index d8cbaba78c..938f27a569 100644 --- a/src/replication/cluster_delete.h +++ b/src/replication/cluster_delete.h @@ -15,6 +15,6 @@ #include "std/string.h" // ask remote nodes to delete given cluster by name -void SendClusterDeleteToNodes ( const VecTraits_T& dNodes, const CSphString& sCluster ); +void SendClusterDeleteToNodes ( const VecTraits_T& dNodes, const CSphString & sCluster, const CSphString & sUser ); diff --git a/src/replication/cluster_file_send.cpp b/src/replication/cluster_file_send.cpp index dc9a025ced..766aafbef3 100644 --- a/src/replication/cluster_file_send.cpp +++ b/src/replication/cluster_file_send.cpp @@ -406,7 +406,7 @@ void FileReader_t::RetryFile ( int iRemoteFile, bool bNetError, WriteResult_e eR } // send file to multiple nodes by chunks as API command CLUSTER_FILE_SEND -bool RemoteClusterFileSend ( const SyncSrc_t & tSigSrc, const CSphVector & dDesc, const CSphString & sCluster, const CSphString & sIndex ) +bool RemoteClusterFileSend ( const SyncSrc_t & tSigSrc, const CSphVector & dDesc, const CSphString & sCluster, const CSphString & sIndex, const CSphString & sUser ) { StringBuilder_c tErrors ( ";" ); @@ -437,7 +437,7 @@ bool RemoteClusterFileSend ( const SyncSrc_t & tSigSrc, const CSphVector dNodes; dNodes.Resize ( dReaders.GetLength() ); ARRAY_FOREACH ( i, dReaders ) - dNodes[i] = ClusterFileSend_c::CreateAgent ( *dReaders[i].m_pAgentDesc, dReaders[i].m_pSyncDst->m_tmTimeoutFile, dReaders[i].m_tFileSendRequest ); + dNodes[i] = ClusterFileSend_c::CreateAgent ( *dReaders[i].m_pAgentDesc, sUser, dReaders[i].m_pSyncDst->m_tmTimeoutFile, dReaders[i].m_tFileSendRequest ); // submit initial jobs CSphRefcountedPtr tReporter ( GetObserver() ); @@ -490,7 +490,7 @@ bool RemoteClusterFileSend ( const SyncSrc_t & tSigSrc, const CSphVectorRelease(); - AgentConn_t* pNextJob = ClusterFileSend_c::CreateAgent ( *tReader.m_pAgentDesc, tReader.m_pSyncDst->m_tmTimeoutFile, tReader.m_tFileSendRequest ); + AgentConn_t* pNextJob = ClusterFileSend_c::CreateAgent ( *tReader.m_pAgentDesc, sUser, tReader.m_pSyncDst->m_tmTimeoutFile, tReader.m_tFileSendRequest ); dNodes[iAgent] = pNextJob; VectorAgentConn_t dNewNode; diff --git a/src/replication/cluster_file_send.h b/src/replication/cluster_file_send.h index d0a84850c2..28146648c9 100644 --- a/src/replication/cluster_file_send.h +++ b/src/replication/cluster_file_send.h @@ -24,5 +24,5 @@ struct RemoteFileState_t }; // send file to multiple nodes by chunks as API command CLUSTER_FILE_SEND -bool RemoteClusterFileSend ( const SyncSrc_t & tSigSrc, const CSphVector & dDesc, const CSphString & sCluster, const CSphString & sIndex ); +bool RemoteClusterFileSend ( const SyncSrc_t & tSigSrc, const CSphVector & dDesc, const CSphString & sCluster, const CSphString & sIndex, const CSphString & sUser ); diff --git a/src/replication/cluster_get_nodes.cpp b/src/replication/cluster_get_nodes.cpp index cb55f83f44..706bf028af 100644 --- a/src/replication/cluster_get_nodes.cpp +++ b/src/replication/cluster_get_nodes.cpp @@ -58,7 +58,7 @@ StrVec_t RemoteClusterGetNodes ( VectorAgentConn_t & dAgents ) } // command to all remote nodes at cluster to get actual nodes list -StrVec_t QueryNodeListFromRemotes ( const VecTraits_T& dClusterNodes, const CSphString& sCluster ) +StrVec_t QueryNodeListFromRemotes ( const VecTraits_T& dClusterNodes, const CSphString& sCluster, const CSphString & sUser ) { StrVec_t dNodes; TlsMsg::ResetErr(); @@ -76,7 +76,7 @@ StrVec_t QueryNodeListFromRemotes ( const VecTraits_T& dClusterNodes ClusterRequest_t dRequest; dRequest.m_sCluster = sCluster; - VecRefPtrs_t dAgents = ClusterGetNodes_c::MakeAgents ( dDesc, ReplicationTimeoutAnyNode(), dRequest ); + VecRefPtrs_t dAgents = ClusterGetNodes_c::MakeAgents ( dDesc, sUser, ReplicationTimeoutAnyNode(), dRequest ); dNodes = RemoteClusterGetNodes ( dAgents ); ScopedComma_c tColon ( TlsMsg::Err(), ";" ); @@ -105,9 +105,9 @@ void ReceiveClusterGetNodes ( ISphOutputBuffer& tOut, InputBuffer_c& tBuf, CSphS ClusterGetNodes_c::BuildReply ( tOut, dNodes ); } -StrVec_t GetNodeListFromRemotes ( const ClusterDesc_t& tDesc ) +StrVec_t GetNodeListFromRemotes ( const ClusterDesc_t & tDesc ) { - auto dNodes = QueryNodeListFromRemotes ( tDesc.m_dClusterNodes, tDesc.m_sName ); + auto dNodes = QueryNodeListFromRemotes ( tDesc.m_dClusterNodes, tDesc.m_sName, tDesc.m_sUser ); if ( dNodes.IsEmpty() ) TlsMsg::Err ( "cluster '%s', no nodes available(%s), error: %s", tDesc.m_sName.cstr(), StrVec2Str( tDesc.m_dClusterNodes ).cstr(), TlsMsg::szError() ); else @@ -146,12 +146,12 @@ void operator>> ( InputBuffer_c & tIn, ClusterNodesStatesReply_t & tReq ) tState.m_sHash = tIn.GetString(); } -static bool SendClusterNodesStates ( const CSphString & sCluster, const VecTraits_T & dNodes, ClusterNodesStatesVec_t & dStates ) +static bool SendClusterNodesStates ( const CSphString & sCluster, const CSphString & sUser, const VecTraits_T & dNodes, ClusterNodesStatesVec_t & dStates ) { ClusterNodeState_c::REQUEST_T tReq; tReq.m_sCluster = sCluster; - auto dAgents = ClusterNodeState_c::MakeAgents ( GetDescAPINodes ( dNodes, Resolve_e::SLOW ), ReplicationTimeoutQuery(), tReq ); + auto dAgents = ClusterNodeState_c::MakeAgents ( GetDescAPINodes ( dNodes, Resolve_e::SLOW ), sUser, ReplicationTimeoutQuery(), tReq ); // no nodes left seems a valid case if ( dAgents.IsEmpty() ) return true; @@ -172,7 +172,7 @@ static bool SendClusterNodesStates ( const CSphString & sCluster, const VecTrait ClusterNodesStatesVec_t GetStatesFromRemotes ( const ClusterDesc_t & tDesc ) { ClusterNodesStatesVec_t dStates; - SendClusterNodesStates ( tDesc.m_sName, tDesc.m_dClusterNodes, dStates ); + SendClusterNodesStates ( tDesc.m_sName, tDesc.m_sUser, tDesc.m_dClusterNodes, dStates ); return dStates; } @@ -267,10 +267,10 @@ bool CheckRemotesVersions ( const ClusterDesc_t & tDesc, bool bWithServerId ) VecRefPtrs_t dAgents; if ( !bWithServerId ) { - dAgents = ClusterNodeVer_c::MakeAgents ( GetDescAPINodes ( tDesc.m_dClusterNodes, Resolve_e::QUICK ), ReplicationTimeoutAnyNode(), tReqVer ); + dAgents = ClusterNodeVer_c::MakeAgents ( GetDescAPINodes ( tDesc.m_dClusterNodes, Resolve_e::QUICK ), tDesc.m_sUser, ReplicationTimeoutAnyNode(), tReqVer ); } else { - dAgents = ClusterNodeVerId_c::MakeAgents ( GetDescAPINodes ( tDesc.m_dClusterNodes, Resolve_e::QUICK ), ReplicationTimeoutAnyNode(), tReqId ); + dAgents = ClusterNodeVerId_c::MakeAgents ( GetDescAPINodes ( tDesc.m_dClusterNodes, Resolve_e::QUICK ), tDesc.m_sUser, ReplicationTimeoutAnyNode(), tReqId ); } // no nodes left seems a valid case if ( dAgents.IsEmpty() ) diff --git a/src/replication/cluster_synced.cpp b/src/replication/cluster_synced.cpp index 415180ba75..ff91caec8f 100644 --- a/src/replication/cluster_synced.cpp +++ b/src/replication/cluster_synced.cpp @@ -50,10 +50,10 @@ void operator>> ( InputBuffer_c& tIn, ClusterSyncedRequest_t& tReq ) using ClusterSynced_c = ClusterCommand_T; // API command to remote node to issue cluster synced callback -bool SendClusterSynced ( const VecAgentDesc_t& dDesc, const ClusterSyncedRequest_t& tRequest ) +bool SendClusterSynced ( const VecAgentDesc_t & dDesc, const ClusterSyncedRequest_t & tRequest, const CSphString & sUser ) { ClusterSynced_c tReq; - auto dNodes = tReq.MakeAgents ( dDesc, ReplicationTimeoutQuery(), tRequest ); + auto dNodes = tReq.MakeAgents ( dDesc, sUser, ReplicationTimeoutQuery(), tRequest ); return PerformRemoteTasksWrap ( dNodes, tReq, tReq, true ); } diff --git a/src/replication/cluster_synced.h b/src/replication/cluster_synced.h index 31876cf657..9b78c2b11e 100644 --- a/src/replication/cluster_synced.h +++ b/src/replication/cluster_synced.h @@ -24,6 +24,6 @@ struct ClusterSyncedRequest_t : ClusterRequest_t CSphString m_sMsg; }; -bool SendClusterSynced ( const VecAgentDesc_t& dDesc, const ClusterSyncedRequest_t& tRequest ); +bool SendClusterSynced ( const VecAgentDesc_t & dDesc, const ClusterSyncedRequest_t & tRequest, const CSphString & sUser ); diff --git a/src/replication/cluster_update_nodes.cpp b/src/replication/cluster_update_nodes.cpp index 8b53488e74..d084e120e5 100644 --- a/src/replication/cluster_update_nodes.cpp +++ b/src/replication/cluster_update_nodes.cpp @@ -31,13 +31,13 @@ void operator>> ( InputBuffer_c& tIn, UpdateNodesRequest_t& tReq ) tReq.m_eKindNodes = (NODES_E)( tIn.GetByte() ? 1 : 0 ); } -bool SendClusterUpdateNodes ( const CSphString& sCluster, NODES_E eNodes, const VecTraits_T& dNodes ) +bool SendClusterUpdateNodes ( const CSphString & sCluster, const CSphString & sUser, NODES_E eNodes, const VecTraits_T& dNodes ) { ClusterUpdateNodes_c::REQUEST_T tRequest; tRequest.m_sCluster = sCluster; tRequest.m_eKindNodes = eNodes; - auto dAgents = ClusterUpdateNodes_c::MakeAgents ( GetDescAPINodes ( dNodes, Resolve_e::SLOW ), ReplicationTimeoutQuery(), tRequest ); + auto dAgents = ClusterUpdateNodes_c::MakeAgents ( GetDescAPINodes ( dNodes, Resolve_e::SLOW ), sUser, ReplicationTimeoutQuery(), tRequest ); // no nodes left seems a valid case if ( dAgents.IsEmpty() ) return true; diff --git a/src/replication/cluster_update_nodes.h b/src/replication/cluster_update_nodes.h index 45b24c835f..86a17d6211 100644 --- a/src/replication/cluster_update_nodes.h +++ b/src/replication/cluster_update_nodes.h @@ -21,5 +21,5 @@ struct UpdateNodesRequest_t : public ClusterRequest_t NODES_E m_eKindNodes; }; -bool SendClusterUpdateNodes ( const CSphString& sCluster, NODES_E eNodes, const VecTraits_T& dNodes ); +bool SendClusterUpdateNodes ( const CSphString & sCluster, const CSphString & sUser, NODES_E eNodes, const VecTraits_T & dNodes ); diff --git a/src/replication/replicate_index.cpp b/src/replication/replicate_index.cpp index 94f12be57a..33e55897da 100644 --- a/src/replication/replicate_index.cpp +++ b/src/replication/replicate_index.cpp @@ -49,7 +49,7 @@ class IndexSaveGuard_c: public ISphNoncopyable cServedIndexRefPtr_c m_pServedIndex; }; -static bool ActivateIndexOnRemotes ( const CSphString& sCluster, const CSphString& sIndex, IndexType_e eType, bool bSendOk, const VecTraits_T& dActivateIndexes, int64_t tmLongOpTimeout ) +static bool ActivateIndexOnRemotes ( const CSphString& sCluster, const CSphString& sIndex, const CSphString & sUser, IndexType_e eType, bool bSendOk, const VecTraits_T& dActivateIndexes, int64_t tmLongOpTimeout ) { // send a command to activate transferred index ClusterIndexAddLocalRequest_t tAddLocal; @@ -63,7 +63,7 @@ static bool ActivateIndexOnRemotes ( const CSphString& sCluster, const CSphStrin ARRAY_FOREACH ( i, dActivateIndexes ) { const AgentDesc_t& tDesc = *dActivateIndexes[i]; - dNodes[i] = ClusterIndexAddLocal_c::CreateAgent ( tDesc, ReplicationTimeoutQuery(), tAddLocal ); + dNodes[i] = ClusterIndexAddLocal_c::CreateAgent ( tDesc, sUser, ReplicationTimeoutQuery(), tAddLocal ); } sphLogDebugRpl ( "sent table '%s' %s to %d nodes with timeout %d.%03d sec", sIndex.cstr(), ( bSendOk ? "loading" : "rollback" ), dNodes.GetLength(), (int)( tmLongOpTimeout / 1000 ), (int)( tmLongOpTimeout % 1000 ) ); @@ -137,7 +137,7 @@ bool SyncSrc_t::CalculateFilesSignatures() } // send local index to remote nodes via API -bool ReplicateIndexToNodes ( const CSphString& sCluster, const CSphString& sIndex, const VecTraits_T& dDesc, const cServedIndexRefPtr_c& pServedIndex ) +bool ReplicateIndexToNodes ( const CSphString & sCluster, const CSphString & sIndex, const CSphString & sUser, const VecTraits_T & dDesc, const cServedIndexRefPtr_c & pServedIndex ) { assert ( !dDesc.IsEmpty ()); @@ -167,7 +167,7 @@ bool ReplicateIndexToNodes ( const CSphString& sCluster, const CSphString& sInde tRequest.m_sIndex = sIndex; tRequest.m_pChunks = &tSigSrc; tRequest.m_sIndexFileName = GetBaseName ( sIndexPath ); - auto dNodes = ClusterFileReserve_c::MakeAgents ( dDesc, tmLongOpTimeout, tRequest ); + auto dNodes = ClusterFileReserve_c::MakeAgents ( dDesc, sUser, tmLongOpTimeout, tRequest ); assert ( dDesc.GetLength() == dNodes.GetLength() ); auto bOk = SendClusterFileReserve ( dNodes ); @@ -228,12 +228,12 @@ bool ReplicateIndexToNodes ( const CSphString& sCluster, const CSphString& sInde bool bSendOk = true; if ( !dSendStates.IsEmpty() ) - bSendOk = RemoteClusterFileSend ( tSigSrc, dSendStates, sCluster, sIndex ); + bSendOk = RemoteClusterFileSend ( tSigSrc, dSendStates, sCluster, sIndex, sUser ); // allow index local write operations passed without replicator tIndexSaveGuard.EnableSave (); - return ActivateIndexOnRemotes ( sCluster, sIndex, eType, bSendOk, dActivateIndexes, tmLongOpTimeout ) && bSendOk; + return ActivateIndexOnRemotes ( sCluster, sIndex, sUser, eType, bSendOk, dActivateIndexes, tmLongOpTimeout ) && bSendOk; } struct DistIndexSendRequest_t : public ClusterRequest_t @@ -272,7 +272,7 @@ void operator>> ( InputBuffer_c & tIn, DistIndexSendRequest_t & tReq ) using ClusterSendDistIndex_c = ClusterCommand_T; // send distributed index to remote nodes via API -bool ReplicateDistIndexToNodes ( const CSphString & sCluster, const CSphString & sIndex, const VecTraits_T & dDesc ) +bool ReplicateDistIndexToNodes ( const CSphString & sCluster, const CSphString & sIndex, const CSphString & sUser, const VecTraits_T & dDesc ) { cDistributedIndexRefPtr_t pDist ( GetDistr ( sIndex ) ); if ( !pDist ) @@ -285,7 +285,7 @@ bool ReplicateDistIndexToNodes ( const CSphString & sCluster, const CSphString & DistIndexSendRequest_t tSend ( *pDist, sCluster, sIndex ); int64_t tmTimeout = ReplicationTimeoutQuery(); - auto dNodes = ClusterSendDistIndex_c::MakeAgents ( dDesc, tmTimeout, tSend ); + auto dNodes = ClusterSendDistIndex_c::MakeAgents ( dDesc, sUser, tmTimeout, tSend ); sphLogDebugRpl ( "sending table '%s' to %d nodes with timeout %d.%03d sec", sIndex.cstr(), dNodes.GetLength(), (int)( tmTimeout / 1000 ), (int)( tmTimeout % 1000 ) ); diff --git a/src/replication/replicate_index.h b/src/replication/replicate_index.h index 20dd5e30a1..ac4d5bb9f2 100644 --- a/src/replication/replicate_index.h +++ b/src/replication/replicate_index.h @@ -14,7 +14,7 @@ #include "searchdha.h" // send local index to remote nodes via API -bool ReplicateIndexToNodes ( const CSphString& sCluster, const CSphString& sIndex, const VecTraits_T& dDesc, const cServedIndexRefPtr_c& pServedIndex ); +bool ReplicateIndexToNodes ( const CSphString & sCluster, const CSphString & sIndex, const CSphString & sUser, const VecTraits_T & dDesc, const cServedIndexRefPtr_c & pServedIndex ); // send distributed index to remote nodes via API -bool ReplicateDistIndexToNodes ( const CSphString & sCluster, const CSphString & sIndex, const VecTraits_T & dDesc ); +bool ReplicateDistIndexToNodes ( const CSphString & sCluster, const CSphString & sIndex, const CSphString & sUser, const VecTraits_T & dDesc ); diff --git a/src/schema/schema.cpp b/src/schema/schema.cpp index 1e60c418b5..6cb5267003 100644 --- a/src/schema/schema.cpp +++ b/src/schema/schema.cpp @@ -356,7 +356,9 @@ bool CSphSchema::IsReserved ( const char* szToken ) "FACET", "FALSE", "FORCE", "FROM", "IGNORE", "IN", "INDEXES", "INNER", "IS", "JOIN", "KNN", "LEFT", "LIMIT", "MOD", "NOT", "NO_COLUMNARSCAN", "NO_DOCIDINDEX", "NO_SECONDARYINDEX", "NULL", "OFFSET", "ON", "OR", "ORDER", "RELOAD", "SECONDARYINDEX", "SELECT", "SYSFILTERS", - "TRUE", NULL + "TRUE" + , "TOKEN" + , NULL }; const char** p = dReserved; diff --git a/src/searchd.cpp b/src/searchd.cpp index 90eaffb98a..ad17cf5ec0 100644 --- a/src/searchd.cpp +++ b/src/searchd.cpp @@ -81,6 +81,10 @@ #include "detail/indexlink.h" #include "detail/expmeter.h" +#include "auth/auth.h" +#include "auth/auth_common.h" +#include "auth/auth_proto_mysql.h" + extern "C" { #include "sphinxudf.h" @@ -1597,7 +1601,7 @@ void SendErrorReply ( ISphOutputBuffer & tOut, const char * sTemplate, ... ) sError.SetSprintfVa ( sTemplate, ap ); va_end ( ap ); - auto tHdr = APIHeader ( tOut, SEARCHD_ERROR ); + auto tHdr = APIAnswer ( tOut, 0, SEARCHD_ERROR ); tOut.SendString ( sError.cstr() ); // --console logging @@ -1983,7 +1987,7 @@ void SearchRequestBuilder_c::SendQuery ( const char * sIndexes, ISphOutputBuffer void SearchRequestBuilder_c::BuildRequest ( const AgentConn_t & tAgent, ISphOutputBuffer & tOut ) const { - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH ); // API header + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, tAgent.m_tAuthToken ); // API header tOut.SendInt ( VER_COMMAND_SEARCH_MASTER ); tOut.SendInt ( m_dQueries.GetLength() ); @@ -5340,7 +5344,7 @@ class SearchHandler_c int m_iDupeId = -1; }; - bool ParseSysVar(); + bool ParseSystem(); bool ParseIdxSubkeys(); bool CheckMultiQuery() const; bool AcquireInvokedIndexes(); @@ -6778,12 +6782,14 @@ void HandleMysqlDescribe ( RowBuffer_i & tOut, SqlStmt_t * pStmt ); void HandleSelectIndexStatus ( RowBuffer_i & tOut, const SqlStmt_t * pStmt ); void HandleSelectFiles ( RowBuffer_i & tOut, const SqlStmt_t * pStmt ); -bool SearchHandler_c::ParseSysVar () +bool SearchHandler_c::ParseSystem() { - const auto& sVar = m_dLocal.First().m_sName; + const auto & sName = m_dLocal.First().m_sName; const auto & dSubkeys = m_dNQueries.First ().m_dStringSubkeys; + bool bSysVar = ( sName=="@@system" ); + bool bAuthTbl = ( sName.Begins ( GetPrefixAuth().cstr() ) ); - if ( sVar=="@@system" ) + if ( bSysVar ) { if ( !dSubkeys.IsEmpty () ) { @@ -6831,9 +6837,17 @@ bool SearchHandler_c::ParseSysVar () return true; } } + } else if ( bAuthTbl ) + { + cServedIndexRefPtr_c pIndex { MakeDynamicAuthIndex ( sName, m_sError ) }; + if ( !pIndex ) + return false; + + m_dAcquired.AddIndex ( sName, std::move ( pIndex ) ); + return true; } - m_sError << "no such variable " << sVar; + m_sError << "no such " << ( bAuthTbl ? "table " : "variable " ) << sName; dSubkeys.for_each ( [this] ( const auto& s ) { m_sError << s; } ); return false; } @@ -7183,10 +7197,11 @@ bool SearchHandler_c::BuildIndexList ( int & iDivideLimits, VecRefPtrsAgentConn_ m_dLocal.Reset (); int iOrderTag = 0; bool bSysVar = tQuery.m_sIndexes.Begins ( "@@" ); + bool bAuthTbl = tQuery.m_sIndexes.Begins ( GetPrefixAuth().cstr() ); // search through specified local indexes StrVec_t dIdxNames; - if ( bSysVar ) + if ( bSysVar || bAuthTbl ) dIdxNames.Add ( tQuery.m_sIndexes ); else { @@ -7268,7 +7283,7 @@ bool SearchHandler_c::BuildIndexList ( int & iDivideLimits, VecRefPtrsAgentConn_ else m_dLocal.SwapData ( dLocals ); - return !bSysVar; + return !( bSysVar || bAuthTbl ); } // generate warning about slow full text expansion for queries there @@ -7412,7 +7427,7 @@ void SearchHandler_c::RunSubset ( int iStart, int iEnd ) } else { // process query to @@*, as @@system.threads, etc. - if ( !ParseSysVar () ) + if ( !ParseSystem () ) return; // here we deal } @@ -7455,6 +7470,7 @@ void SearchHandler_c::RunSubset ( int iStart, int iEnd ) tReqBuilder = std::make_unique ( m_dNQueries, iDivideLimits ); tParser = std::make_unique ( iQueries ); tReporter = GetObserver(); + SetSessionAuth ( dRemotes ); // run remote queries. tReporter will tell us when they're finished. // also blackholes will be removed from this flow of remotes. @@ -7827,6 +7843,12 @@ void HandleCommandSearch ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & t if ( !ParseSearchQuery ( tReq, tOut, dQuery, uVer, uMasterVer ) ) return; + CSphString sTmpIndex; + const CSphString & sIndex = ( tHandler.m_dQueries.IsEmpty() ? sTmpIndex.scstr() : tHandler.m_dQueries[0].m_sIndexes ); + // assumes the multiple queries are to the same index + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::READ, sIndex, tOut ) ) + return; + if ( !tHandler.m_dQueries.IsEmpty() ) { QueryType_e eQueryType = tHandler.m_dQueries[0].m_eQueryType; @@ -8022,7 +8044,8 @@ static const char * g_dSqlStmts[] = "flush_hostnames", "flush_logs", "reload_indexes", "sysfilters", "debug", "alter_killlist_target", "alter_index_settings", "join_cluster", "cluster_create", "cluster_delete", "cluster_index_add", "cluster_index_delete", "cluster_update", "explain", "import_table", "freeze_indexes", "unfreeze_indexes", - "show_settings", "alter_rebuild_si", "kill", "show_locks", "show_scroll", "show_table_indexes" + "show_settings", "alter_rebuild_si", "kill", "show_locks", "show_scroll", "show_table_indexes", "reload_auth", + "show_permissions", "show_users", "show_token" }; @@ -8244,7 +8267,7 @@ void SnippetRemote_c::BuildRequest ( const AgentConn_t & tAgent, ISphOutputBuffe tAgent.m_iStoreTag = iWorker; } - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, tAgent.m_tAuthToken ); tOut.SendInt ( 0 ); tOut.SendInt ( PackAPISnippetFlags ( m_tSettings, true ) ); @@ -8531,6 +8554,7 @@ static void MakeRemoteScatteredSnippets ( CSphVector & dQueries, // and finally most interesting remote case with possibly scattered. auto dAgents = GetDistrAgents ( pDist ); int iRemoteAgents = dAgents.GetLength(); + SetSessionAuth ( dAgents ); SnippetRemote_c tRemotes ( dQueries, q ); tRemotes.m_dTasks.Resize ( iRemoteAgents ); @@ -8572,6 +8596,7 @@ static void MakeRemoteNonScatteredSnippets ( CSphVector & dQueri auto dAgents = GetDistrAgents ( pDist ); int iRemoteAgents = dAgents.GetLength(); + SetSessionAuth ( dAgents ); SnippetRemote_c tRemotes ( dQueries, q ); tRemotes.m_dTasks.Resize ( iRemoteAgents ); @@ -8821,6 +8846,10 @@ void HandleCommandExcerpt ( ISphOutputBuffer & tOut, int iVer, InputBuffer_c & t return; } } + + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::READ, sIndex, tOut ) ) + return; + myinfo::SetTaskInfo ( R"(api-snippet datasize=%.1Dk query="%s")", GetSnippetDataSize ( dQueries ), q.m_sQuery.scstr ()); if ( !MakeSnippets ( sIndex, dQueries, q, sError ) ) @@ -8874,6 +8903,9 @@ static void HandleCommandKeywords ( ISphOutputBuffer & tOut, WORD uVer, InputBuf if ( uVer>=0x102 ) tSettings.m_eJiebaMode = (JiebaMode_e)tReq.GetInt(); + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::READ, sIndex, tOut ) ) + return; + CSphString sError; SearchFailuresLog_c tFailureLog; CSphVector < CSphKeywordInfo > dKeywords; @@ -8947,7 +8979,7 @@ void UpdateRequestBuilder_c::BuildRequest ( const AgentConn_t & tAgent, ISphOutp auto& tUpd = *m_pUpd; // API header - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, tAgent.m_tAuthToken ); tOut.SendString ( sIndexes ); tOut.SendInt ( tUpd.m_dAttributes.GetLength() ); @@ -9181,6 +9213,9 @@ void HandleCommandUpdate ( ISphOutputBuffer & tOut, int iVer, InputBuffer_c & tR if ( tReq.GetError() ) return SendErrorReply ( tOut, "invalid or truncated request" ); + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::WRITE, sIndexes, tOut ) ) + return; + // check index names StrVec_t dIndexNames; ParseIndexList ( sIndexes, dIndexNames ); @@ -9227,6 +9262,7 @@ void HandleCommandUpdate ( ISphOutputBuffer & tOut, int iVer, InputBuffer_c & tR { VecRefPtrsAgentConn_t dAgents; pDist->GetAllHosts ( dAgents ); + SetSessionAuth ( dAgents ); // connect to remote agents and query them UpdateRequestBuilder_c tReqBuilder ( pUpd ); @@ -9685,6 +9721,9 @@ void HandleCommandStatus ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & t bool bGlobalStat = tReq.GetDword ()!=0; + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::SCHEMA, "", tOut ) ) + return; + VectorLike dStatus; if ( bGlobalStat ) @@ -9717,6 +9756,9 @@ void HandleCommandFlush ( ISphOutputBuffer & tOut, WORD uVer ) if ( !CheckCommandVersion ( uVer, VER_COMMAND_FLUSHATTRS, tOut ) ) return; + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::WRITE, "", tOut ) ) + return; + int iTag = CommandFlush (); // return last flush tag, just for the fun of it auto tReply = APIAnswer ( tOut, VER_COMMAND_FLUSHATTRS ); @@ -10088,7 +10130,7 @@ class PqRequestBuilder_c : public RequestBuilder_i } const char * sIndex = tAgent.m_tDesc.m_sIndexes.cstr (); - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_CALLPQ, VER_COMMAND_CALLPQ ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_CALLPQ, VER_COMMAND_CALLPQ, tAgent.m_tAuthToken ); DWORD uFlags = 0; if ( m_tOpts.m_bGetDocs ) @@ -10616,6 +10658,8 @@ void PercolateMatchDocuments ( const BlobVec_t & dDocs, const PercolateOptions_t CSphRefcountedPtr pReporter { nullptr }; if ( bHaveRemotes ) { + SetSessionAuth ( dAgents ); + pReqBuilder = std::make_unique ( dDocs, tOpts, iStart, iStep ); iStart += iStep * dAgents.GetLength (); pParser = std::make_unique(); @@ -10706,6 +10750,9 @@ void HandleCommandCallPq ( ISphOutputBuffer &tOut, WORD uVer, InputBuffer_c &tRe return; } + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::READ, tOpts.m_sIndex, tOut ) ) + return; + // working CSphSessionAccum tAcc; CPqResult tResult; @@ -11478,6 +11525,23 @@ void sphHandleMysqlInsert ( StmtErrorReporter_i & tOut, const SqlStmt_t & tStmt auto pServed = GetServed ( tStmt.m_sIndex ); if ( !ServedDesc_t::IsMutable ( pServed ) ) { + if ( tStmt.m_sIndex.Begins ( GetPrefixAuth().cstr() ) ) + { + bool bCommit = ( pSession->m_bAutoCommit && !pSession->m_bInTransaction ); + if ( !bCommit ) + { + tOut.Error ( "table '%s': %s is not supported on system auth tables when autocommit=0", ( bReplace ? "REPLACE" : "INSERT" ), tStmt.m_sIndex.cstr() ); + return; + } + + CSphString sError; + if ( !InsertAuthDocuments ( tStmt, sError ) ) + tOut.Error ( "%s", sError.cstr () ); + else + tOut.Ok ( tStmt.m_iRowsAffected, CSphString(), 0 ); // FIXME!!! + return; + } + bool bDistTable = false; if ( !pServed ) { @@ -11926,6 +11990,8 @@ bool DoGetKeywords ( const CSphString & sIndex, const CSphString & sQuery, const int iAgentsReply = 0; if ( !dAgents.IsEmpty() ) { + SetSessionAuth ( dAgents ); + // connect to remote agents and query them KeywordsRequestBuilder_c tReqBuilder ( tSettings, sQuery ); KeywordsReplyParser_c tParser ( tSettings.m_bStats, dKeywords ); @@ -12095,7 +12161,7 @@ void KeywordsRequestBuilder_c::BuildRequest ( const AgentConn_t & tAgent, ISphOu { const CSphString & sIndexes = tAgent.m_tDesc.m_sIndexes; - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, tAgent.m_tAuthToken ); tOut.SendString ( m_sTerm.cstr() ); tOut.SendString ( sIndexes.cstr() ); @@ -12380,6 +12446,9 @@ void HandleCommandSuggest ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & return; } + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::READ, sIndex, tOut ) ) + return; + auto pServed = GetServed ( sIndex ); if ( !pServed ) { @@ -12408,7 +12477,7 @@ class SuggestRequestBuilder_c : public RequestBuilder_i void BuildRequest ( const AgentConn_t & tAgent, ISphOutputBuffer & tOut ) const final { - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_SUGGEST, VER_COMMAND_SUGGEST ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_SUGGEST, VER_COMMAND_SUGGEST, tAgent.m_tAuthToken ); tOut.SendString ( tAgent.m_tDesc.m_sIndexes.cstr() ); tOut.SendString ( m_sWord ); @@ -12496,6 +12565,8 @@ static bool SuggestDistIndexGet ( const cDistributedIndexRefPtr_t & pDistributed int iAgentsReply = 0; if ( !dAgents.IsEmpty() ) { + SetSessionAuth ( dAgents ); + SuggestResult_t tCur; // connect to remote agents and query them SuggestRequestBuilder_c tReqBuilder ( tArgs, sWord ); @@ -12949,6 +13020,11 @@ void HandleMysqlShowTables ( RowBuffer_i & tOut, const SqlStmt_t * pStmt ) bool bIsSystem = tIdx.m_sName.SubString ( 0, 7 ).EqN ("system."); return bSystem == bIsSystem; }; + CSphString sUser; + CSphString sPermsError; + bool bAuthCheck = IsAuthEnabled(); + if ( bAuthCheck ) + sUser = session::GetUser(); // output the results VectorLike dTable ( pStmt->m_sStringParam, { "Table", "Type" } ); @@ -12956,6 +13032,9 @@ void HandleMysqlShowTables ( RowBuffer_i & tOut, const SqlStmt_t * pStmt ) { if ( !fnFilter ( dPair ) ) continue; + if ( bAuthCheck && !CheckPerms ( sUser, AuthAction_e::READ, dPair.m_sName, false, sPermsError ) ) + continue; + if ( bWithClusters && !dPair.m_sCluster.IsEmpty ()) dTable.MatchTuplet ( SphSprintf ("%s:%s", dPair.m_sCluster.cstr(), dPair.m_sName.cstr()).cstr(), szIndexType ( dPair.m_eType ) ); else @@ -13567,10 +13646,10 @@ class UVarRequestBuilder_c : public RequestBuilder_i m_iLength = pCur-m_dBuf.Begin(); } - void BuildRequest ( const AgentConn_t &, ISphOutputBuffer & tOut ) const final + void BuildRequest ( const AgentConn_t & tAgent, ISphOutputBuffer & tOut ) const final { // API header - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_UVAR, VER_COMMAND_UVAR ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_UVAR, VER_COMMAND_UVAR, tAgent.m_tAuthToken ); tOut.SendString ( m_sName.cstr() ); tOut.SendInt ( m_iUserVars ); @@ -13619,6 +13698,8 @@ static bool SendUserVar ( const char * sIndex, const char * sUserVarName, CSphVe // connect to remote agents and query them if ( !dAgents.IsEmpty() ) { + SetSessionAuth ( dAgents ); + UVarRequestBuilder_c tReqBuilder ( sUserVarName, dSetValues ); UVarReplyParser_c tParser; PerformRemoteTasks ( dAgents, &tReqBuilder, &tParser ); @@ -13650,6 +13731,9 @@ void HandleCommandUserVar ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & return; } + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::WRITE, sUserVar, tOut ) ) + return; + SphAttr_t iLast = 0; const BYTE * pCur = dBuf.Begin(); ARRAY_FOREACH ( i, dUserVar ) @@ -13719,7 +13803,7 @@ void SphinxqlRequestBuilder_c::BuildRequest ( const AgentConn_t & tAgent, ISphOu const char* sIndexes = tAgent.m_tDesc.m_sIndexes.cstr(); // API header - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_SPHINXQL, VER_COMMAND_SPHINXQL ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_SPHINXQL, VER_COMMAND_SPHINXQL, tAgent.m_tAuthToken ); APIBlob_c dWrapper ( tOut ); // sphinxql wrapped twice, so one more length need to be written. tOut.SendBytes ( m_sBegin ); tOut.SendBytes ( sIndexes ); @@ -13901,6 +13985,7 @@ void sphHandleMysqlUpdate ( StmtErrorReporter_i & tOut, const SqlStmt_t & tStmt, VecRefPtrs_t dAgents; pDist->GetAllHosts ( dAgents ); + SetSessionAuth ( dAgents ); // connect to remote agents and query them std::unique_ptr pRequestBuilder = CreateRequestBuilder ( sQuery, tStmt ); @@ -14589,6 +14674,23 @@ void sphHandleMysqlDelete ( StmtErrorReporter_i & tOut, const SqlStmt_t & tStmt, return; } + if ( dNames[0].Begins ( GetPrefixAuth().cstr() ) ) + { + if ( !bCommit ) + { + tOut.Error ( "table '%s': DELETE is not supported on system auth tables when autocommit=0", tStmt.m_sIndex.cstr() ); + return; + } + + CSphString sError; + int iAffected = DeleteAuthDocuments ( dNames[0], tStmt, sError ); + if ( !sError.IsEmpty() ) + tOut.Error ( "%s", sError.cstr () ); + else + tOut.Ok ( iAffected ); + return; + } + DistrPtrs_t dDistributed; CSphString sMissed; if ( !ExtractDistributedIndexes ( dNames, dDistributed, sMissed ) ) @@ -14598,7 +14700,7 @@ void sphHandleMysqlDelete ( StmtErrorReporter_i & tOut, const SqlStmt_t & tStmt, } // delete to agents works only with commit=1 - if ( !bCommit ) + if ( !bCommit ) { for ( auto &pDist : dDistributed ) { @@ -14642,6 +14744,7 @@ void sphHandleMysqlDelete ( StmtErrorReporter_i & tOut, const SqlStmt_t & tStmt, const DistributedIndex_t * pDist = dDistributed[iIdx]; VecRefPtrsAgentConn_t dAgents; pDist->GetAllHosts ( dAgents ); + SetSessionAuth ( dAgents ); int iGot = 0; int iWarns = 0; @@ -17561,6 +17664,13 @@ bool ClientSession_c::Execute ( Str_t sQuery, RowBuffer_i & tOut ) } } + if ( bParsedOK && !SqlCheckPerms ( session::GetUser(), dStmt, m_sError ) ) + { + FreezeLastMeta(); + tOut.Error ( m_sError.cstr(), EMYSQL_ERR::ACCESS_DENIED_ERROR ); + return true; + } + // handle multi SQL query if ( bParsedOK && dStmt.GetLength()>1 ) { @@ -18014,6 +18124,27 @@ bool ClientSession_c::Execute ( Str_t sQuery, RowBuffer_i & tOut ) HandleMysqlShowTableIndexes ( tOut, *pStmt ); return true; + case STMT_RELOAD_AUTH: + if ( AuthReload ( m_sError ) ) + tOut.Ok(); + else + tOut.Error ( m_sError.cstr() ); + return true; + + case STMT_SHOW_PERMISSIONS: + HandleMysqlShowPerms ( tOut ); + return true; + + case STMT_SHOW_USERS: + HandleMysqlShowUsers ( tOut ); + return true; + + case STMT_SHOW_TOKEN: + { + const CSphString & sUser = ( pStmt->m_dCallStrings.GetLength() ? pStmt->m_dCallStrings[0] : session::GetUser() ); + HandleMysqlShowToken ( sUser, tOut ); + } + return true; default: m_sError.SetSprintf ( "internal error: unhandled statement type (value=%d)", eStmt ); @@ -18081,6 +18212,11 @@ void session::SetUser ( const CSphString & sUser ) GetClientSession()->m_sUser = sUser; } +const CSphString & session::GetUser() +{ + return GetClientSession()->m_sUser; +} + void session::SetAutoCommit ( bool bAutoCommit ) { GetClientSession()->m_bAutoCommit = bAutoCommit; @@ -20726,6 +20862,7 @@ void ConfigureSearchd ( const CSphConfig & hConf, bool bOptPIDFile, bool bTestMo ConfigureMerge(hSearchd); SetJoinBatchSize ( hSearchd.GetInt ( "join_batch_size", GetJoinBatchSize() ) ); SetRtFlushDiskPeriod ( hSearchd.GetSTimeS ( "diskchunk_flush_write_timeout", bTestMode ? -1 : 1 ), hSearchd.GetSTimeS ( "diskchunk_flush_search_timeout", 30 ) ); + AuthConfigure ( hSearchd ); } static void DirMustWritable ( const CSphString & sDataDir ) diff --git a/src/searchdaemon.h b/src/searchdaemon.h index 44981a01e4..fb83677607 100644 --- a/src/searchdaemon.h +++ b/src/searchdaemon.h @@ -454,8 +454,10 @@ class APIBlob_c } }; +struct ApiAuthToken_t; + // RAII Start Sphinx API command/request header -APIBlob_c APIHeader ( ISphOutputBuffer & dBuff, WORD uCommand, WORD uVer = 0 /* SEARCHD_OK */ ); +APIBlob_c APIHeader ( ISphOutputBuffer & dBuff, WORD uCommand, WORD uVer, const ApiAuthToken_t & tToken ); // RAII Sphinx API answer APIBlob_c APIAnswer ( ISphOutputBuffer & dBuff, WORD uVer = 0, WORD uStatus = 0 /* SEARCHD_OK */ ); @@ -1326,6 +1328,7 @@ class CSphSessionAccum // from mysqld_error.h enum class EMYSQL_ERR : WORD { + ACCESS_DENIED_ERROR = 1045, UNKNOWN_COM_ERROR = 1047, SERVER_SHUTDOWN = 1053, PARSE_ERROR = 1064, @@ -1370,6 +1373,7 @@ enum class EHTTP_STATUS : BYTE _200, _206, _400, + _401, _403, _404, _405, @@ -1398,6 +1402,7 @@ enum class EHTTP_ENDPOINT : BYTE CLI, CLI_JSON, ES_BULK, + TOKEN, TOTAL }; @@ -1420,6 +1425,7 @@ const CSphString& sphGetLogFile() noexcept; void sphProcessHttpQueryNoResponce ( const CSphString& sEndpoint, const CSphString& sQuery, CSphVector & dResult ); void sphHttpErrorReply ( CSphVector & dData, EHTTP_STATUS eCode, const char * szError ); +void sphHttpErrorReply ( CSphVector & dData, EHTTP_STATUS eCode, const char * sError, const char * sHeaderField ); void LoadCompatHttp ( const char * sData ); void SaveCompatHttp ( JsonEscapedBuilder & tOut ); void SetupCompatHttp(); @@ -1441,6 +1447,7 @@ namespace session bool Execute ( Str_t sQuery, RowBuffer_i& tOut ); void SetFederatedUser(); void SetUser ( const CSphString & sUser ); + const CSphString & GetUser(); void SetAutoCommit ( bool bAutoCommit ); void SetInTrans ( bool bInTrans ); bool IsAutoCommit(); diff --git a/src/searchdbuddy.cpp b/src/searchdbuddy.cpp index 321c6b16af..49cab7959a 100644 --- a/src/searchdbuddy.cpp +++ b/src/searchdbuddy.cpp @@ -34,6 +34,8 @@ #include "netfetch.h" #include "searchdbuddy.h" +#include "auth/auth.h" +#include "auth/auth_proto_http.h" static std::unique_ptr g_pBuddy; static CSphString g_sPath; @@ -67,6 +69,7 @@ static int g_iBuddyVersion = 3; static bool g_bBuddyVersion = false; extern CSphString g_sStatusVersion; static CSphString g_sContainerName; +static const CSphString g_sBuddyTokenName ( "BUDDY_TOKEN" ); // windows docker needs port XXX:9999 port mapping static std::unique_ptr g_pBuddyPortList { nullptr }; @@ -373,12 +376,16 @@ BuddyState_e TryToStart ( const char * sArgs, CSphString & sError ) std::unique_ptr pBuddy; std::error_code tErrorCode; + boost::process::environment tEnv = boost::this_process::environment(); + if ( IsAuthEnabled() ) + tEnv[g_sBuddyTokenName.cstr()] = GetBuddyToken().scstr(); + #if _WIN32 BuddyWindow_t tWnd; - pBuddy.reset ( new boost::process::child ( sCmd, ( boost::process::std_out & boost::process::std_err ) > *g_pPipe, tWnd, boost::process::limit_handles, boost::process::error ( tErrorCode ) ) ); + pBuddy.reset ( new boost::process::child ( sCmd, ( boost::process::std_out & boost::process::std_err ) > *g_pPipe, tWnd, boost::process::limit_handles, boost::process::error ( tErrorCode ), tEnv ) ); #else PreservedStd_t tPreserveStd; - pBuddy.reset ( new boost::process::child ( sCmd, ( boost::process::std_out & boost::process::std_err ) > *g_pPipe, boost::process::limit_handles, boost::process::error ( tErrorCode ) , tPreserveStd ) ); + pBuddy.reset ( new boost::process::child ( sCmd, ( boost::process::std_out & boost::process::std_err ) > *g_pPipe, boost::process::limit_handles, boost::process::error ( tErrorCode ) , tPreserveStd, tEnv ) ); #endif if ( !pBuddy->running ( tErrorCode ) ) @@ -733,9 +740,9 @@ static bool SetSessionMeta ( const JsonObj_c & tBudyyReply ) // we call it ALWAYS, because even with absolutely correct result, we still might reject it for '/cli' endpoint if buddy is not available or prohibited bool ProcessHttpQueryBuddy ( HttpProcessResult_t & tRes, Str_t sSrcQuery, OptionsHash_t & hOptions, CSphVector & dResult, bool bNeedHttpResponse, http_method eRequestType ) { - if ( tRes.m_bOk || !HasBuddy() || tRes.m_eEndpoint==EHTTP_ENDPOINT::INDEX || IsBuddyQuery ( hOptions ) ) + if ( tRes.m_bOk || !HasBuddy() || tRes.m_eEndpoint==EHTTP_ENDPOINT::INDEX || tRes.m_eEndpoint==EHTTP_ENDPOINT::TOKEN || IsBuddyQuery ( hOptions ) || tRes.m_bSkipBuddy ) { - if ( tRes.m_eEndpoint==EHTTP_ENDPOINT::CLI ) + if ( tRes.m_eEndpoint==EHTTP_ENDPOINT::CLI && !tRes.m_bSkipBuddy ) { if ( !HasBuddy() ) tRes.m_sError.SetSprintf ( "can not process /cli endpoint without buddy" ); @@ -906,6 +913,8 @@ CSphString BuddyGetPath ( const CSphString & sConfigPath, const CSphString & , b sCmd.Appendf ( "-v \"%s\":/var/lib/manticore -e DATA_DIR=/var/lib/manticore", sDataDir.cstr() ); sCmd.Appendf ( "-w /buddy" ); // workdir is buddy root dir sCmd.Appendf ( "--name %s", g_sContainerName.cstr() ); // the name of the buddy container is the hash of the config + if ( IsAuthEnabled() ) + sCmd.Appendf ( "-e %s", g_sBuddyTokenName.cstr() ); // BUDDY_TOKEN for all request into daemon from the env variable sCmd.Appendf ( "%s /buddy/src/main.php", sDefaultBuddyDockerImage ); // docker image and the buddy start command return CSphString ( sCmd ); diff --git a/src/searchdconfig.cpp b/src/searchdconfig.cpp index b4d36cdd7b..a5a74ca561 100644 --- a/src/searchdconfig.cpp +++ b/src/searchdconfig.cpp @@ -214,6 +214,8 @@ bool ClusterDesc_t::Parse ( const bson::Bson_c& tBson, const CSphString& sName, } ); m_sPath = String ( tBson.ChildByName ( "path" ) ); + m_sUser = String ( tBson.ChildByName ( "user" ) ); + return true; } @@ -248,6 +250,7 @@ void ClusterDesc_t::Save ( JsonEscapedBuilder& tOut ) const for_each ( m_hIndexes, [&tOut] ( const auto& tIndex ) { tOut.String ( tIndex.first ); } ); } tOut.NamedStringNonEmpty ( "path", m_sPath ); + tOut.NamedStringNonEmpty ( "user", m_sUser ); } ////////////////////////////////////////////////////////////////////////// diff --git a/src/searchdconfig.h b/src/searchdconfig.h index 1f8a502303..575432dcb7 100644 --- a/src/searchdconfig.h +++ b/src/searchdconfig.h @@ -43,6 +43,7 @@ struct ClusterDesc_t sph::StringSet m_hIndexes; // list of index name in cluster StrVec_t m_dClusterNodes; // string list of nodes (node - address:API_port) ClusterOptions_t m_tOptions; // options for Galera + CSphString m_sUser; // user that owns that cluster bool Parse ( const bson::Bson_c & tBson, const CSphString& sName, CSphString & sWarning ); void Save ( JsonEscapedBuilder& tOut ) const; diff --git a/src/searchdfields.cpp b/src/searchdfields.cpp index 14896d1c0b..83a8b64d2f 100644 --- a/src/searchdfields.cpp +++ b/src/searchdfields.cpp @@ -10,6 +10,7 @@ #include "sphinxstd.h" #include "searchdha.h" +#include "auth/auth.h" struct FieldRequest_t { @@ -74,7 +75,7 @@ struct GetFieldRequestBuilder_t : public RequestBuilder_i auto * pRes = (RemoteFieldsAnswer_t *)tAgent.m_pResult.get(); assert ( pRes ); - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_GETFIELD, VER_COMMAND_GETFIELD ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_GETFIELD, VER_COMMAND_GETFIELD, tAgent.m_tAuthToken ); tOut.SendString ( tAgent.m_tDesc.m_sIndexes.cstr() ); tOut.SendDword ( m_dFieldCols.GetLength() ); @@ -119,7 +120,7 @@ struct ProxyFieldRequestBuilder_t : public RequestBuilder_i void BuildRequest ( const AgentConn_t & tAgent, ISphOutputBuffer & tOut ) const final { - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_GETFIELD, VER_COMMAND_GETFIELD ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_GETFIELD, VER_COMMAND_GETFIELD, tAgent.m_tAuthToken ); tOut.SendString ( tAgent.m_tDesc.m_sIndexes.cstr() ); tOut.SendDword ( m_tArgs.m_dFieldNames.GetLength() ); ARRAY_FOREACH ( i, m_tArgs.m_dFieldNames ) @@ -275,7 +276,7 @@ bool GetFieldFromDist ( VecRefPtrsAgentConn_t & dRemotes, const FieldRequest_t & return true; } -bool GetFields ( const FieldRequest_t & tReq, FieldBlob_t & tRes, DocHash_t & hFetchedDocs ) +static bool GetFields ( const FieldRequest_t & tReq, FieldBlob_t & tRes, DocHash_t & hFetchedDocs ) { if ( tReq.m_dDocs.IsEmpty() ) return true; @@ -300,6 +301,7 @@ bool GetFields ( const FieldRequest_t & tReq, FieldBlob_t & tRes, DocHash_t & hF pDistReply = std::make_unique(); pDistReporter = GetObserver(); + SetSessionAuth ( dRemotes ); ScheduleDistrJobs ( dRemotes, pDistReq.get(), pDistReply.get(), pDistReporter.Ptr() ); } @@ -600,6 +602,9 @@ void HandleCommandGetField ( ISphOutputBuffer & tOut, WORD uVer, InputBuffer_c & return; } + if ( !ApiCheckPerms ( session::GetUser(), AuthAction_e::READ, tRequest.m_sIndexes, tOut ) ) + return; + // fetch stored fields DocHash_t tFetched ( tRequest.m_dDocs.GetLength() ); FieldBlob_t tRes; @@ -646,6 +651,7 @@ void RemotesGetField ( AggrResult_t & tRes, const CSphQuery & tQuery ) // connect to remote agents and query them GetFieldRequestBuilder_t tBuilder ( dFieldCols ); GetFieldReplyParser_t tParser; + SetSessionAuth ( dAgents ); PerformRemoteTasks ( dAgents, &tBuilder, &tParser ); StringBuilder_c sError { "," }; diff --git a/src/searchdha.cpp b/src/searchdha.cpp index 6fbd11e27d..1194223af1 100644 --- a/src/searchdha.cpp +++ b/src/searchdha.cpp @@ -2378,7 +2378,7 @@ bool AgentConn_t::DoQuery() if ( IsPersistent() && m_iSock==-1 ) { { - auto tHdr = APIHeader ( m_tOutput, SEARCHD_COMMAND_PERSIST ); + auto tHdr = APIHeader ( m_tOutput, SEARCHD_COMMAND_PERSIST, 0, m_tAuthToken ); m_tOutput.SendInt ( 1 ); // set persistent to 1. } m_tOutput.StartNewChunk (); diff --git a/src/searchdha.h b/src/searchdha.h index 0c642d7d7a..1ce8432103 100644 --- a/src/searchdha.h +++ b/src/searchdha.h @@ -27,6 +27,7 @@ bool LoadExFunctions (); #include "sphinxutils.h" #include "searchdaemon.h" #include "timeout_queue.h" +#include "auth/auth_proto_api.h" ///////////////////////////////////////////////////////////////////////////// // SOME SHARED GLOBAL VARIABLES @@ -495,6 +496,8 @@ struct AgentConn_t : public ISphRefcountedMT LPKEY m_pPollerTask = nullptr; ///< internal for poller. fixme! privatize? volatile bool m_bSuccess {false}; ///< agent got processed, no need to retry + ApiAuthToken_t m_tAuthToken; + public: AgentConn_t () = default; diff --git a/src/searchdhttp.cpp b/src/searchdhttp.cpp index b8ebf8b3f6..12b5dec37f 100644 --- a/src/searchdhttp.cpp +++ b/src/searchdhttp.cpp @@ -26,6 +26,7 @@ #include "searchdbuddy.h" #include "aggrexpr.h" #include "compressed_http.h" +#include "auth/auth_proto_http.h" static bool g_bLogBadHttpReq = val_from_env ( "MANTICORE_LOG_HTTP_BAD_REQ", false ); // log content of bad http requests, ruled by this env variable static int g_iLogHttpData = val_from_env ( "MANTICORE_LOG_HTTP_DATA", 0 ); // verbose logging of http data, ruled by this env variable @@ -71,6 +72,7 @@ EHTTP_STATUS HttpGetStatusCodes ( int iStatus ) noexcept case 200: return EHTTP_STATUS::_200; case 206: return EHTTP_STATUS::_206; case 400: return EHTTP_STATUS::_400; + case 401: return EHTTP_STATUS::_401; case 403: return EHTTP_STATUS::_403; case 404: return EHTTP_STATUS::_404; case 405: return EHTTP_STATUS::_405; @@ -94,6 +96,7 @@ inline constexpr const char* HttpGetStatusName ( EHTTP_STATUS eStatus ) noexcept case EHTTP_STATUS::_200: return "200 OK"; case EHTTP_STATUS::_206: return "206 Partial Content"; case EHTTP_STATUS::_400: return "400 Bad Request"; + case EHTTP_STATUS::_401: return "400 Unauthorized status"; case EHTTP_STATUS::_403: return "403 Forbidden"; case EHTTP_STATUS::_404: return "404 Not Found"; case EHTTP_STATUS::_405: return "405 Method Not Allowed"; @@ -169,7 +172,8 @@ static Endpoint_t g_dEndpoints[(size_t)EHTTP_ENDPOINT::TOTAL] = { "pq", "json/pq" }, { "cli", nullptr }, { "cli_json", nullptr }, - { "_bulk", nullptr } + { "_bulk", nullptr }, + { "token", nullptr } }; EHTTP_ENDPOINT StrToHttpEndpoint ( const CSphString& sEndpoint ) noexcept @@ -475,6 +479,21 @@ void HttpBuildReply ( CSphVector & dData, EHTTP_STATUS eCode, Str_t sReply dData.Append ( sReply ); } +void HttpBuildReply ( CSphVector & dData, EHTTP_STATUS eCode, Str_t sReply, const char * sHeaderField ) +{ + StringBuilder_c sHttp; + sHttp.Sprintf ( "HTTP/1.1 %s\r\n", HttpGetStatusName ( eCode ) ); + sHttp.Sprintf ( "Content-Type: application/json; charset=UTF-8\r\n" ); + sHttp.Sprintf ( "Content-Length: %d\r\n", sReply.second ); + if ( sHeaderField && *sHeaderField ) + sHttp.Sprintf ( "%s\r\n", sHeaderField ); + + sHttp.Sprintf ( "\r\n" ); + + dData.Reserve ( sHttp.GetLength() + sReply.second ); + dData.Append ( (Str_t)sHttp ); + dData.Append ( sReply ); +} HttpRequestParser_c::HttpRequestParser_c() { @@ -912,7 +931,7 @@ class JsonRequestBuilder_c : public RequestBuilder_i CSphString sRequest = m_tQuery.AsString(); - auto tWr = APIHeader ( tOut, SEARCHD_COMMAND_JSON, VER_COMMAND_JSON ); // API header + auto tWr = APIHeader ( tOut, SEARCHD_COMMAND_JSON, VER_COMMAND_JSON, tAgent.m_tAuthToken ); // API header tOut.SendString ( m_sEndpoint.cstr() ); tOut.SendString ( sRequest.cstr() ); } @@ -1407,6 +1426,9 @@ class HttpSearchHandler_c : public HttpHandler_c, public HttpOptionTrait_t std::unique_ptr tHandler = CreateMsearchHandler ( std::move ( pQueryParser ), m_eQueryType, m_tParsed ); SetStmt ( *tHandler ); + if ( !HttpCheckPerms ( session::GetUser(), AuthAction_e::READ, m_tParsed.m_tQuery.m_sIndexes, m_eHttpCode, m_sError, m_dData ) ) + return false; + QueryProfile_c tProfile; tProfile.m_eNeedPlan = (PLAN_FLAVOUR)m_tParsed.m_iPlan; tProfile.m_bNeedProfile = m_tParsed.m_bProfile; @@ -1669,12 +1691,13 @@ class JsonRowBuffer_c : public RowBuffer_i m_dBuf.FinishBlock ( false ); // root object } - void Error ( const char * szError, EMYSQL_ERR ) override + void Error ( const char * szError, EMYSQL_ERR eErr ) override { auto _ = m_dBuf.Object ( false ); DataFinish ( 0, szError, nullptr ); m_bError = true; m_sError = szError; + m_eError = eErr; } void Ok ( int iAffectedRows, int iWarns, const char * sMessage, bool bMoreResults, int64_t iLastInsertId ) override @@ -1720,6 +1743,8 @@ class JsonRowBuffer_c : public RowBuffer_i return m_dBuf; } + EMYSQL_ERR m_eError = EMYSQL_ERR::UNKNOWN_COM_ERROR; + private: JsonEscapedBuilder m_dBuf; CSphVector m_dColumns; @@ -1878,7 +1903,7 @@ class HttpRawSqlHandler_c final: public HttpHandler_c, public HttpOptionTrait_t session::Execute ( m_sQuery, tOut ); if ( tOut.IsError() ) { - ReportError ( tOut.GetError().scstr(), EHTTP_STATUS::_500 ); + ReportError ( tOut.GetError().scstr(), ( tOut.m_eError==EMYSQL_ERR::ACCESS_DENIED_ERROR ? EHTTP_STATUS::_403 : EHTTP_STATUS::_500 ) ); return false; } BuildReply ( tOut.Finish(), EHTTP_STATUS::_200 ); @@ -2027,6 +2052,9 @@ class HttpHandler_JsonInsert_c final : public HttpHandler_c return false; } + if ( !HttpCheckPerms ( session::GetUser(), AuthAction_e::WRITE, tStmt.m_tQuery.m_sIndexes, m_eHttpCode, m_sError, m_dData ) ) + return false; + tStmt.m_sEndpoint = HttpEndpointToStr ( m_bReplace ? EHTTP_ENDPOINT::JSON_REPLACE : EHTTP_ENDPOINT::JSON_INSERT ); JsonObj_c tResult = JsonNull; bool bResult = ProcessInsert ( tStmt, tDocId, tResult, m_sError, ResultSetFormat_e::MntSearch ); @@ -2104,6 +2132,9 @@ class HttpHandler_JsonUpdate_c : public HttpHandler_c, HttpJsonUpdateTraits_c return false; } + if ( !HttpCheckPerms ( session::GetUser(), AuthAction_e::WRITE, tStmt.m_tQuery.m_sIndexes, m_eHttpCode, m_sError, m_dData ) ) + return false; + JsonObj_c tResult = JsonNull; bool bResult = ProcessQuery ( tStmt, tDocId, tResult ); @@ -2335,6 +2366,10 @@ class HttpHandler_JsonBulk_c : public HttpHandler_c, public HttpJsonUpdateTraits return FinishBulk ( EHTTP_STATUS::_400 ); } + // should check permissions for every new tnx to the new index + if ( tStmt.m_sIndex!=sTxnIdx && !HttpCheckPerms ( session::GetUser(), AuthAction_e::WRITE, tStmt.m_sIndex, m_eHttpCode, m_sError, m_dData ) ) + return false; + if ( sTxnIdx.IsEmpty() ) { sTxnIdx = tStmt.m_sIndex; @@ -2479,6 +2514,13 @@ class HttpHandlerEsBulk_c : public HttpCompatBaseHandler_c, public HttpJsonUpdat void ReportLogError ( const char * sError, HttpErrorType_e eType , EHTTP_STATUS eStatus, bool bLogOnly ); }; +class HttpTokenHandler_c final: public HttpHandler_c, public HttpOptionTrait_t +{ +public: + explicit HttpTokenHandler_c ( const OptionsHash_t & tOptions ); + bool Process () final; +}; + static std::unique_ptr CreateHttpHandler ( EHTTP_ENDPOINT eEndpoint, CharStream_c & tSource, Str_t & sQuery, OptionsHash_t & tOptions, http_method eRequestType ) { const CSphString * pOption = nullptr; @@ -2598,6 +2640,9 @@ static std::unique_ptr CreateHttpHandler ( EHTTP_ENDPOINT eEndpoi else return std::make_unique ( sQuery, eRequestType, tOptions ); + case EHTTP_ENDPOINT::TOKEN: + return std::make_unique ( tOptions ); + case EHTTP_ENDPOINT::TOTAL: SetQuery ( tSource.ReadAll() ); if ( tSource.GetError() ) @@ -2612,7 +2657,7 @@ static std::unique_ptr CreateHttpHandler ( EHTTP_ENDPOINT eEndpoi return nullptr; } -HttpProcessResult_t ProcessHttpQuery ( CharStream_c & tSource, Str_t & sSrcQuery, OptionsHash_t & hOptions, CSphVector & dResult, bool bNeedHttpResponse, http_method eRequestType ) +HttpProcessResult_t ProcessHttpQuery ( CharStream_c & tSource, Str_t & sSrcQuery, OptionsHash_t & hOptions, CSphVector & dResult, bool bNeedHttpResponse, http_method eRequestType, bool bSkipAuth ) { TRACE_CONN ( "conn", "ProcessHttpQuery" ); @@ -2620,6 +2665,12 @@ HttpProcessResult_t ProcessHttpQuery ( CharStream_c & tSource, Str_t & sSrcQuery const CSphString & sEndpoint = hOptions["endpoint"]; tRes.m_eEndpoint = StrToHttpEndpoint ( sEndpoint ); + CSphString sUser; + if ( !bSkipAuth && !CheckAuth ( hOptions, tRes, dResult, sUser ) ) + return tRes; + + // should set client user to pass it further into distributed index + session::SetUser ( sUser ); std::unique_ptr pHandler = CreateHttpHandler ( tRes.m_eEndpoint, tSource, sSrcQuery, hOptions, eRequestType ); if ( !pHandler ) @@ -2652,11 +2703,13 @@ HttpProcessResult_t ProcessHttpQuery ( CharStream_c & tSource, Str_t & sSrcQuery tRes.m_bOk = pHandler->Process(); tRes.m_sError = pHandler->GetError(); tRes.m_eReplyHttpCode = pHandler->GetStatusCode(); + tRes.m_bSkipBuddy = ( tRes.m_eReplyHttpCode==EHTTP_STATUS::_403 ); // error with status code 403 Forbidden should not route into buddy dResult = std::move ( pHandler->GetResult() ); return tRes; } +/// json command over API void sphProcessHttpQueryNoResponce ( const CSphString & sEndpoint, const CSphString & sQuery, CSphVector & dResult ) { OptionsHash_t hOptions; @@ -2664,7 +2717,8 @@ void sphProcessHttpQueryNoResponce ( const CSphString & sEndpoint, const CSphStr BlobStream_c tQuery ( sQuery ); Str_t sSrcQuery; - HttpProcessResult_t tRes = ProcessHttpQuery ( tQuery, sSrcQuery, hOptions, dResult, false, HTTP_GET ); + // no need to issue authentification as it checked at the API interface + HttpProcessResult_t tRes = ProcessHttpQuery ( tQuery, sSrcQuery, hOptions, dResult, false, HTTP_GET, true ); ProcessHttpQueryBuddy ( tRes, sSrcQuery, hOptions, dResult, false, HTTP_GET ); } @@ -2719,7 +2773,7 @@ bool HttpRequestParser_c::ProcessClientHttp ( AsyncNetInputBuffer_c& tIn, CSphVe } else { - tRes = ProcessHttpQuery ( *pSource, sSrcQuery, m_hOptions, dResult, true, m_eType ); + tRes = ProcessHttpQuery ( *pSource, sSrcQuery, m_hOptions, dResult, true, m_eType, false ); } return ProcessHttpQueryBuddy ( tRes, sSrcQuery, m_hOptions, dResult, true, m_eType ); @@ -2733,6 +2787,15 @@ void sphHttpErrorReply ( CSphVector & dData, EHTTP_STATUS eCode, const cha HttpBuildReply ( dData, eCode, FromStr (sJsonError), false ); } +void sphHttpErrorReply ( CSphVector & dData, EHTTP_STATUS eCode, const char * sError, const char * sHeaderField ) +{ + JsonObj_c tErr; + tErr.AddStr ( "error", sError ); + CSphString sJsonError = tErr.AsString(); + + HttpBuildReply ( dData, eCode, FromStr ( sJsonError ), sHeaderField ); +} + static void EncodePercolateMatchResult ( const PercolateMatchResult_t & tRes, const CSphFixedVector & dDocids, const CSphString & sIndex, JsonEscapedBuilder & tOut ) { ScopedComma_c sRootBlock ( tOut, ",", "{", "}" ); @@ -3102,7 +3165,12 @@ bool HttpHandlerPQ_c::Process() eOp = PercolateOp_e::SEARCH; if ( IsEmpty ( m_sQuery ) ) + { + if ( !HttpCheckPerms ( session::GetUser(), AuthAction_e::READ, sIndex, m_eHttpCode, m_sError, m_dData ) ) + return false; + return ListQueries ( sIndex ); + } const JsonObj_c tRoot ( m_sQuery ); if ( !tRoot ) @@ -3112,7 +3180,12 @@ bool HttpHandlerPQ_c::Process() } if ( !tRoot.Size() ) + { + if ( !HttpCheckPerms ( session::GetUser(), AuthAction_e::READ, sIndex, m_eHttpCode, m_sError, m_dData ) ) + return false; + return ListQueries ( sIndex ); + } if ( eOp==PercolateOp_e::UNKNOWN ) { @@ -3136,6 +3209,9 @@ bool HttpHandlerPQ_c::Process() return false; } + if ( !HttpCheckPerms ( session::GetUser(), ( eOp==PercolateOp_e::SEARCH ? AuthAction_e::READ : AuthAction_e::WRITE ), sIndex, m_eHttpCode, m_sError, m_dData ) ) + return false; + bool bVerbose = false; JsonObj_c tVerbose = tRoot.GetItem ( "verbose" ); if ( tVerbose ) @@ -3413,6 +3489,9 @@ bool HttpHandlerEsBulk_c::Process() { bNextLineMeta = false; } + + if ( !HttpCheckPerms ( session::GetUser(), AuthAction_e::WRITE, tDoc.m_sIndex, m_eHttpCode, m_sError, m_dData ) ) + return false; } } CSphVector dTnx; @@ -3682,3 +3761,23 @@ const char * GetErrorTypeName ( HttpErrorType_e eType ) return nullptr;; } } + +HttpTokenHandler_c::HttpTokenHandler_c ( const OptionsHash_t & tOptions ) + : HttpOptionTrait_t ( tOptions ) +{ +} +bool HttpTokenHandler_c::Process () +{ + TRACE_CONN ( "conn", "HttpTokenHandler_c::Process" ); + + if( !HttpCheckPerms ( session::GetUser(), AuthAction_e::ADMIN, CSphString(), m_eHttpCode, m_sError, m_dData ) ) + return false; + + CSphString sToken = CreateSessionToken(); + + StringBuilder_c tOut; + tOut.Sprintf ( R"( {"token":"%s"} )", sToken.cstr() ); + + BuildReply ( tOut.cstr(), EHTTP_STATUS::_200 ); + return true; +} diff --git a/src/searchdhttp.h b/src/searchdhttp.h index d374fec66c..12a933fd34 100644 --- a/src/searchdhttp.h +++ b/src/searchdhttp.h @@ -80,7 +80,8 @@ class HttpRequestParser_c : public ISphNoncopyable http_parser m_tParser; }; -void HttpBuildReply ( CSphVector& dData, EHTTP_STATUS eCode, Str_t sReply, bool bHtml ); +void HttpBuildReply ( CSphVector & dData, EHTTP_STATUS eCode, Str_t sReply, bool bHtml ); +void HttpBuildReply ( CSphVector & dData, EHTTP_STATUS eCode, Str_t sReply, const StrVec_t & dHeaderFields ); /////////////////////////////////////////////////////////////////////// /// Stream reader @@ -114,12 +115,15 @@ struct HttpProcessResult_t { EHTTP_ENDPOINT m_eEndpoint { EHTTP_ENDPOINT::TOTAL }; EHTTP_STATUS m_eReplyHttpCode = EHTTP_STATUS::_200; + bool m_bOk { false }; + bool m_bSkipBuddy { false }; // auth error should not pass into buddy but only to client + CSphString m_sError; }; void ReplyBuf ( Str_t sResult, EHTTP_STATUS eStatus, bool bNeedHttpResponse, CSphVector & dData ); -HttpProcessResult_t ProcessHttpQuery ( CharStream_c & tSource, Str_t & sSrcQuery, OptionsHash_t & hOptions, CSphVector & dResult, bool bNeedHttpResponse, http_method eRequestType ); +HttpProcessResult_t ProcessHttpQuery ( CharStream_c & tSource, Str_t & sSrcQuery, OptionsHash_t & hOptions, CSphVector & dResult, bool bNeedHttpResponse, http_method eRequestType, bool bSkipAuth ); namespace bson { class Bson_c; diff --git a/src/searchdreplication.cpp b/src/searchdreplication.cpp index 40b16efcf1..d4ee770e62 100644 --- a/src/searchdreplication.cpp +++ b/src/searchdreplication.cpp @@ -19,6 +19,7 @@ #include "coroutine.h" #include "digest_sha1.h" #include "tracer.h" +#include "auth/auth.h" #include "replication/wsrep_cxx.h" #include "replication/common.h" @@ -96,6 +97,7 @@ struct ReplicationCluster_t final : public ClusterDesc_t, Wsrep::Cluster_i { m_sName = std::move ( tDesc.m_sName ); m_sPath = std::move ( tDesc.m_sPath ); + m_sUser = std::move ( tDesc.m_sUser ); m_dClusterNodes = std::move ( tDesc.m_dClusterNodes ); m_tOptions = std::move ( tDesc.m_tOptions ); } @@ -1541,7 +1543,7 @@ static std::optional CheckClusterOption ( const SmallStringHash_Tm_sVal.IsEmpty()) { TlsMsg::Err ( "'%s' should have a string value", szName ); - return {}; + return std::nullopt; } return ( *ppVal )->m_sVal; @@ -1608,12 +1610,17 @@ static std::optional ClusterDescFromSphinxqlStatement ( const CSp if ( !ClusterCheckPath ( tPath.value(), sCluster.cstr(), true ) ) return tDesc; + auto tUser = CheckClusterOption ( hValues, "user" ); + if ( !tUser ) + return tDesc; + // all is ok, create cluster desc tDesc.emplace(); tDesc->m_sName = sCluster; tDesc->m_sPath = tPath.value(); tDesc->m_dClusterNodes = dClusterNodes; tDesc->m_tOptions.Parse ( tOptions.value() ); + tDesc->m_sUser = ( tUser.value().IsEmpty() ? session::GetUser() : tUser.value() ); return tDesc; } @@ -1628,6 +1635,11 @@ bool ClusterJoin ( const CSphString & sCluster, const StrVec_t & dNames, const C if ( !tDesc ) return false; + CSphString sError; + // cluster user (might be not the session user) should be able to perform replication actions + if ( IsAuthEnabled() && !CheckPerms ( tDesc->m_sUser, AuthAction_e::REPLICATION, CSphString(), true, sError ) ) + return false; + sphLogDebugRpl ( "joining cluster '%s', nodes: %s", sCluster.cstr(), StrVec2Str ( tDesc->m_dClusterNodes ).cstr() ); // need to clean up Galera system files left from previous cluster @@ -1688,6 +1700,11 @@ bool ClusterCreate ( const CSphString & sCluster, const StrVec_t & dNames, const if ( !tDesc ) return TlsMsg::Err ( "failed to create desc: %s", TlsMsg::szError() ); + CSphString sPermError; + // cluster user (might be not the session user) should be able to perform replication actions + if ( IsAuthEnabled() && !CheckPerms ( tDesc->m_sUser, AuthAction_e::REPLICATION, CSphString(), true, sPermError ) ) + return TlsMsg::Err ( sPermError ); + // need to clean up Galera system files left from previous cluster CleanClusterFiles ( GetDatadirPath ( tDesc->m_sPath ) ); @@ -1804,7 +1821,7 @@ bool GloballyDeleteCluster ( const CSphString & sCluster, CSphString & sError ) } auto dNodes = pCluster->FilterViewNodesByProto(); - SendClusterDeleteToNodes ( dNodes, sCluster ); + SendClusterDeleteToNodes ( dNodes, sCluster, pCluster->m_sUser ); bool bOk = ClusterDelete ( sCluster ); bOk &= SaveConfigInt ( sError ); @@ -1888,7 +1905,7 @@ static bool SendIndex ( const CSphString & sIndex, ReplicationClusterRefPtr_c pC if ( dDesc.GetLength() ) { - bool bReplicated = ( bMutable ? ReplicateIndexToNodes ( pCluster->m_sName, sIndex, dDesc, pServed ) : ReplicateDistIndexToNodes ( pCluster->m_sName, sIndex, dDesc ) ); + bool bReplicated = ( bMutable ? ReplicateIndexToNodes ( pCluster->m_sName, sIndex, pCluster->m_sUser, dDesc, pServed ) : ReplicateDistIndexToNodes ( pCluster->m_sName, sIndex, pCluster->m_sUser, dDesc ) ); if ( !bReplicated ) { if ( TlsMsg::HasErr() ) @@ -2118,7 +2135,7 @@ bool SendClusterIndexes ( const ReplicationCluster_t * pCluster, const CSphStrin tSyncedRequest.m_dIndexes = dIndexes; if ( bBypass ) - return SendClusterSynced ( dDesc, tSyncedRequest ); + return SendClusterSynced ( dDesc, tSyncedRequest, pCluster->m_sUser ); bool bSentOk = true; while ( true ) @@ -2137,7 +2154,7 @@ bool SendClusterIndexes ( const ReplicationCluster_t * pCluster, const CSphStrin continue; } - bool bReplicated = ( bMutable ? ReplicateIndexToNodes ( pCluster->m_sName, sIndex, dDesc, pServed ) : ReplicateDistIndexToNodes ( pCluster->m_sName, sIndex, dDesc ) ); + bool bReplicated = ( bMutable ? ReplicateIndexToNodes ( pCluster->m_sName, sIndex, pCluster->m_sUser, dDesc, pServed ) : ReplicateDistIndexToNodes ( pCluster->m_sName, sIndex, pCluster->m_sUser, dDesc ) ); if ( !bReplicated ) { sphWarning ( "%s", TlsMsg::szError() ); @@ -2165,7 +2182,7 @@ bool SendClusterIndexes ( const ReplicationCluster_t * pCluster, const CSphStrin tSyncedRequest.m_dIndexes = dIndexes; tSyncedRequest.m_bSendFilesSuccess = bSentOk; tSyncedRequest.m_sMsg = TlsMsg::MoveToString(); - bool bSyncOk = SendClusterSynced ( dDesc, tSyncedRequest ); + bool bSyncOk = SendClusterSynced ( dDesc, tSyncedRequest, pCluster->m_sUser ); return bSentOk && bSyncOk; } @@ -2274,7 +2291,7 @@ bool DoClusterAlterUpdate ( const CSphString & sCluster, const CSphString & sUpd return false; // remote nodes update after locals updated - if ( !SendClusterUpdateNodes ( sCluster, eNodes, dNodes ) ) + if ( !SendClusterUpdateNodes ( sCluster, pCluster->m_sUser, eNodes, dNodes ) ) { sphWarning ( "cluster %s nodes update error %s", sCluster.cstr(), TlsMsg::szError() ); TlsMsg::ResetErr(); diff --git a/src/searchdsql.h b/src/searchdsql.h index 1ffd06ad48..99f8e90eae 100644 --- a/src/searchdsql.h +++ b/src/searchdsql.h @@ -162,6 +162,10 @@ enum SqlStmt_e STMT_SHOW_LOCKS, STMT_SHOW_SCROLL, STMT_SHOW_TABLE_INDEXES, + STMT_RELOAD_AUTH, + STMT_SHOW_PERMISSIONS, + STMT_SHOW_USERS, + STMT_SHOW_TOKEN, STMT_TOTAL }; diff --git a/src/sphinx.h b/src/sphinx.h index a2e056cbd7..f4d1148a6c 100644 --- a/src/sphinx.h +++ b/src/sphinx.h @@ -58,7 +58,7 @@ extern const char * szGIT_BRANCH_ID; extern const char * szGDB_SOURCE_DIR; #define SPHINX_SEARCHD_PROTO 1 -#define SPHINX_CLIENT_VERSION 1 +#define SPHINX_CLIENT_VERSION 2 constexpr int64_t SMALL_INDEX_THRESH = 8192; diff --git a/src/sphinxjson.cpp b/src/sphinxjson.cpp index 000a9cbc00..2b7bbc162c 100644 --- a/src/sphinxjson.cpp +++ b/src/sphinxjson.cpp @@ -3108,11 +3108,16 @@ bool Bson_c::BsonToBson ( CSphVector &dOutput ) const } -const char * Bson_c::sError () const +const char * Bson_c::Error () const { return m_sError.cstr(); } +bool Bson_c::HasError () const +{ + return !m_sError.IsEmpty(); +} + BsonIterator_c::BsonIterator_c ( const NodeHandle_t &dParent ) { if ( bson::IsNullNode ( dParent ) ) diff --git a/src/sphinxjson.h b/src/sphinxjson.h index 28bfa2f9f7..f639cce3cd 100644 --- a/src/sphinxjson.h +++ b/src/sphinxjson.h @@ -658,7 +658,8 @@ class Bson_c // helpers inline ESphJsonType GetType() const { return m_dData.second; } operator NodeHandle_t () const { return m_dData; } - const char * sError () const; + const char * Error () const; + bool HasError () const; }; // iterate over Bson_c diff --git a/src/sphinxql.l b/src/sphinxql.l index 197a9aabf2..d3f3ea30b7 100644 --- a/src/sphinxql.l +++ b/src/sphinxql.l @@ -176,6 +176,7 @@ FLOAT_CONSTANT {INT}\.{INT}?{EXP}?|{INT}?\.{INT}{EXP}|{INT}{EXP} "OPTIMIZE" { YYSTOREBOUNDS; return TOK_OPTIMIZE; } "OR" { YYSTOREBOUNDS; return TOK_OR; } "ORDER" { YYSTOREBOUNDS; return TOK_ORDER; } +"PERMISSIONS" { YYSTOREBOUNDS; return TOK_PERMISSIONS; } "PLAN" { YYSTOREBOUNDS; return TOK_PLAN; } "PLUGINS" { YYSTOREBOUNDS; return TOK_PLUGINS; } "PROFILE" { YYSTOREBOUNDS; return TOK_PROFILE; } @@ -205,10 +206,12 @@ FLOAT_CONSTANT {INT}\.{INT}?{EXP}?|{INT}?\.{INT}{EXP}|{INT}{EXP} "TABLES" { YYSTOREBOUNDS; return TOK_TABLES; } "THREADS" { YYSTOREBOUNDS; return TOK_THREADS; } "TO" { YYSTOREBOUNDS; return TOK_TO; } +"TOKEN" { YYSTOREBOUNDS; return TOK_TOKEN; } "TRANSACTION" { YYSTOREBOUNDS; return TOK_TRANSACTION; } "TRUE" { YYSTOREBOUNDS; return TOK_TRUE; } "UNFREEZE" { YYSTOREBOUNDS; return TOK_UNFREEZE; } "UPDATE" { YYSTOREBOUNDS; return TOK_UPDATE; } +"USERS" { YYSTOREBOUNDS; return TOK_USERS; } "VALUES" { YYSTOREBOUNDS; return TOK_VALUES; } "VARIABLES" { YYSTOREBOUNDS; return TOK_VARIABLES; } "WARNINGS" { YYSTOREBOUNDS; return TOK_WARNINGS; } diff --git a/src/sphinxql.y b/src/sphinxql.y index 1b8cd77e87..89246427d2 100644 --- a/src/sphinxql.y +++ b/src/sphinxql.y @@ -122,6 +122,7 @@ %token TOK_OPTION %token TOK_ORDER %token TOK_OPTIMIZE +%token TOK_PERMISSIONS %token TOK_PLAN %token TOK_PLUGINS %token TOK_PROFILE @@ -152,10 +153,12 @@ %token TOK_TABLES %token TOK_THREADS %token TOK_TO +%token TOK_TOKEN %token TOK_TRANSACTION %token TOK_TRUE %token TOK_UNFREEZE %token TOK_UPDATE +%token TOK_USERS %token TOK_VALUES %token TOK_VARIABLES %token TOK_WARNINGS @@ -277,7 +280,7 @@ reserved_tokens_without_option: | TOK_WARNINGS | TOK_WEIGHT | TOK_WHERE | TOK_WITHIN | TOK_KILL | TOK_QUERY | TOK_INTERVAL | TOK_REGEX | TOK_DATE_ADD | TOK_DATE_SUB | TOK_DAY | TOK_HOUR | TOK_MINUTE | TOK_MONTH | TOK_QUARTER | TOK_SECOND | TOK_WEEK | TOK_YEAR - | TOK_LOCKS | TOK_SCROLL + | TOK_LOCKS | TOK_SCROLL | TOK_USERS | TOK_PERMISSIONS ; names_transaction_collate: @@ -1579,6 +1582,23 @@ show_what: { pParser->m_pStmt->m_eStmt = STMT_SHOW_LOCKS; } + | TOK_PERMISSIONS + { + pParser->m_pStmt->m_eStmt = STMT_SHOW_PERMISSIONS; + } + | TOK_USERS + { + pParser->m_pStmt->m_eStmt = STMT_SHOW_USERS; + } + | TOK_TOKEN + { + pParser->m_pStmt->m_eStmt = STMT_SHOW_TOKEN; + } + | TOK_TOKEN TOK_QUOTED_STRING + { + pParser->m_pStmt->m_dCallStrings.Add() = pParser->ToStringUnescape ( $2 ); + pParser->m_pStmt->m_eStmt = STMT_SHOW_TOKEN; + } ; index_or_table: diff --git a/src/sphinxql_second.l b/src/sphinxql_second.l index 6e52d25b1d..6d276cd732 100644 --- a/src/sphinxql_second.l +++ b/src/sphinxql_second.l @@ -90,6 +90,7 @@ FLOAT_CONSTANT ({INT}\.{INT}?|{INT}?\.{INT}){EXPONENT}? "ATTACH" { STORE_BOUNDS; return TOK_ATTACH; } "ATTRIBUTES" { STORE_BOUNDS; return TOK_ATTRIBUTES; } +"AUTH" { STORE_BOUNDS; return TOK_AUTH; } "CLUSTER" { STORE_BOUNDS; return TOK_CLUSTER; } "COMMITTED" { STORE_BOUNDS; return TOK_COMMITTED; } "COMPRESS" { STORE_BOUNDS; return TOK_COMPRESS; } diff --git a/src/sphinxql_second.y b/src/sphinxql_second.y index ff38efaf3d..167cf60885 100644 --- a/src/sphinxql_second.y +++ b/src/sphinxql_second.y @@ -25,6 +25,7 @@ %token TOK_ATTACH %token TOK_ATTRIBUTES +%token TOK_AUTH %token TOK_BACKTICKED_SUBKEY %token TOK_BAD_NUMERIC %token TOK_CLUSTER @@ -101,6 +102,7 @@ statement: | freeze_indexes | unfreeze_indexes | kill_connid + | reload_auth ; ////////////////////////////////////////////////////////////////////////// @@ -499,6 +501,13 @@ option_item: } ; +reload_auth: + TOK_RELOAD TOK_AUTH + { + SqlStmt_t & tStmt = *pParser->m_pStmt; + tStmt.m_eStmt = STMT_RELOAD_AUTH; + } + ; %% diff --git a/src/sphinxutils.cpp b/src/sphinxutils.cpp index d00b2fafb7..29a6ae48b9 100644 --- a/src/sphinxutils.cpp +++ b/src/sphinxutils.cpp @@ -1086,6 +1086,9 @@ static KeyDesc_t g_dKeysSearchd[] = { "diskchunk_flush_write_timeout", 0, nullptr }, { "diskchunk_flush_search_timeout", 0, nullptr }, { "kibana_version_string", 0, NULL }, + { "auth_user", 0, NULL }, + { "auth_pass", 0, NULL }, + { "auth_user_file", 0, NULL }, { NULL, 0, NULL } }; diff --git a/src/std/base64.cpp b/src/std/base64.cpp index 57ae6c3e9f..ce8ac1ba7b 100644 --- a/src/std/base64.cpp +++ b/src/std/base64.cpp @@ -45,3 +45,28 @@ CSphString EncodeBase64 ( const CSphString & sValue ) auto sTmp = std::string(It(std::begin(sVal)), It(std::end(sVal))); return sTmp.append((3 - sVal.size() % 3) % 3, '=').c_str(); } + +void DecodeBinBase64 ( const CSphString & sSrc, CSphVector & dDst ) +{ + std::string_view sVal = sSrc.cstr(); + dDst.Reserve ( dDst.GetLength() + ( ( sVal.size() + 2 ) / 3 ) * 4 ); + + using namespace boost::archive::iterators; + using It = transform_width, 8, 6>; + for ( It tIt ( std::begin(sVal) ); tIt!=It( std::end(sVal) ); tIt++ ) + dDst.Add ( *tIt ); + + while ( dDst.GetLength() && dDst.Last()=='\0' ) + dDst.Pop(); +} + + +CSphString EncodeBinBase64 ( const VecTraits_T & dSrc ) +{ + std::string_view sVal { (const char *)dSrc.Begin(), (std::string_view::size_type)dSrc.GetLength() }; + + using namespace boost::archive::iterators; + using It = base64_from_binary>; + auto sTmp = std::string(It(std::begin(sVal)), It(std::end(sVal))); + return sTmp.append((3 - sVal.size() % 3) % 3, '=').c_str(); +} diff --git a/src/std/base64.h b/src/std/base64.h index 41c8fbc898..a0ad1ca355 100644 --- a/src/std/base64.h +++ b/src/std/base64.h @@ -14,3 +14,6 @@ CSphString EncodeBase64 ( const CSphString & sValue ); bool DecodeBase64 ( const CSphString & sValue, CSphString & sResult ); + +void DecodeBinBase64 ( const CSphString & sSrc, CSphVector & dDst ); +CSphString EncodeBinBase64 ( const VecTraits_T & dSrc ); diff --git a/src/taskping.cpp b/src/taskping.cpp index 7dc34972eb..111e192a10 100644 --- a/src/taskping.cpp +++ b/src/taskping.cpp @@ -25,7 +25,7 @@ class PingBuilder_c final : public RequestBuilder_i, public ReplyParser_i, publi void BuildRequest ( const AgentConn_t&, ISphOutputBuffer& tOut ) const final { // API header - auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_PING, VER_COMMAND_PING ); + auto tHdr = APIHeader ( tOut, SEARCHD_COMMAND_PING, VER_COMMAND_PING, ApiAuthToken_t() ); tOut.SendInt ( m_iSendCookie ); } diff --git a/test/clt-tests/core/authentification.rec b/test/clt-tests/core/authentification.rec new file mode 100644 index 0000000000..4f0c21a342 --- /dev/null +++ b/test/clt-tests/core/authentification.rec @@ -0,0 +1,60 @@ +––– input ––– +echo "password" | htpasswd -s -c -i /tmp/.htpasswd user +––– output ––– +Adding password for user user +––– input ––– +sed -i 's/searchd {/searchd {\nauth_user_file = \/tmp\/.htpasswd/g' /etc/manticoresearch/manticore.conf +––– output ––– +––– input ––– +timeout 5 searchd --nodetach | awk -F 'FATAL: ' '{print $2}' +––– output ––– +file '/tmp/.htpasswd' has permissions to all +––– input ––– +chmod 600 /tmp/.htpasswd +––– output ––– +––– block: ../base/start-searchd ––– +––– input ––– +mysql -h0 -P9306 -e "show version\G" +––– output ––– +ERROR 1047 (08S01): Access denied for user 'root' (using password: NO) +––– input ––– +mysql -h0 -P9306 -e "show version\G" -uuser -ppassword +––– output ––– +mysql: [Warning] Using a password on the command line interface can be insecure. +*************************** 1. row *************************** +Component: Daemon +Version: %{VERSION} +*************************** 2. row *************************** +Component: Columnar +Version: columnar %{VERSION} +*************************** 3. row *************************** +Component: Secondary +Version: secondary %{VERSION} +*************************** 4. row *************************** +Component: KNN +Version: knn %{VERSION} +*************************** 5. row *************************** +Component: Buddy +Version: buddy %{VERSION} +––– input ––– +sed -i 's/auth_user_file = \/tmp\/.htpasswd//g' /etc/manticoresearch/manticore.conf +––– output ––– +––– block: ../base/start-searchd ––– +––– input ––– +mysql -h0 -P9306 -e "show version\G" +––– output ––– +*************************** 1. row *************************** +Component: Daemon +Version: %{VERSION} +*************************** 2. row *************************** +Component: Columnar +Version: columnar %{VERSION} +*************************** 3. row *************************** +Component: Secondary +Version: secondary %{VERSION} +*************************** 4. row *************************** +Component: KNN +Version: knn %{VERSION} +*************************** 5. row *************************** +Component: Buddy +Version: buddy %{VERSION} diff --git a/test/helpers.inc b/test/helpers.inc index e6b6d186af..ea4c1fd7bb 100644 --- a/test/helpers.inc +++ b/test/helpers.inc @@ -6,6 +6,9 @@ ini_set ('memory_limit', '256M'); require_once ( $g_locals['api'] ); +$g_daemon_user = "test2"; +$g_daemon_user_password = "test2"; + function MyMicrotime () { $q = @gettimeofday(); @@ -594,7 +597,7 @@ function GetTmDelta() function StartSearchd ( $config_file, $error_file, $pidfile, &$error, $requirements, $addr=false, $port=false ) { - global $g_locals, $windows, $cygwin, $action_wait_timeout, $sd_address, $sd_port; + global $g_locals, $windows, $cygwin, $action_wait_timeout, $sd_address, $sd_port, $g_daemon_user, $g_daemon_user_password; $abs_config_file = testdir($config_file); @@ -720,6 +723,7 @@ function StartSearchd ( $config_file, $error_file, $pidfile, &$error, $requireme $cl = new SphinxClient; $cl->SetServer ( $addr, $port ); $cl->SetConnectTimeout ( 10 ); + $cl->SetUser ( $g_daemon_user, $g_daemon_user_password ); $ok = false; $start = MyMicrotime(); @@ -1059,7 +1063,7 @@ function TouchVal ( &$var, $name, $default="" ) function ConnectSpecificQL($agent, $vip=false) { - global $agents; + global $agents, $g_daemon_user, $g_daemon_user_password; $address = $agents[$agent]["address"]; if ( $vip ) @@ -1071,7 +1075,7 @@ function ConnectSpecificQL($agent, $vip=false) if ($address == "localhost") $address = "127.0.0.1"; mysqli_report(MYSQLI_REPORT_OFF); - return @mysqli_connect ( $address, '', '', '', $port ); + return @mysqli_connect ( $address, $g_daemon_user, $g_daemon_user_password, '', $port ); } function ConnectQL() @@ -1271,9 +1275,14 @@ function HttpFormatResultSet ( $result, $nquery, $keep_json_ctrls=false ) function RunCurl ( $curl_desc, &$rows ) { + global $g_daemon_user, $g_daemon_user_password; + $con = curl_init(); curl_setopt_array ( $con, $curl_desc ); + curl_setopt($con, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($con, CURLOPT_USERPWD, "$g_daemon_user:$g_daemon_user_password"); + $rows = curl_exec ( $con ); $rows = preg_replace('/"time":\d+(\.\d+)*,/', '"time":0.000,', $rows); $http_code = curl_getinfo ( $con, CURLINFO_HTTP_CODE ); @@ -2416,10 +2425,11 @@ class SphinxConfig function RunQueryApiUpdate ( $query, &$error, &$examples ) { - global $sd_address, $sd_port; + global $sd_address, $sd_port, $g_daemon_user, $g_daemon_user_password; $cl = new APIClient(); $cl->SetServer ( $sd_address, $sd_port ); + $cl->SetUser ( $g_daemon_user, $g_daemon_user_password ); $res = $cl->XUpdateAttributes ($query['index'], $query['query']['attrs'], $query['query']['values'], $query['mode'] ); $query_result = array ('updated'=> $res); @@ -2852,7 +2862,7 @@ class SphinxConfig function RunQuery ( &$error, &$examples, $benchmark = false ) { - global $sd_address, $sd_port, $action_retries, $action_wait_timeout, $g_pick_query; + global $sd_address, $sd_port, $action_retries, $action_wait_timeout, $g_pick_query, $g_daemon_user, $g_daemon_user_password; $total = $done = 0; if ( $benchmark ) @@ -2867,10 +2877,12 @@ class SphinxConfig $cl = new APIClient; $pconn = $benchmark && method_exists ( $cl, 'Open' ); + $cl->SetUser ( $g_daemon_user, $g_daemon_user_password ); if ( $pconn ) { $cl = new APIClient; $cl->SetServer ( $sd_address, $sd_port ); + $cl->SetUser ( $g_daemon_user, $g_daemon_user_password ); $cl->Open (); } @@ -2937,6 +2949,7 @@ class SphinxConfig { $cl = new APIClient(); $cl->SetServer ( $sd_address, $sd_port ); + $cl->SetUser ( $g_daemon_user, $g_daemon_user_password ); } else { $cl->ResetFilters (); @@ -3059,7 +3072,7 @@ class SphinxConfig function RunCustomTest ( & $error ) { - global $sd_address, $sd_port, $action_retries, $action_wait_timeout, $g_locals; + global $sd_address, $sd_port, $action_retries, $action_wait_timeout, $g_locals, $g_daemon_user, $g_daemon_user_password; $bOk = false; $results = false; @@ -3068,6 +3081,7 @@ class SphinxConfig { $cl = new APIClient; $cl->SetServer ( $sd_address, $sd_port ); + $cl->SetUser ( $g_daemon_user, $g_daemon_user_password ); $results = false; $run_func = function( $client, $ql, &$results ) { eval( $this->_custom_test ); }; @@ -4240,7 +4254,7 @@ class SphinxConfig function WriteSearchdSettings ( $fp ) { - global $sd_log, $sd_split_logs, $sd_query_log, $sd_network_timeout, $sd_max_children, $sd_pid_file; + global $sd_log, $sd_split_logs, $sd_query_log, $sd_network_timeout, $sd_max_children, $sd_pid_file, $windows; if ( $this->_compat098 ) { @@ -4280,6 +4294,12 @@ class SphinxConfig fwrite ( $fp, "\tmax_connections = $sd_max_children\n" ); fwrite ( $fp, "\tpid_file = ".$this->_sd_pid_file."\n" ); fwrite ( $fp, "#\tbinlog_path =\n" ); + if ( $windows ) + { + //fwrite ( $fp, "\tauth_user_file = C:\\dev\\sphinx\\build\\m_dbg22\\test\\.htpasswd \n" ); + }else { + //fwrite ( $fp, "\tauth_user_file = /home/stas/build/m_dbg/test/.htpasswd\n" ); + } } function WriteSqlSettings ( $fp, $attributes )