-
Notifications
You must be signed in to change notification settings - Fork 9k
Expand file tree
/
Copy pathauthenticateUserByHTTP.cpp
More file actions
420 lines (368 loc) · 20 KB
/
Copy pathauthenticateUserByHTTP.cpp
File metadata and controls
420 lines (368 loc) · 20 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
#include <Access/Authentication.h>
#include <Access/Credentials.h>
#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>
#include <Core/ServerSettings.h>
#include <Interpreters/Context.h>
#include <Interpreters/Session.h>
#include <Poco/Net/HTTPBasicCredentials.h>
#include <optional>
#if USE_SSL
# include <Common/Crypto/X509Certificate.h>
#endif
namespace DB
{
namespace ErrorCodes
{
extern const int AUTHENTICATION_FAILED;
extern const int BAD_ARGUMENTS;
extern const int INCORRECT_DATA;
extern const int SUPPORT_IS_DISABLED;
}
namespace ServerSetting
{
extern const ServerSettingsString default_session_user;
}
namespace
{
/// Throws an exception that multiple authorization schemes are used simultaneously.
[[noreturn]] void throwMultipleAuthenticationMethods(std::string_view method1, std::string_view method2)
{
throw Exception(ErrorCodes::AUTHENTICATION_FAILED,
"Invalid authentication: it is not allowed to use {} and {} simultaneously", method1, method2);
}
/// Checks that a specified user name is not empty, and throws an exception if it's empty.
/// An empty user name means that the request carried no user name and the default session
/// user is configured to be empty (i.e. requests without a user name are prohibited), so the
/// reject is recorded in `system.session_log` as a login failure to keep it auditable.
void checkUserNameNotEmptyAndServerHasEnoughMemory(
const String & user_name,
std::string_view method,
const ContextPtr & context,
Session & session,
const Poco::Net::SocketAddress & address)
{
/// The empty-user reject must come before the memory check: the request is rejected
/// either way, and rejecting it as an anonymous login keeps the `LoginFailure` row in
/// `system.session_log` that the `default_session_user` setting promises for every
/// prohibited anonymous connection, even when the server is over the memory limit.
if (user_name.empty())
{
auto exception = Exception(ErrorCodes::AUTHENTICATION_FAILED, "Got an empty user name from {}", method);
session.onAuthenticationFailure(user_name, address, exception);
throw exception; /// NOLINT
}
auto users_to_ignore_early_memory_limit_check = context->getUsersToIgnoreEarlyMemoryLimitCheck();
if (!(users_to_ignore_early_memory_limit_check && users_to_ignore_early_memory_limit_check->contains(user_name)))
{
LOG_TEST(getLogger("authenticateUserByHTTP"), "Checking memory limit for user: {}", user_name);
CurrentMemoryTracker::check();
}
else
LOG_TEST(getLogger("authenticateUserByHTTP"), "Skipping memory limit check for user: {}", user_name);
}
}
bool authenticateUserByHTTP(
const HTTPServerRequest & request,
const HTMLForm & params,
HTTPServerResponse & response,
Session & session,
std::unique_ptr<Credentials> & request_credentials,
const HTTPHandlerConnectionConfig & connection_config,
ContextPtr global_context,
LoggerPtr log);
bool authenticateUserByHTTP(
const HTTPServerRequest & request,
const HTMLForm & params,
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 name assumed when the client passed an empty user name (or none at all):
/// the `default_session_user` server setting, possibly overridden for this handler
/// (composable protocols allow a per-endpoint default user). It can be explicitly
/// configured to be empty to prohibit requests without a user name. Interserver HTTP
/// connections do not pass through this function (see `InterserverIOHTTPHandler`),
/// so the default session user is never applied to them.
const String default_session_user = connection_config.default_session_user
? *connection_config.default_session_user
: String(global_context->getServerSettings()[ServerSetting::default_session_user]);
/// The user and password can be passed by headers (similar to X-Auth-*),
/// which is used by load balancers to pass authentication information.
std::string user = request.get("X-ClickHouse-User", "");
std::string password = request.get("X-ClickHouse-Key", "");
std::string quota_key = request.get("X-ClickHouse-Quota", "");
bool has_auth_headers = !user.empty() || !password.empty();
/// 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).
bool has_credentials_in_query_params = params.has("user") || params.has("password");
/// Whether the request carries an `Authorization` header that should be treated as
/// credentials. The sentinel value `never` (which `play.html` sets on the requests it can
/// add headers to) disables it.
bool has_authorization_header = request.hasCredentials() && request.get("Authorization") != "never";
/// Credentials passed in the URL query parameters take precedence over the HTTP
/// `Authorization` header: when both are present, the header is ignored instead of
/// rejecting the request for mixing authentication methods.
///
/// This is needed because once a browser has remembered HTTP Basic credentials for an
/// origin, it attaches the `Authorization` header to every subsequent request to that
/// origin automatically - including requests that the application has no way to add or
/// remove headers from, such as a form submission or a download navigation. The Web UI
/// (`play.html`) authenticates by putting the user name and password into the URL query
/// parameters, so without this precedence such a request would carry both the remembered
/// header and the parameters and be rejected. (The special value `Authorization: never`
/// also suppresses the header, but it can only be set from a scripted request such as
/// `fetch` or `XHR`, not from a plain navigation.)
///
/// This precedence applies only to the default authentication path. When the handler has
/// its own configured credentials, an `Authorization` header is still rejected as a mix of
/// authentication methods, regardless of the query parameters (see below).
bool has_http_credentials = has_authorization_header && !has_credentials_in_query_params;
std::string spnego_challenge;
#if USE_SSL
X509Certificate::Subjects certificate_subjects;
/// Capture the TLS client certificate (if the client presented one) regardless of the selected
/// authentication method, so that session_log records it even when the connection authenticates
/// by another method (headers, basic, query parameters, config) or the login fails.
/// Mirrors the native protocol path in TCPHandler::receiveHello.
std::optional<X509Certificate> peer_certificate;
if (request.havePeerCertificate())
{
peer_certificate = request.peerCertificate();
session.setClientCertificate(*peer_certificate);
}
#endif
/// Client info and its effective address must be ready before the early empty-user
/// rejection below, so its session-log record has the same address as regular HTTP
/// authentication failures.
session.setHTTPClientInfo(request);
const auto & client_info = session.getClientInfo();
auto forwarded_address = client_info.getLastForwardedFor();
const bool use_forwarded_address = global_context->getConfigRef().getBool("auth_use_forwarded_address", false);
if (use_forwarded_address && !client_info.forwarded_for.empty() && !forwarded_address)
throw Exception(
ErrorCodes::INCORRECT_DATA,
"Invalid address in `X-Forwarded-For` HTTP header: expected an IP literal with an optional numeric port");
const auto client_address = forwarded_address && use_forwarded_address
? *forwarded_address
: request.clientAddress();
if (config_credentials)
{
checkUserNameNotEmptyAndServerHasEnoughMemory(
config_credentials->getUserName(), "config authentication", global_context, session, client_address);
}
if (has_ssl_certificate_auth)
{
#if USE_SSL
/// It is prohibited to mix different authorization schemes. The mix is rejected before
/// the empty user name is resolved through the default session user: a handler with
/// fixed credentials ignores the setting, so a stray incomplete authentication header
/// must produce the mixed-authentication error, not an anonymous-login reject.
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)
throwMultipleAuthenticationMethods("SSL certificate authentication", "Authorization HTTP header");
if (has_credentials_in_query_params)
throwMultipleAuthenticationMethods("SSL certificate authentication", "authentication via parameters");
/// For SSL certificate authentication we extract the user name from the "X-ClickHouse-User" HTTP header.
/// If the header is not set (or empty), the certificate must authenticate the default session user.
if (user.empty())
user = default_session_user;
checkUserNameNotEmptyAndServerHasEnoughMemory(user, "X-ClickHouse HTTP headers", global_context, session, client_address);
if (peer_certificate)
certificate_subjects = peer_certificate->extractAllSubjects();
if (certificate_subjects.empty())
throw Exception(ErrorCodes::AUTHENTICATION_FAILED,
"Invalid authentication: SSL certificate authentication requires nonempty certificate's Common Name or Subject Alternative Name");
#else
UNUSED(log);
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED,
"SSL certificate authentication disabled because ClickHouse was built without SSL library");
#endif
}
else if (has_auth_headers)
{
/// It is prohibited to mix different authorization schemes. The mix is rejected before
/// the empty user name is resolved through the default session user (see above).
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)
throwMultipleAuthenticationMethods("X-ClickHouse HTTP headers", "authentication via parameters");
/// The client passed "X-ClickHouse-Key" without "X-ClickHouse-User" (or with an empty one):
/// the password is checked against the default session user.
if (user.empty())
user = default_session_user;
checkUserNameNotEmptyAndServerHasEnoughMemory(user, "X-ClickHouse HTTP headers", global_context, session, client_address);
}
else if (has_http_credentials)
{
/// It is prohibited to mix different authorization schemes.
/// (Authentication via query parameters takes precedence over the `Authorization`
/// header and is handled above by excluding it from `has_http_credentials`.)
if (has_config_credentials)
throwMultipleAuthenticationMethods("Authorization HTTP header", "authentication set in config");
std::string scheme;
std::string auth_info;
request.getCredentials(scheme, auth_info);
if (Poco::icompare(scheme, "Basic") == 0)
{
Poco::Net::HTTPBasicCredentials credentials(auth_info);
user = credentials.getUsername();
password = credentials.getPassword();
/// An empty user name in Basic credentials means the default session user.
if (user.empty())
user = default_session_user;
checkUserNameNotEmptyAndServerHasEnoughMemory(user, "Authorization HTTP header", global_context, session, client_address);
}
else if (Poco::icompare(scheme, "Negotiate") == 0)
{
spnego_challenge = auth_info;
if (spnego_challenge.empty())
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Invalid authentication: SPNEGO challenge is empty");
}
else
{
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Invalid authentication: '{}' HTTP Authorization scheme is not supported", scheme);
}
}
else
{
/// Authentication via the URL query parameters (or, if absent, the default session user).
/// The query parameters take precedence over the `Authorization` header (which was
/// excluded from `has_http_credentials` above), but mixing the header with credentials
/// configured for the handler is still rejected, as for every other method.
if (has_config_credentials && has_authorization_header)
throwMultipleAuthenticationMethods("Authorization HTTP header", "authentication set in config");
user = params.get("user", "");
password = params.get("password", "");
/// When the handler has credentials configured (`handler.user` of an `http_handlers`
/// rule or `user` of a `prometheus` protocol), they are applied below regardless of
/// the request, so an absent user name is not resolved through the default session
/// user: an empty `default_session_user` (which prohibits requests without a user
/// name) must not reject handlers with a fixed user. The configured user name has
/// already been checked above.
if (!has_config_credentials)
{
/// If the user name is not set (or set to an empty string), the default session user is assumed.
if (user.empty())
user = default_session_user;
checkUserNameNotEmptyAndServerHasEnoughMemory(user, "authentication via parameters", global_context, session, client_address);
}
}
if (has_config_credentials)
{
current_credentials = std::make_unique<AlwaysAllowCredentials>(*config_credentials);
}
#if USE_SSL
else if (!certificate_subjects.empty())
{
chassert(!user.empty());
if (!current_credentials)
current_credentials = std::make_unique<SSLCertificateCredentials>(user, std::move(certificate_subjects));
auto * certificate_credentials = dynamic_cast<SSLCertificateCredentials *>(current_credentials.get());
if (!certificate_credentials)
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Invalid authentication: expected SSL certificate authorization scheme");
}
else if (!spnego_challenge.empty())
{
if (!current_credentials)
current_credentials = global_context->makeGSSAcceptorContext();
auto * gss_acceptor_context = dynamic_cast<GSSAcceptorContext *>(current_credentials.get());
if (!gss_acceptor_context)
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Invalid authentication: unexpected 'Negotiate' HTTP Authorization scheme expected");
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
const auto spnego_response = base64Encode(gss_acceptor_context->processToken(base64Decode(spnego_challenge), log));
#pragma clang diagnostic pop
if (!spnego_response.empty())
response.set("WWW-Authenticate", "Negotiate " + spnego_response);
if (!gss_acceptor_context->isFailed() && !gss_acceptor_context->isReady())
{
if (spnego_response.empty())
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Invalid authentication: 'Negotiate' HTTP Authorization failure");
response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED);
response.send();
/// Keep the credentials for next HTTP request. A client can handle HTTP_UNAUTHORIZED and send us more credentials with the next HTTP request.
request_credentials = std::move(current_credentials);
return false;
}
}
#endif
else // I.e., now using user name and password strings ("Basic").
{
if (!current_credentials)
current_credentials = std::make_unique<BasicCredentials>();
auto * basic_credentials = dynamic_cast<BasicCredentials *>(current_credentials.get());
if (!basic_credentials)
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Invalid authentication: expected 'Basic' HTTP Authorization scheme");
if (request.get("Authorization", "") != "never")
basic_credentials->enableInteractiveBasicAuthenticationInTheBrowser();
chassert(!user.empty());
basic_credentials->setUserName(user);
basic_credentials->setPassword(password);
}
if (params.has("quota_key"))
{
if (!quota_key.empty())
throw Exception(ErrorCodes::BAD_ARGUMENTS,
"Invalid authentication: it is not allowed "
"to use quota key as HTTP header and as parameter simultaneously");
quota_key = params.get("quota_key");
}
/// Client info is already set above; the quota key is used for accounting parameters in `setUser`.
session.setQuotaClientKey(quota_key);
try
{
if (forwarded_address && use_forwarded_address)
session.authenticate(*current_credentials, *forwarded_address, request.clientAddress());
else
session.authenticate(*current_credentials, request.clientAddress());
}
catch (const Authentication::Require<BasicCredentials> & required_credentials)
{
current_credentials = std::make_unique<BasicCredentials>();
if (required_credentials.getRealm().empty())
response.set("WWW-Authenticate", "Basic");
else
response.set("WWW-Authenticate", "Basic realm=\"" + required_credentials.getRealm() + "\"");
response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED);
response.send();
/// Keep the credentials for next HTTP request. A client can handle HTTP_UNAUTHORIZED and send us more credentials with the next HTTP request.
request_credentials = std::move(current_credentials);
return false;
}
catch (const Authentication::Require<GSSAcceptorContext> & required_credentials)
{
current_credentials = global_context->makeGSSAcceptorContext();
if (required_credentials.getRealm().empty())
response.set("WWW-Authenticate", "Negotiate");
else
response.set("WWW-Authenticate", "Negotiate realm=\"" + required_credentials.getRealm() + "\"");
response.setStatusAndReason(HTTPResponse::HTTP_UNAUTHORIZED);
response.send();
/// Keep the credentials for next HTTP request. A client can handle HTTP_UNAUTHORIZED and send us more credentials with the next HTTP request.
request_credentials = std::move(current_credentials);
return false;
}
return true;
}
}