Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add external HTTP Basic authenticator #55199

Merged
merged 8 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
91 changes: 91 additions & 0 deletions docs/en/operations/external-authenticators/http.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
slug: /en/operations/external-authenticators/http
title: "HTTP"
---
import SelfManaged from '@site/docs/en/_snippets/_self_managed_only_no_roadmap.md';

<SelfManaged />

HTTP server can be used to authenticate ClickHouse users. HTTP authentication can only be used as an external authenticator for existing users, which are defined in `users.xml` or in local access control paths. Currently, [Basic](https://datatracker.ietf.org/doc/html/rfc7617) authentication scheme using GET method is supported.

## HTTP authentication server definition {#http-auth-server-definition}

To define HTTP authentication server you must add `http_authentication_servers` section to the `config.xml`.

**Example**
```xml
<clickhouse>
<!- ... -->
<http_authentication_servers>
<basic_auth_server>
<uri>http://localhost:8000/auth</uri>
<connection_timeout_ms>1000</connection_timeout_ms>
<receive_timeout_ms>1000</receive_timeout_ms>
<send_timeout_ms>1000</send_timeout_ms>
<max_tries>3</max_tries>
<retry_initial_backoff_ms>50</retry_initial_backoff_ms>
<retry_max_backoff_ms>1000</retry_max_backoff_ms>
</basic_auth_server>
</http_authentication_servers>
</clickhouse>

```

Note, that you can define multiple HTTP servers inside the `http_authentication_servers` section using distinct names.

**Parameters**
- `uri` - URI for making authentication request

Timeouts in milliseconds on the socket used for communicating with the server:
- `connection_timeout_ms` - Default: 1000 ms.
- `receive_timeout_ms` - Default: 1000 ms.
- `send_timeout_ms` - Default: 1000 ms.

Retry parameters:
- `max_tries` - The maximum number of attempts to make an authentication request. Default: 3
- `retry_initial_backoff_ms` - The backoff initial interval on retry. Default: 50 ms
- `retry_max_backoff_ms` - The maximum backoff interval. Default: 1000 ms

### Enabling HTTP authentication in `users.xml` {#enabling-http-auth-in-users-xml}

In order to enable HTTP authentication for the user, specify `http_authentication` section instead of `password` or similar sections in the user definition.

Parameters:
- `server` - Name of the HTTP authentication server configured in the main `config.xml` file as described previously.
- `scheme` - HTTP authentication scheme. `Basic` is only supported now. Default: Basic

Example (goes into `users.xml`):
```xml
<clickhouse>
<!- ... -->
<my_user>
<!- ... -->
<http_authentication>
<server>basic_server</server>
<scheme>basic</scheme>
</http_authentication>
</test_user_2>
</clickhouse>
```

:::note
Note that HTTP authentication cannot be used alongside with any other authentication mechanism. The presence of any other sections like `password` alongside `http_authentication` will force ClickHouse to shutdown.
:::

### Enabling HTTP authentication using SQL {#enabling-http-auth-using-sql}

When [SQL-driven Access Control and Account Management](/docs/en/guides/sre/user-management/index.md#access-control) is enabled in ClickHouse, users identified by HTTP authentication can also be created using SQL statements.

```sql
CREATE USER my_user IDENTIFIED WITH HTTP SERVER 'basic_server' SCHEME 'Basic'
```

...or, `Basic` is default without explicit scheme definition

```sql
CREATE USER my_user IDENTIFIED WITH HTTP SERVER 'basic_server'
```

### Passing session settings {#passing-session-settings}

If a response body from HTTP authentication server has JSON format and contains `settings` sub-object, ClickHouse will try parse its key: value pairs as string values and set them as session settings for authenticated user's current session. If parsing is failed, a response body from server will be ignored.
1 change: 1 addition & 0 deletions docs/en/operations/external-authenticators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ The following external authenticators and directories are supported:
- [LDAP](./ldap.md#external-authenticators-ldap) [Authenticator](./ldap.md#ldap-external-authenticator) and [Directory](./ldap.md#ldap-external-user-directory)
- Kerberos [Authenticator](./kerberos.md#external-authenticators-kerberos)
- [SSL X.509 authentication](./ssl-x509.md#ssl-external-authentication)
- HTTP [Authenticator](./http.md)
3 changes: 2 additions & 1 deletion docs/en/sql-reference/statements/create/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Syntax:
``` sql
CREATE USER [IF NOT EXISTS | OR REPLACE] name1 [ON CLUSTER cluster_name1]
[, name2 [ON CLUSTER cluster_name2] ...]
[NOT IDENTIFIED | IDENTIFIED {[WITH {no_password | plaintext_password | sha256_password | sha256_hash | double_sha1_password | double_sha1_hash}] BY {'password' | 'hash'}} | {WITH ldap SERVER 'server_name'} | {WITH kerberos [REALM 'realm']} | {WITH ssl_certificate CN 'common_name'} | {WITH ssh_key BY KEY 'public_key' TYPE 'ssh-rsa|...'}]
[NOT IDENTIFIED | IDENTIFIED {[WITH {no_password | plaintext_password | sha256_password | sha256_hash | double_sha1_password | double_sha1_hash}] BY {'password' | 'hash'}} | {WITH ldap SERVER 'server_name'} | {WITH kerberos [REALM 'realm']} | {WITH ssl_certificate CN 'common_name'} | {WITH ssh_key BY KEY 'public_key' TYPE 'ssh-rsa|...'} | {WITH http SERVER 'server_name' [SCHEME 'Basic']}]
[HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
[VALID UNTIL datetime]
[IN access_storage_type]
Expand Down Expand Up @@ -40,6 +40,7 @@ There are multiple ways of user identification:
- `IDENTIFIED WITH kerberos` or `IDENTIFIED WITH kerberos REALM 'realm'`
- `IDENTIFIED WITH ssl_certificate CN 'mysite.com:user'`
- `IDENTIFIED WITH ssh_key BY KEY 'public_key' TYPE 'ssh-rsa', KEY 'another_public_key' TYPE 'ssh-ed25519'`
- `IDENTIFIED WITH http SERVER 'http_server'` or `IDENTIFIED WITH http SERVER 'http_server' SCHEME 'basic'`
- `IDENTIFIED BY 'qwerty'`

Password complexity requirements can be edited in [config.xml](/docs/en/operations/configuration-files). Below is an example configuration that requires passwords to be at least 12 characters long and contain 1 number. Each password complexity rule requires a regex to match against passwords and a description of the rule.
Expand Down
2 changes: 1 addition & 1 deletion src/Access/AccessControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ AccessChangesNotifier & AccessControl::getChangesNotifier()
}


UUID AccessControl::authenticate(const Credentials & credentials, const Poco::Net::IPAddress & address) const
AuthResult AccessControl::authenticate(const Credentials & credentials, const Poco::Net::IPAddress & address) const
{
try
{
Expand Down
2 changes: 1 addition & 1 deletion src/Access/AccessControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class AccessControl : public MultipleAccessStorage
scope_guard subscribeForChanges(const UUID & id, const OnChangedHandler & handler) const;
scope_guard subscribeForChanges(const std::vector<UUID> & ids, const OnChangedHandler & handler) const;

UUID authenticate(const Credentials & credentials, const Poco::Net::IPAddress & address) const;
AuthResult authenticate(const Credentials & credentials, const Poco::Net::IPAddress & address) const;

/// Makes a backup of access entities.
void restoreFromBackup(RestorerFromBackup & restorer) override;
Expand Down
18 changes: 17 additions & 1 deletion src/Access/Authentication.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ namespace
}


bool Authentication::areCredentialsValid(const Credentials & credentials, const AuthenticationData & auth_data, const ExternalAuthenticators & external_authenticators)
bool Authentication::areCredentialsValid(
const Credentials & credentials,
const AuthenticationData & auth_data,
const ExternalAuthenticators & external_authenticators,
SettingsChanges & settings)
{
if (!credentials.isReady())
return false;
Expand All @@ -100,6 +104,7 @@ bool Authentication::areCredentialsValid(const Credentials & credentials, const
case AuthenticationType::DOUBLE_SHA1_PASSWORD:
case AuthenticationType::BCRYPT_PASSWORD:
case AuthenticationType::LDAP:
case AuthenticationType::HTTP:
throw Authentication::Require<BasicCredentials>("ClickHouse Basic Authentication");

case AuthenticationType::KERBEROS:
Expand Down Expand Up @@ -133,6 +138,7 @@ bool Authentication::areCredentialsValid(const Credentials & credentials, const
case AuthenticationType::BCRYPT_PASSWORD:
case AuthenticationType::LDAP:
case AuthenticationType::KERBEROS:
case AuthenticationType::HTTP:
throw Authentication::Require<BasicCredentials>("ClickHouse Basic Authentication");

case AuthenticationType::SSL_CERTIFICATE:
Expand Down Expand Up @@ -177,6 +183,14 @@ bool Authentication::areCredentialsValid(const Credentials & credentials, const
case AuthenticationType::BCRYPT_PASSWORD:
return checkPasswordBcrypt(basic_credentials->getPassword(), auth_data.getPasswordHashBinary());

case AuthenticationType::HTTP:
switch (auth_data.getHTTPAuthenticationScheme())
{
case HTTPAuthenticationScheme::BASIC:
return external_authenticators.checkHTTPBasicCredentials(
auth_data.getHTTPAuthenticationServerName(), *basic_credentials, settings);
}

case AuthenticationType::MAX:
break;
}
Expand All @@ -192,6 +206,7 @@ bool Authentication::areCredentialsValid(const Credentials & credentials, const
case AuthenticationType::DOUBLE_SHA1_PASSWORD:
case AuthenticationType::BCRYPT_PASSWORD:
case AuthenticationType::LDAP:
case AuthenticationType::HTTP:
throw Authentication::Require<BasicCredentials>("ClickHouse Basic Authentication");

case AuthenticationType::KERBEROS:
Expand All @@ -218,6 +233,7 @@ bool Authentication::areCredentialsValid(const Credentials & credentials, const
case AuthenticationType::DOUBLE_SHA1_PASSWORD:
case AuthenticationType::BCRYPT_PASSWORD:
case AuthenticationType::LDAP:
case AuthenticationType::HTTP:
throw Authentication::Require<BasicCredentials>("ClickHouse Basic Authentication");

case AuthenticationType::KERBEROS:
Expand Down
9 changes: 8 additions & 1 deletion src/Access/Authentication.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ namespace ErrorCodes

class Credentials;
class ExternalAuthenticators;
class SettingsChanges;

/// TODO: Try to move this checking to Credentials.
struct Authentication
{
/// Checks the credentials (passwords, readiness, etc.)
static bool areCredentialsValid(const Credentials & credentials, const AuthenticationData & auth_data, const ExternalAuthenticators & external_authenticators);
/// If necessary, makes a request to external authenticators and fills in the session settings if they were
/// returned by the authentication server
static bool areCredentialsValid(
const Credentials & credentials,
const AuthenticationData & auth_data,
const ExternalAuthenticators & external_authenticators,
SettingsChanges & settings);

// A signaling class used to communicate requirements for credentials.
template <typename CredentialsType>
Expand Down
22 changes: 21 additions & 1 deletion src/Access/AuthenticationData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ bool operator ==(const AuthenticationData & lhs, const AuthenticationData & rhs)
return (lhs.type == rhs.type) && (lhs.password_hash == rhs.password_hash)
&& (lhs.ldap_server_name == rhs.ldap_server_name) && (lhs.kerberos_realm == rhs.kerberos_realm)
&& (lhs.ssl_certificate_common_names == rhs.ssl_certificate_common_names)
&& (lhs.ssh_keys == rhs.ssh_keys);
&& (lhs.ssh_keys == rhs.ssh_keys) && (lhs.http_auth_scheme == rhs.http_auth_scheme)
&& (lhs.http_auth_server_name == rhs.http_auth_server_name);
}


Expand All @@ -128,6 +129,7 @@ void AuthenticationData::setPassword(const String & password_)
case AuthenticationType::KERBEROS:
case AuthenticationType::SSL_CERTIFICATE:
case AuthenticationType::SSH_KEY:
case AuthenticationType::HTTP:
throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot specify password for authentication type {}", toString(type));

case AuthenticationType::MAX:
Expand Down Expand Up @@ -232,6 +234,7 @@ void AuthenticationData::setPasswordHashBinary(const Digest & hash)
case AuthenticationType::KERBEROS:
case AuthenticationType::SSL_CERTIFICATE:
case AuthenticationType::SSH_KEY:
case AuthenticationType::HTTP:
throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot specify password binary hash for authentication type {}", toString(type));

case AuthenticationType::MAX:
Expand Down Expand Up @@ -326,6 +329,12 @@ std::shared_ptr<ASTAuthenticationData> AuthenticationData::toAST() const
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "SSH is disabled, because ClickHouse is built without OpenSSL");
#endif
}
case AuthenticationType::HTTP:
{
node->children.push_back(std::make_shared<ASTLiteral>(getHTTPAuthenticationServerName()));
node->children.push_back(std::make_shared<ASTLiteral>(toString(getHTTPAuthenticationScheme())));
break;
}

case AuthenticationType::NO_PASSWORD: [[fallthrough]];
case AuthenticationType::MAX:
Expand Down Expand Up @@ -484,6 +493,17 @@ AuthenticationData AuthenticationData::fromAST(const ASTAuthenticationData & que

auth_data.setSSLCertificateCommonNames(std::move(common_names));
}
else if (query.type == AuthenticationType::HTTP)
{
String server = checkAndGetLiteralArgument<String>(args[0], "http_auth_server_name");
auto scheme = HTTPAuthenticationScheme::BASIC; // Default scheme

if (args_size > 1)
scheme = parseHTTPAuthenticationScheme(checkAndGetLiteralArgument<String>(args[1], "scheme"));

auth_data.setHTTPAuthenticationServerName(server);
auth_data.setHTTPAuthenticationScheme(scheme);
}
else
{
throw Exception(ErrorCodes::LOGICAL_ERROR, "Unexpected ASTAuthenticationData structure");
Expand Down
16 changes: 13 additions & 3 deletions src/Access/AuthenticationData.h
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
#pragma once

#include <Access/Common/AuthenticationType.h>
#include <Common/SSH/Wrappers.h>
#include <Parsers/Access/ASTAuthenticationData.h>
#include <Access/Common/HTTPAuthenticationScheme.h>
#include <Interpreters/Context_fwd.h>
#include <Parsers/Access/ASTAuthenticationData.h>
#include <Common/SSH/Wrappers.h>

#include <vector>
#include <base/types.h>
#include <boost/container/flat_set.hpp>
#include <vector>

namespace DB
{
Expand Down Expand Up @@ -61,6 +62,12 @@ class AuthenticationData
const std::vector<ssh::SSHKey> & getSSHKeys() const { return ssh_keys; }
void setSSHKeys(std::vector<ssh::SSHKey> && ssh_keys_) { ssh_keys = std::forward<std::vector<ssh::SSHKey>>(ssh_keys_); }

HTTPAuthenticationScheme getHTTPAuthenticationScheme() const { return http_auth_scheme; }
void setHTTPAuthenticationScheme(HTTPAuthenticationScheme scheme) { http_auth_scheme = scheme; }

const String & getHTTPAuthenticationServerName() const { return http_auth_server_name; }
void setHTTPAuthenticationServerName(const String & name) { http_auth_server_name = name; }

friend bool operator ==(const AuthenticationData & lhs, const AuthenticationData & rhs);
friend bool operator !=(const AuthenticationData & lhs, const AuthenticationData & rhs) { return !(lhs == rhs); }

Expand Down Expand Up @@ -88,6 +95,9 @@ class AuthenticationData
boost::container::flat_set<String> ssl_certificate_common_names;
String salt;
std::vector<ssh::SSHKey> ssh_keys;
/// HTTP authentication properties
String http_auth_server_name;
HTTPAuthenticationScheme http_auth_scheme = HTTPAuthenticationScheme::BASIC;
};

}
5 changes: 5 additions & 0 deletions src/Access/Common/AuthenticationType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ const AuthenticationTypeInfo & AuthenticationTypeInfo::get(AuthenticationType ty
static const auto info = make_info("SSH_KEY");
return info;
}
case AuthenticationType::HTTP:
{
static const auto info = make_info("HTTP");
return info;
}
case AuthenticationType::MAX:
break;
}
Expand Down
3 changes: 3 additions & 0 deletions src/Access/Common/AuthenticationType.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ enum class AuthenticationType
/// The check is performed on server side by decrypting the data and comparing with the original string.
SSH_KEY,

/// Authentication through HTTP protocol
HTTP,

MAX,
};

Expand Down
31 changes: 31 additions & 0 deletions src/Access/Common/HTTPAuthenticationScheme.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#include "HTTPAuthenticationScheme.h"

#include <base/types.h>
#include <Poco/String.h>
#include <Common/Exception.h>

#include <magic_enum.hpp>


namespace DB
{
namespace ErrorCodes
{
extern const int BAD_ARGUMENTS;
}


String toString(HTTPAuthenticationScheme scheme)
{
return String(magic_enum::enum_name(scheme));
}

HTTPAuthenticationScheme parseHTTPAuthenticationScheme(const String & scheme_str)
{
auto scheme = magic_enum::enum_cast<HTTPAuthenticationScheme>(Poco::toUpper(scheme_str));
if (!scheme)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Unknown HTTP authentication scheme: {}. Possible value is 'BASIC'", scheme_str);
return *scheme;
}

}
16 changes: 16 additions & 0 deletions src/Access/Common/HTTPAuthenticationScheme.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include <base/types.h>

namespace DB
{

enum class HTTPAuthenticationScheme
{
BASIC,
};


String toString(HTTPAuthenticationScheme scheme);
HTTPAuthenticationScheme parseHTTPAuthenticationScheme(const String & scheme_str);
}