mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-09-19 16:20:50 +00:00
Merge pull request #62829 from ClickHouse/jwt-auth-in-client
Support JWT in `clickhouse-client`
This commit is contained in:
commit
665b4a9d1d
@ -193,6 +193,7 @@ You can pass parameters to `clickhouse-client` (all parameters have a default va
|
||||
- `--hardware-utilization` — Print hardware utilization information in progress bar.
|
||||
- `--print-profile-events` – Print `ProfileEvents` packets.
|
||||
- `--profile-events-delay-ms` – Delay between printing `ProfileEvents` packets (-1 - print only totals, 0 - print every single packet).
|
||||
- `--jwt` – If specified, enables authorization via JSON Web Token. Server JWT authorization is available only in ClickHouse Cloud.
|
||||
|
||||
Instead of `--host`, `--port`, `--user` and `--password` options, ClickHouse client also supports connection strings (see next section).
|
||||
|
||||
|
@ -141,6 +141,7 @@ $ clickhouse-client --param_tbl="numbers" --param_db="system" --param_col="numbe
|
||||
- `--secure` — если указано, будет использован безопасный канал.
|
||||
- `--history_file` - путь к файлу с историей команд.
|
||||
- `--param_<name>` — значение параметра для [запроса с параметрами](#cli-queries-with-parameters).
|
||||
- `--jwt` – авторизация с использованием JSON Web Token. Доступно только в ClickHouse Cloud.
|
||||
|
||||
Вместо параметров `--host`, `--port`, `--user` и `--password` клиент ClickHouse также поддерживает строки подключения (смотри следующий раздел).
|
||||
|
||||
|
@ -64,6 +64,7 @@ namespace ErrorCodes
|
||||
extern const int NETWORK_ERROR;
|
||||
extern const int AUTHENTICATION_FAILED;
|
||||
extern const int NO_ELEMENTS_IN_CONFIG;
|
||||
extern const int USER_EXPIRED;
|
||||
}
|
||||
|
||||
|
||||
@ -74,6 +75,12 @@ void Client::processError(const String & query) const
|
||||
fmt::print(stderr, "Received exception from server (version {}):\n{}\n",
|
||||
server_version,
|
||||
getExceptionMessage(*server_exception, print_stack_trace, true));
|
||||
|
||||
if (server_exception->code() == ErrorCodes::USER_EXPIRED)
|
||||
{
|
||||
server_exception->rethrow();
|
||||
}
|
||||
|
||||
if (is_interactive)
|
||||
{
|
||||
fmt::print(stderr, "\n");
|
||||
@ -944,6 +951,7 @@ void Client::addOptions(OptionsDescription & options_description)
|
||||
("ssh-key-file", po::value<std::string>(), "File containing the SSH private key for authenticate with the server.")
|
||||
("ssh-key-passphrase", po::value<std::string>(), "Passphrase for the SSH private key specified by --ssh-key-file.")
|
||||
("quota_key", po::value<std::string>(), "A string to differentiate quotas when the user have keyed quotas configured on server")
|
||||
("jwt", po::value<std::string>(), "Use JWT for authentication")
|
||||
|
||||
("max_client_network_bandwidth", po::value<int>(), "the maximum speed of data exchange over the network for the client in bytes per second.")
|
||||
("compression", po::value<bool>(), "enable or disable compression (enabled by default for remote communication and disabled for localhost communication).")
|
||||
@ -1102,6 +1110,12 @@ void Client::processOptions(const OptionsDescription & options_description,
|
||||
config().setBool("no-warnings", true);
|
||||
if (options.count("fake-drop"))
|
||||
config().setString("ignore_drop_queries_probability", "1");
|
||||
if (options.count("jwt"))
|
||||
{
|
||||
if (!options["user"].defaulted())
|
||||
throw Exception(ErrorCodes::BAD_ARGUMENTS, "User and JWT flags can't be specified together");
|
||||
config().setString("jwt", options["jwt"].as<std::string>());
|
||||
}
|
||||
if (options.count("accept-invalid-certificate"))
|
||||
{
|
||||
config().setString("openSSL.client.invalidCertificateHandler.name", "AcceptCertificateHandler");
|
||||
|
@ -108,6 +108,9 @@ bool Authentication::areCredentialsValid(
|
||||
case AuthenticationType::HTTP:
|
||||
throw Authentication::Require<BasicCredentials>("ClickHouse Basic Authentication");
|
||||
|
||||
case AuthenticationType::JWT:
|
||||
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "JWT is available only in ClickHouse Cloud");
|
||||
|
||||
case AuthenticationType::KERBEROS:
|
||||
return external_authenticators.checkKerberosCredentials(auth_data.getKerberosRealm(), *gss_acceptor_context);
|
||||
|
||||
@ -149,6 +152,9 @@ bool Authentication::areCredentialsValid(
|
||||
case AuthenticationType::SSL_CERTIFICATE:
|
||||
throw Authentication::Require<BasicCredentials>("ClickHouse X.509 Authentication");
|
||||
|
||||
case AuthenticationType::JWT:
|
||||
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "JWT is available only in ClickHouse Cloud");
|
||||
|
||||
case AuthenticationType::SSH_KEY:
|
||||
#if USE_SSH
|
||||
throw Authentication::Require<SshCredentials>("SSH Keys Authentication");
|
||||
@ -193,6 +199,9 @@ bool Authentication::areCredentialsValid(
|
||||
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "SSH is disabled, because ClickHouse is built without libssh");
|
||||
#endif
|
||||
|
||||
case AuthenticationType::JWT:
|
||||
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "JWT is available only in ClickHouse Cloud");
|
||||
|
||||
case AuthenticationType::BCRYPT_PASSWORD:
|
||||
return checkPasswordBcrypt(basic_credentials->getPassword(), auth_data.getPasswordHashBinary());
|
||||
|
||||
@ -222,6 +231,9 @@ bool Authentication::areCredentialsValid(
|
||||
case AuthenticationType::HTTP:
|
||||
throw Authentication::Require<BasicCredentials>("ClickHouse Basic Authentication");
|
||||
|
||||
case AuthenticationType::JWT:
|
||||
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "JWT is available only in ClickHouse Cloud");
|
||||
|
||||
case AuthenticationType::KERBEROS:
|
||||
throw Authentication::Require<GSSAcceptorContext>(auth_data.getKerberosRealm());
|
||||
|
||||
@ -254,6 +266,9 @@ bool Authentication::areCredentialsValid(
|
||||
case AuthenticationType::HTTP:
|
||||
throw Authentication::Require<BasicCredentials>("ClickHouse Basic Authentication");
|
||||
|
||||
case AuthenticationType::JWT:
|
||||
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "JWT is available only in ClickHouse Cloud");
|
||||
|
||||
case AuthenticationType::KERBEROS:
|
||||
throw Authentication::Require<GSSAcceptorContext>(auth_data.getKerberosRealm());
|
||||
|
||||
|
@ -135,6 +135,7 @@ void AuthenticationData::setPassword(const String & password_)
|
||||
case AuthenticationType::BCRYPT_PASSWORD:
|
||||
case AuthenticationType::NO_PASSWORD:
|
||||
case AuthenticationType::LDAP:
|
||||
case AuthenticationType::JWT:
|
||||
case AuthenticationType::KERBEROS:
|
||||
case AuthenticationType::SSL_CERTIFICATE:
|
||||
case AuthenticationType::SSH_KEY:
|
||||
@ -251,6 +252,7 @@ void AuthenticationData::setPasswordHashBinary(const Digest & hash)
|
||||
|
||||
case AuthenticationType::NO_PASSWORD:
|
||||
case AuthenticationType::LDAP:
|
||||
case AuthenticationType::JWT:
|
||||
case AuthenticationType::KERBEROS:
|
||||
case AuthenticationType::SSL_CERTIFICATE:
|
||||
case AuthenticationType::SSH_KEY:
|
||||
@ -322,6 +324,10 @@ std::shared_ptr<ASTAuthenticationData> AuthenticationData::toAST() const
|
||||
node->children.push_back(std::make_shared<ASTLiteral>(getLDAPServerName()));
|
||||
break;
|
||||
}
|
||||
case AuthenticationType::JWT:
|
||||
{
|
||||
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "JWT is available only in ClickHouse Cloud");
|
||||
}
|
||||
case AuthenticationType::KERBEROS:
|
||||
{
|
||||
const auto & realm = getKerberosRealm();
|
||||
|
@ -72,6 +72,11 @@ const AuthenticationTypeInfo & AuthenticationTypeInfo::get(AuthenticationType ty
|
||||
static const auto info = make_info(Keyword::HTTP);
|
||||
return info;
|
||||
}
|
||||
case AuthenticationType::JWT:
|
||||
{
|
||||
static const auto info = make_info(Keyword::JWT);
|
||||
return info;
|
||||
}
|
||||
case AuthenticationType::MAX:
|
||||
break;
|
||||
}
|
||||
|
@ -41,6 +41,9 @@ enum class AuthenticationType : uint8_t
|
||||
/// Authentication through HTTP protocol
|
||||
HTTP,
|
||||
|
||||
/// JSON Web Token
|
||||
JWT,
|
||||
|
||||
MAX,
|
||||
};
|
||||
|
||||
|
@ -33,6 +33,8 @@ void User::setName(const String & name_)
|
||||
throw Exception(ErrorCodes::BAD_ARGUMENTS, "User name '{}' is reserved", name_);
|
||||
if (name_.starts_with(EncodedUserInfo::SSH_KEY_AUTHENTICAION_MARKER))
|
||||
throw Exception(ErrorCodes::BAD_ARGUMENTS, "User name '{}' is reserved", name_);
|
||||
if (name_.starts_with(EncodedUserInfo::JWT_AUTHENTICAION_MARKER))
|
||||
throw Exception(ErrorCodes::BAD_ARGUMENTS, "User name '{}' is reserved", name_);
|
||||
name = name_;
|
||||
}
|
||||
|
||||
|
@ -109,6 +109,7 @@ namespace ErrorCodes
|
||||
extern const int USER_SESSION_LIMIT_EXCEEDED;
|
||||
extern const int NOT_IMPLEMENTED;
|
||||
extern const int CANNOT_READ_FROM_FILE_DESCRIPTOR;
|
||||
extern const int USER_EXPIRED;
|
||||
}
|
||||
|
||||
}
|
||||
@ -2270,7 +2271,7 @@ bool ClientBase::executeMultiQuery(const String & all_queries_text)
|
||||
catch (...)
|
||||
{
|
||||
// Surprisingly, this is a client error. A server error would
|
||||
// have been reported without throwing (see onReceiveSeverException()).
|
||||
// have been reported without throwing (see onReceiveExceptionFromServer()).
|
||||
client_exception = std::make_unique<Exception>(getCurrentExceptionMessageAndPattern(print_stack_trace), getCurrentExceptionCode());
|
||||
have_error = true;
|
||||
}
|
||||
@ -2643,6 +2644,9 @@ void ClientBase::runInteractive()
|
||||
}
|
||||
catch (const Exception & e)
|
||||
{
|
||||
if (e.code() == ErrorCodes::USER_EXPIRED)
|
||||
break;
|
||||
|
||||
/// We don't need to handle the test hints in the interactive mode.
|
||||
std::cerr << "Exception on client:" << std::endl << getExceptionMessage(e, print_stack_trace, true) << std::endl << std::endl;
|
||||
client_exception.reset(e.clone());
|
||||
|
@ -129,6 +129,7 @@ protected:
|
||||
const std::vector<Arguments> & hosts_and_ports_arguments) = 0;
|
||||
virtual void processConfig() = 0;
|
||||
|
||||
/// Returns true if query processing was successful.
|
||||
bool processQueryText(const String & text);
|
||||
|
||||
virtual void readArguments(
|
||||
|
@ -74,6 +74,7 @@ Connection::Connection(const String & host_, UInt16 port_,
|
||||
const String & default_database_,
|
||||
const String & user_, const String & password_,
|
||||
[[maybe_unused]] const SSHKey & ssh_private_key_,
|
||||
const String & jwt_,
|
||||
const String & quota_key_,
|
||||
const String & cluster_,
|
||||
const String & cluster_secret_,
|
||||
@ -86,6 +87,7 @@ Connection::Connection(const String & host_, UInt16 port_,
|
||||
, ssh_private_key(ssh_private_key_)
|
||||
#endif
|
||||
, quota_key(quota_key_)
|
||||
, jwt(jwt_)
|
||||
, cluster(cluster_)
|
||||
, cluster_secret(cluster_secret_)
|
||||
, client_name(client_name_)
|
||||
@ -341,6 +343,11 @@ void Connection::sendHello()
|
||||
performHandshakeForSSHAuth();
|
||||
}
|
||||
#endif
|
||||
else if (!jwt.empty())
|
||||
{
|
||||
writeStringBinary(EncodedUserInfo::JWT_AUTHENTICAION_MARKER, *out);
|
||||
writeStringBinary(jwt, *out);
|
||||
}
|
||||
else
|
||||
{
|
||||
writeStringBinary(user, *out);
|
||||
@ -1310,6 +1317,7 @@ ServerConnectionPtr Connection::createConnection(const ConnectionParameters & pa
|
||||
parameters.user,
|
||||
parameters.password,
|
||||
parameters.ssh_private_key,
|
||||
parameters.jwt,
|
||||
parameters.quota_key,
|
||||
"", /* cluster */
|
||||
"", /* cluster_secret */
|
||||
|
@ -53,6 +53,7 @@ public:
|
||||
const String & default_database_,
|
||||
const String & user_, const String & password_,
|
||||
const SSHKey & ssh_private_key_,
|
||||
const String & jwt_,
|
||||
const String & quota_key_,
|
||||
const String & cluster_,
|
||||
const String & cluster_secret_,
|
||||
@ -173,6 +174,7 @@ private:
|
||||
SSHKey ssh_private_key;
|
||||
#endif
|
||||
String quota_key;
|
||||
String jwt;
|
||||
|
||||
/// For inter-server authorization
|
||||
String cluster;
|
||||
|
@ -52,31 +52,11 @@ ConnectionParameters::ConnectionParameters(const Poco::Util::AbstractConfigurati
|
||||
/// changed the default value to "default" to fix the issue when the user in the prompt is blank
|
||||
user = config.getString("user", "default");
|
||||
|
||||
if (!config.has("ssh-key-file"))
|
||||
if (config.has("jwt"))
|
||||
{
|
||||
bool password_prompt = false;
|
||||
if (config.getBool("ask-password", false))
|
||||
{
|
||||
if (config.has("password"))
|
||||
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Specified both --password and --ask-password. Remove one of them");
|
||||
password_prompt = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
password = config.getString("password", "");
|
||||
/// if the value of --password is omitted, the password will be set implicitly to "\n"
|
||||
if (password == ASK_PASSWORD)
|
||||
password_prompt = true;
|
||||
}
|
||||
if (password_prompt)
|
||||
{
|
||||
std::string prompt{"Password for user (" + user + "): "};
|
||||
char buf[1000] = {};
|
||||
if (auto * result = readpassphrase(prompt.c_str(), buf, sizeof(buf), 0))
|
||||
password = result;
|
||||
}
|
||||
jwt = config.getString("jwt");
|
||||
}
|
||||
else
|
||||
else if (config.has("ssh-key-file"))
|
||||
{
|
||||
#if USE_SSH
|
||||
std::string filename = config.getString("ssh-key-file");
|
||||
@ -102,6 +82,30 @@ ConnectionParameters::ConnectionParameters(const Poco::Util::AbstractConfigurati
|
||||
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "SSH is disabled, because ClickHouse is built without libssh");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
bool password_prompt = false;
|
||||
if (config.getBool("ask-password", false))
|
||||
{
|
||||
if (config.has("password"))
|
||||
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Specified both --password and --ask-password. Remove one of them");
|
||||
password_prompt = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
password = config.getString("password", "");
|
||||
/// if the value of --password is omitted, the password will be set implicitly to "\n"
|
||||
if (password == ASK_PASSWORD)
|
||||
password_prompt = true;
|
||||
}
|
||||
if (password_prompt)
|
||||
{
|
||||
std::string prompt{"Password for user (" + user + "): "};
|
||||
char buf[1000] = {};
|
||||
if (auto * result = readpassphrase(prompt.c_str(), buf, sizeof(buf), 0))
|
||||
password = result;
|
||||
}
|
||||
}
|
||||
|
||||
quota_key = config.getString("quota_key", "");
|
||||
|
||||
@ -139,7 +143,7 @@ ConnectionParameters::ConnectionParameters(const Poco::Util::AbstractConfigurati
|
||||
}
|
||||
|
||||
UInt16 ConnectionParameters::getPortFromConfig(const Poco::Util::AbstractConfiguration & config,
|
||||
std::string connection_host)
|
||||
const std::string & connection_host)
|
||||
{
|
||||
bool is_secure = enableSecureConnection(config, connection_host);
|
||||
return config.getInt("port",
|
||||
|
@ -22,6 +22,7 @@ struct ConnectionParameters
|
||||
std::string password;
|
||||
std::string quota_key;
|
||||
SSHKey ssh_private_key;
|
||||
std::string jwt;
|
||||
Protocol::Secure security = Protocol::Secure::Disable;
|
||||
Protocol::Compression compression = Protocol::Compression::Enable;
|
||||
ConnectionTimeouts timeouts;
|
||||
@ -30,7 +31,7 @@ struct ConnectionParameters
|
||||
ConnectionParameters(const Poco::Util::AbstractConfiguration & config, std::string host);
|
||||
ConnectionParameters(const Poco::Util::AbstractConfiguration & config, std::string host, std::optional<UInt16> port);
|
||||
|
||||
static UInt16 getPortFromConfig(const Poco::Util::AbstractConfiguration & config, std::string connection_host);
|
||||
static UInt16 getPortFromConfig(const Poco::Util::AbstractConfiguration & config, const std::string & connection_host);
|
||||
|
||||
/// Ask to enter the user's password if password option contains this value.
|
||||
/// "\n" is used because there is hardly a chance that a user would use '\n' as password.
|
||||
|
@ -123,7 +123,7 @@ protected:
|
||||
{
|
||||
return std::make_shared<Connection>(
|
||||
host, port,
|
||||
default_database, user, password, SSHKey(), quota_key,
|
||||
default_database, user, password, SSHKey(), /*jwt*/ "", quota_key,
|
||||
cluster, cluster_secret,
|
||||
client_name, compression, secure);
|
||||
}
|
||||
|
@ -63,6 +63,9 @@ const char USER_INTERSERVER_MARKER[] = " INTERSERVER SECRET ";
|
||||
/// Marker for SSH-keys-based authentication (passed as the user name)
|
||||
const char SSH_KEY_AUTHENTICAION_MARKER[] = " SSH KEY AUTHENTICATION ";
|
||||
|
||||
/// Market for JSON Web Token authentication
|
||||
const char JWT_AUTHENTICAION_MARKER[] = " JWT AUTHENTICATION ";
|
||||
|
||||
};
|
||||
|
||||
namespace Protocol
|
||||
|
@ -86,6 +86,7 @@ ColumnsDescription SessionLogElement::getColumnsDescription()
|
||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::SHA256_PASSWORD),
|
||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::DOUBLE_SHA1_PASSWORD),
|
||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::LDAP),
|
||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::JWT),
|
||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::KERBEROS),
|
||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::SSH_KEY),
|
||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::SSL_CERTIFICATE),
|
||||
@ -93,7 +94,7 @@ ColumnsDescription SessionLogElement::getColumnsDescription()
|
||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::HTTP),
|
||||
});
|
||||
#undef AUTH_TYPE_NAME_AND_VALUE
|
||||
static_assert(static_cast<int>(AuthenticationType::MAX) == 10);
|
||||
static_assert(static_cast<int>(AuthenticationType::MAX) == 11);
|
||||
|
||||
auto interface_type_column = std::make_shared<DataTypeEnum8>(
|
||||
DataTypeEnum8::Values
|
||||
|
@ -89,6 +89,12 @@ void ASTAuthenticationData::formatImpl(const FormatSettings & settings, FormatSt
|
||||
password = true;
|
||||
break;
|
||||
}
|
||||
case AuthenticationType::JWT:
|
||||
{
|
||||
prefix = "CLAIMS";
|
||||
parameter = true;
|
||||
break;
|
||||
}
|
||||
case AuthenticationType::LDAP:
|
||||
{
|
||||
prefix = "SERVER";
|
||||
|
@ -250,6 +250,7 @@ namespace DB
|
||||
MR_MACROS(IS_NOT_NULL, "IS NOT NULL") \
|
||||
MR_MACROS(IS_NULL, "IS NULL") \
|
||||
MR_MACROS(JOIN, "JOIN") \
|
||||
MR_MACROS(JWT, "JWT") \
|
||||
MR_MACROS(KERBEROS, "KERBEROS") \
|
||||
MR_MACROS(KEY_BY, "KEY BY") \
|
||||
MR_MACROS(KEY, "KEY") \
|
||||
|
@ -90,6 +90,7 @@ message QueryInfo {
|
||||
string user_name = 9;
|
||||
string password = 10;
|
||||
string quota = 11;
|
||||
string jwt = 25;
|
||||
|
||||
// Works exactly like sessions in the HTTP protocol.
|
||||
string session_id = 12;
|
||||
|
@ -5656,7 +5656,7 @@ std::optional<QueryPipeline> StorageReplicatedMergeTree::distributedWriteFromClu
|
||||
{
|
||||
auto connection = std::make_shared<Connection>(
|
||||
node.host_name, node.port, query_context->getGlobalContext()->getCurrentDatabase(),
|
||||
node.user, node.password, SSHKey(), node.quota_key, node.cluster, node.cluster_secret,
|
||||
node.user, node.password, SSHKey(), /*jwt*/"", node.quota_key, node.cluster, node.cluster_secret,
|
||||
"ParallelInsertSelectInititiator",
|
||||
node.compression,
|
||||
node.secure
|
||||
|
@ -1136,7 +1136,7 @@ CREATE TABLE system.users
|
||||
`name` String,
|
||||
`id` UUID,
|
||||
`storage` String,
|
||||
`auth_type` Enum8('no_password' = 0, 'plaintext_password' = 1, 'sha256_password' = 2, 'double_sha1_password' = 3, 'ldap' = 4, 'kerberos' = 5, 'ssl_certificate' = 6, 'bcrypt_password' = 7, 'ssh_key' = 8, 'http' = 9),
|
||||
`auth_type` Enum8('no_password' = 0, 'plaintext_password' = 1, 'sha256_password' = 2, 'double_sha1_password' = 3, 'ldap' = 4, 'kerberos' = 5, 'ssl_certificate' = 6, 'bcrypt_password' = 7, 'ssh_key' = 8, 'http' = 9, 'jwt' = 10),
|
||||
`auth_params` String,
|
||||
`host_ip` Array(String),
|
||||
`host_names` Array(String),
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user