Merge pull request #70725 from azat/http_handlers-auth

Add ability to set user/password in http_handlers
This commit is contained in:
Vitaly Baranov 2024-11-01 19:00:15 +00:00 committed by GitHub
commit 08ce37f3e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 192 additions and 37 deletions

View File

@ -15,6 +15,9 @@ public:
explicit Credentials() = default;
explicit Credentials(const String & user_name_);
Credentials(const Credentials &) = default;
Credentials(Credentials &&) = default;
virtual ~Credentials() = default;
const String & getUserName() const;

View File

@ -6,6 +6,7 @@
#include <Access/ExternalAuthenticators.h>
#include <Common/Base64.h>
#include <Common/HTTPHeaderFilter.h>
#include <Server/HTTPHandler.h>
#include <Server/HTTP/HTTPServerRequest.h>
#include <Server/HTTP/HTMLForm.h>
#include <Server/HTTP/HTTPServerResponse.h>
@ -54,11 +55,13 @@ bool authenticateUserByHTTP(
HTTPServerResponse & response,
Session & session,
std::unique_ptr<Credentials> & request_credentials,
const HTTPHandlerConnectionConfig & connection_config,
ContextPtr global_context,
LoggerPtr log)
{
/// Get the credentials created by the previous call of authenticateUserByHTTP() while handling the previous HTTP request.
auto current_credentials = std::move(request_credentials);
const auto & config_credentials = connection_config.credentials;
/// The user and password can be passed by headers (similar to X-Auth-*),
/// which is used by load balancers to pass authentication information.
@ -70,6 +73,7 @@ bool authenticateUserByHTTP(
/// The header 'X-ClickHouse-SSL-Certificate-Auth: on' enables checking the common name
/// extracted from the SSL certificate used for this connection instead of checking password.
bool has_ssl_certificate_auth = (request.get("X-ClickHouse-SSL-Certificate-Auth", "") == "on");
bool has_config_credentials = config_credentials.has_value();
/// User name and password can be passed using HTTP Basic auth or query parameters
/// (both methods are insecure).
@ -79,6 +83,10 @@ bool authenticateUserByHTTP(
std::string spnego_challenge;
SSLCertificateSubjects certificate_subjects;
if (config_credentials)
{
checkUserNameNotEmpty(config_credentials->getUserName(), "config authentication");
}
if (has_ssl_certificate_auth)
{
#if USE_SSL
@ -86,6 +94,8 @@ bool authenticateUserByHTTP(
checkUserNameNotEmpty(user, "X-ClickHouse HTTP headers");
/// It is prohibited to mix different authorization schemes.
if (has_config_credentials)
throwMultipleAuthenticationMethods("SSL certificate authentication", "authentication set in config");
if (!password.empty())
throwMultipleAuthenticationMethods("SSL certificate authentication", "authentication via password");
if (has_http_credentials)
@ -109,6 +119,8 @@ bool authenticateUserByHTTP(
checkUserNameNotEmpty(user, "X-ClickHouse HTTP headers");
/// It is prohibited to mix different authorization schemes.
if (has_config_credentials)
throwMultipleAuthenticationMethods("X-ClickHouse HTTP headers", "authentication set in config");
if (has_http_credentials)
throwMultipleAuthenticationMethods("X-ClickHouse HTTP headers", "Authorization HTTP header");
if (has_credentials_in_query_params)
@ -117,6 +129,8 @@ bool authenticateUserByHTTP(
else if (has_http_credentials)
{
/// It is prohibited to mix different authorization schemes.
if (has_config_credentials)
throwMultipleAuthenticationMethods("Authorization HTTP header", "authentication set in config");
if (has_credentials_in_query_params)
throwMultipleAuthenticationMethods("Authorization HTTP header", "authentication via parameters");
@ -190,6 +204,10 @@ bool authenticateUserByHTTP(
return false;
}
}
else if (has_config_credentials)
{
current_credentials = std::make_unique<BasicCredentials>(*config_credentials);
}
else // I.e., now using user name and password strings ("Basic").
{
if (!current_credentials)

View File

@ -11,13 +11,22 @@ class HTMLForm;
class HTTPServerResponse;
class Session;
class Credentials;
class BasicCredentials;
struct HTTPHandlerConnectionConfig;
/// Authenticates a user via HTTP protocol and initializes a session.
///
/// Usually retrieves the name and the password for that user from either the request's headers or from the query parameters.
/// Returns true when the user successfully authenticated,
/// the session instance will be configured accordingly, and the request_credentials instance will be dropped.
/// Returns false when the user is not authenticated yet, and the HTTP_UNAUTHORIZED response is sent with the "WWW-Authenticate" header,
/// in this case the `request_credentials` instance must be preserved until the next request or until any exception.
/// You can also pass user/password explicitly via `config_credentials`.
///
/// Returns true when the user successfully authenticated:
/// - the session instance will be configured accordingly
/// - and the request_credentials instance will be dropped.
///
/// Returns false when the user is not authenticated yet:
/// - the HTTP_UNAUTHORIZED response is sent with the "WWW-Authenticate" header
/// - the `request_credentials` instance must be preserved until the next request or until any exception.
///
/// Throws an exception if authentication failed.
bool authenticateUserByHTTP(
const HTTPServerRequest & request,
@ -25,6 +34,7 @@ bool authenticateUserByHTTP(
HTTPServerResponse & response,
Session & session,
std::unique_ptr<Credentials> & request_credentials,
const HTTPHandlerConnectionConfig & connection_config,
ContextPtr global_context,
LoggerPtr log);

View File

@ -1,6 +1,5 @@
#include <Server/HTTPHandler.h>
#include <Access/Credentials.h>
#include <Compression/CompressedReadBuffer.h>
#include <Compression/CompressedWriteBuffer.h>
#include <Core/ExternalTable.h>
@ -145,6 +144,15 @@ static std::chrono::steady_clock::duration parseSessionTimeout(
return std::chrono::seconds(session_timeout);
}
HTTPHandlerConnectionConfig::HTTPHandlerConnectionConfig(const Poco::Util::AbstractConfiguration & config, const std::string & config_prefix)
{
if (config.has(config_prefix + ".handler.user") || config.has(config_prefix + ".handler.password"))
{
credentials.emplace(
config.getString(config_prefix + ".handler.user", "default"),
config.getString(config_prefix + ".handler.password", ""));
}
}
void HTTPHandler::pushDelayedResults(Output & used_output)
{
@ -182,11 +190,12 @@ void HTTPHandler::pushDelayedResults(Output & used_output)
}
HTTPHandler::HTTPHandler(IServer & server_, const std::string & name, const HTTPResponseHeaderSetup & http_response_headers_override_)
HTTPHandler::HTTPHandler(IServer & server_, const HTTPHandlerConnectionConfig & connection_config_, const std::string & name, const HTTPResponseHeaderSetup & http_response_headers_override_)
: server(server_)
, log(getLogger(name))
, default_settings(server.context()->getSettingsRef())
, http_response_headers_override(http_response_headers_override_)
, connection_config(connection_config_)
{
server_display_name = server.config().getString("display_name", getFQDNOrHostName());
}
@ -199,7 +208,7 @@ HTTPHandler::~HTTPHandler() = default;
bool HTTPHandler::authenticateUser(HTTPServerRequest & request, HTMLForm & params, HTTPServerResponse & response)
{
return authenticateUserByHTTP(request, params, response, *session, request_credentials, server.context(), log);
return authenticateUserByHTTP(request, params, response, *session, request_credentials, connection_config, server.context(), log);
}
@ -768,8 +777,12 @@ void HTTPHandler::handleRequest(HTTPServerRequest & request, HTTPServerResponse
}
DynamicQueryHandler::DynamicQueryHandler(
IServer & server_, const std::string & param_name_, const HTTPResponseHeaderSetup & http_response_headers_override_)
: HTTPHandler(server_, "DynamicQueryHandler", http_response_headers_override_), param_name(param_name_)
IServer & server_,
const HTTPHandlerConnectionConfig & connection_config,
const std::string & param_name_,
const HTTPResponseHeaderSetup & http_response_headers_override_)
: HTTPHandler(server_, connection_config, "DynamicQueryHandler", http_response_headers_override_)
, param_name(param_name_)
{
}
@ -826,12 +839,13 @@ std::string DynamicQueryHandler::getQuery(HTTPServerRequest & request, HTMLForm
PredefinedQueryHandler::PredefinedQueryHandler(
IServer & server_,
const HTTPHandlerConnectionConfig & connection_config,
const NameSet & receive_params_,
const std::string & predefined_query_,
const CompiledRegexPtr & url_regex_,
const std::unordered_map<String, CompiledRegexPtr> & header_name_with_regex_,
const HTTPResponseHeaderSetup & http_response_headers_override_)
: HTTPHandler(server_, "PredefinedQueryHandler", http_response_headers_override_)
: HTTPHandler(server_, connection_config, "PredefinedQueryHandler", http_response_headers_override_)
, receive_params(receive_params_)
, predefined_query(predefined_query_)
, url_regex(url_regex_)
@ -923,10 +937,11 @@ HTTPRequestHandlerFactoryPtr createDynamicHandlerFactory(IServer & server,
{
auto query_param_name = config.getString(config_prefix + ".handler.query_param_name", "query");
HTTPHandlerConnectionConfig connection_config(config, config_prefix);
HTTPResponseHeaderSetup http_response_headers_override = parseHTTPResponseHeaders(config, config_prefix);
auto creator = [&server, query_param_name, http_response_headers_override]() -> std::unique_ptr<DynamicQueryHandler>
{ return std::make_unique<DynamicQueryHandler>(server, query_param_name, http_response_headers_override); };
auto creator = [&server, query_param_name, http_response_headers_override, connection_config]() -> std::unique_ptr<DynamicQueryHandler>
{ return std::make_unique<DynamicQueryHandler>(server, connection_config, query_param_name, http_response_headers_override); };
auto factory = std::make_shared<HandlingRuleHTTPHandlerFactory<DynamicQueryHandler>>(std::move(creator));
factory->addFiltersFromConfig(config, config_prefix);
@ -968,6 +983,8 @@ HTTPRequestHandlerFactoryPtr createPredefinedHandlerFactory(IServer & server,
Poco::Util::AbstractConfiguration::Keys headers_name;
config.keys(config_prefix + ".headers", headers_name);
HTTPHandlerConnectionConfig connection_config(config, config_prefix);
for (const auto & header_name : headers_name)
{
auto expression = config.getString(config_prefix + ".headers." + header_name);
@ -1001,12 +1018,18 @@ HTTPRequestHandlerFactoryPtr createPredefinedHandlerFactory(IServer & server,
predefined_query,
regex,
headers_name_with_regex,
http_response_headers_override]
http_response_headers_override,
connection_config]
-> std::unique_ptr<PredefinedQueryHandler>
{
return std::make_unique<PredefinedQueryHandler>(
server, analyze_receive_params, predefined_query, regex,
headers_name_with_regex, http_response_headers_override);
server,
connection_config,
analyze_receive_params,
predefined_query,
regex,
headers_name_with_regex,
http_response_headers_override);
};
factory = std::make_shared<HandlingRuleHTTPHandlerFactory<PredefinedQueryHandler>>(std::move(creator));
factory->addFiltersFromConfig(config, config_prefix);
@ -1019,18 +1042,21 @@ HTTPRequestHandlerFactoryPtr createPredefinedHandlerFactory(IServer & server,
analyze_receive_params,
predefined_query,
headers_name_with_regex,
http_response_headers_override]
http_response_headers_override,
connection_config]
-> std::unique_ptr<PredefinedQueryHandler>
{
return std::make_unique<PredefinedQueryHandler>(
server, analyze_receive_params, predefined_query, CompiledRegexPtr{},
headers_name_with_regex, http_response_headers_override);
server,
connection_config,
analyze_receive_params,
predefined_query,
CompiledRegexPtr{},
headers_name_with_regex,
http_response_headers_override);
};
factory = std::make_shared<HandlingRuleHTTPHandlerFactory<PredefinedQueryHandler>>(std::move(creator));
factory->addFiltersFromConfig(config, config_prefix);
return factory;
}

View File

@ -12,6 +12,7 @@
#include <IO/CascadeWriteBuffer.h>
#include <Compression/CompressedWriteBuffer.h>
#include <Common/re2.h>
#include <Access/Credentials.h>
#include "HTTPResponseHeaderWriter.h"
@ -26,17 +27,28 @@ namespace DB
{
class Session;
class Credentials;
class IServer;
struct Settings;
class WriteBufferFromHTTPServerResponse;
using CompiledRegexPtr = std::shared_ptr<const re2::RE2>;
struct HTTPHandlerConnectionConfig
{
std::optional<BasicCredentials> credentials;
/// TODO:
/// String quota;
/// String default_database;
HTTPHandlerConnectionConfig() = default;
HTTPHandlerConnectionConfig(const Poco::Util::AbstractConfiguration & config, const std::string & config_prefix);
};
class HTTPHandler : public HTTPRequestHandler
{
public:
HTTPHandler(IServer & server_, const std::string & name, const HTTPResponseHeaderSetup & http_response_headers_override_);
HTTPHandler(IServer & server_, const HTTPHandlerConnectionConfig & connection_config_, const std::string & name, const HTTPResponseHeaderSetup & http_response_headers_override_);
~HTTPHandler() override;
void handleRequest(HTTPServerRequest & request, HTTPServerResponse & response, const ProfileEvents::Event & write_event) override;
@ -146,16 +158,7 @@ private:
// The request_credential instance may outlive a single request/response loop.
// This happens only when the authentication mechanism requires more than a single request/response exchange (e.g., SPNEGO).
std::unique_ptr<Credentials> request_credentials;
// Returns true when the user successfully authenticated,
// the session instance will be configured accordingly, and the request_credentials instance will be dropped.
// Returns false when the user is not authenticated yet, and the 'Negotiate' response is sent,
// the session and request_credentials instances are preserved.
// Throws an exception if authentication failed.
bool authenticateUser(
HTTPServerRequest & request,
HTMLForm & params,
HTTPServerResponse & response);
HTTPHandlerConnectionConfig connection_config;
/// Also initializes 'used_output'.
void processQuery(
@ -174,6 +177,13 @@ private:
Output & used_output);
static void pushDelayedResults(Output & used_output);
protected:
// @see authenticateUserByHTTP()
virtual bool authenticateUser(
HTTPServerRequest & request,
HTMLForm & params,
HTTPServerResponse & response);
};
class DynamicQueryHandler : public HTTPHandler
@ -184,6 +194,7 @@ private:
public:
explicit DynamicQueryHandler(
IServer & server_,
const HTTPHandlerConnectionConfig & connection_config,
const std::string & param_name_ = "query",
const HTTPResponseHeaderSetup & http_response_headers_override_ = std::nullopt);
@ -203,6 +214,7 @@ private:
public:
PredefinedQueryHandler(
IServer & server_,
const HTTPHandlerConnectionConfig & connection_config,
const NameSet & receive_params_,
const std::string & predefined_query_,
const CompiledRegexPtr & url_regex_,

View File

@ -275,7 +275,7 @@ void addDefaultHandlersFactory(
auto dynamic_creator = [&server] () -> std::unique_ptr<DynamicQueryHandler>
{
return std::make_unique<DynamicQueryHandler>(server, "query");
return std::make_unique<DynamicQueryHandler>(server, HTTPHandlerConnectionConfig{}, "query");
};
auto query_handler = std::make_shared<HandlingRuleHTTPHandlerFactory<DynamicQueryHandler>>(std::move(dynamic_creator));
query_handler->addFilter([](const auto & request)

View File

@ -7,6 +7,7 @@
#include <Server/HTTP/sendExceptionToHTTPClient.h>
#include <Server/IServer.h>
#include <Server/PrometheusMetricsWriter.h>
#include <Server/HTTPHandler.h>
#include "config.h"
#include <Access/Credentials.h>
@ -137,7 +138,7 @@ protected:
bool authenticateUser(HTTPServerRequest & request, HTTPServerResponse & response)
{
return authenticateUserByHTTP(request, *params, response, *session, request_credentials, server().context(), log());
return authenticateUserByHTTP(request, *params, response, *session, request_credentials, HTTPHandlerConnectionConfig{}, server().context(), log());
}
void makeContext(HTTPServerRequest & request)

View File

@ -17,9 +17,10 @@ class SimpleCluster:
cluster.start()
def add_instance(self, name, config_dir):
script_path = os.path.dirname(os.path.realpath(__file__))
return self.cluster.add_instance(
name, main_configs=[os.path.join(script_path, config_dir, "config.xml")]
name,
main_configs=[os.path.join(config_dir, "config.xml")],
user_configs=["users.d/users.yaml"],
)
@ -96,6 +97,16 @@ def test_dynamic_query_handler():
== res_custom_ct.headers["X-Test-Http-Response-Headers-Even-Multiple"]
)
assert cluster.instance.http_request(
"test_dynamic_handler_auth_with_password?query=select+currentUser()"
).content, "with_password"
assert cluster.instance.http_request(
"test_dynamic_handler_auth_with_password_fail?query=select+currentUser()"
).status_code, 403
assert cluster.instance.http_request(
"test_dynamic_handler_auth_without_password?query=select+currentUser()"
).content, "without_password"
def test_predefined_query_handler():
with contextlib.closing(
@ -177,6 +188,16 @@ def test_predefined_query_handler():
)
assert b"max_threads\t1\n" == res1.content
assert cluster.instance.http_request(
"test_predefined_handler_auth_with_password"
).content, "with_password"
assert cluster.instance.http_request(
"test_predefined_handler_auth_with_password_fail"
).status_code, 403
assert cluster.instance.http_request(
"test_predefined_handler_auth_without_password"
).content, "without_password"
def test_fixed_static_handler():
with contextlib.closing(

View File

@ -24,5 +24,32 @@
</http_response_headers>
</handler>
</rule>
<rule>
<methods>GET</methods>
<url>/test_dynamic_handler_auth_with_password</url>
<handler>
<type>dynamic_query_handler</type>
<user>with_password</user>
<password>password</password>
</handler>
</rule>
<rule>
<methods>GET</methods>
<url>/test_dynamic_handler_auth_with_password_fail</url>
<handler>
<type>dynamic_query_handler</type>
<user>with_password</user>
<!-- No password - authentication should fail -->
</handler>
</rule>
<rule>
<methods>GET</methods>
<url>/test_dynamic_handler_auth_without_password</url>
<handler>
<type>dynamic_query_handler</type>
<user>without_password</user>
</handler>
</rule>
</http_handlers>
</clickhouse>

View File

@ -33,5 +33,35 @@
<query>INSERT INTO test_table(id, data) SELECT {id:UInt32}, {_request_body:String}</query>
</handler>
</rule>
<rule>
<methods>GET</methods>
<url>/test_predefined_handler_auth_with_password</url>
<handler>
<type>predefined_query_handler</type>
<user>with_password</user>
<password>password</password>
<query>SELECT currentUser()</query>
</handler>
</rule>
<rule>
<methods>GET</methods>
<url>/test_predefined_handler_auth_with_password_fail</url>
<handler>
<type>predefined_query_handler</type>
<user>with_password</user>
<!-- No password - authentication should fail -->
<query>SELECT currentUser()</query>
</handler>
</rule>
<rule>
<methods>GET</methods>
<url>/test_predefined_handler_auth_without_password</url>
<handler>
<type>predefined_query_handler</type>
<user>without_password</user>
<query>SELECT currentUser()</query>
</handler>
</rule>
</http_handlers>
</clickhouse>

View File

@ -0,0 +1,7 @@
users:
with_password:
profile: default
password: password
without_password:
profile: default
no_password: 1