mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-25 00:52:02 +00:00
Merge 9c1fcc5adc
into 44b4bd38b9
This commit is contained in:
commit
29f4538c36
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -375,3 +375,6 @@
|
|||||||
[submodule "contrib/postgres"]
|
[submodule "contrib/postgres"]
|
||||||
path = contrib/postgres
|
path = contrib/postgres
|
||||||
url = https://github.com/ClickHouse/postgres.git
|
url = https://github.com/ClickHouse/postgres.git
|
||||||
|
[submodule "contrib/libcotp"]
|
||||||
|
path = contrib/libcotp
|
||||||
|
url = https://github.com/paolostivanin/libcotp.git
|
||||||
|
2
contrib/CMakeLists.txt
vendored
2
contrib/CMakeLists.txt
vendored
@ -223,6 +223,8 @@ add_contrib (google-cloud-cpp-cmake google-cloud-cpp) # requires grpc, protobuf,
|
|||||||
|
|
||||||
add_contrib (jwt-cpp-cmake jwt-cpp)
|
add_contrib (jwt-cpp-cmake jwt-cpp)
|
||||||
|
|
||||||
|
add_contrib (libcotp-cmake libcotp)
|
||||||
|
|
||||||
# Put all targets defined here and in subdirectories under "contrib/<immediate-subdir>" folders in GUI-based IDEs.
|
# Put all targets defined here and in subdirectories under "contrib/<immediate-subdir>" folders in GUI-based IDEs.
|
||||||
# Some of third-party projects may override CMAKE_FOLDER or FOLDER property of their targets, so they would not appear
|
# Some of third-party projects may override CMAKE_FOLDER or FOLDER property of their targets, so they would not appear
|
||||||
# in "contrib/..." as originally planned, so we workaround this by fixing FOLDER properties of all targets manually,
|
# in "contrib/..." as originally planned, so we workaround this by fixing FOLDER properties of all targets manually,
|
||||||
|
1
contrib/libcotp
vendored
Submodule
1
contrib/libcotp
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 78a3783ac19604e9e3ad7053f1c43c761066bfb4
|
22
contrib/libcotp-cmake/CMakeLists.txt
Normal file
22
contrib/libcotp-cmake/CMakeLists.txt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
if(NOT ENABLE_SSL)
|
||||||
|
message(STATUS "libcotp: DISABLED because ENABLE_SSL=OFF")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set (LIBCOTP_SOURCE_DIR "${ClickHouse_SOURCE_DIR}/contrib/libcotp")
|
||||||
|
set (LIBCOTP_BINARY_DIR "${ClickHouse_BINARY_DIR}/contrib/libcotp")
|
||||||
|
|
||||||
|
set(SRCS
|
||||||
|
"${LIBCOTP_SOURCE_DIR}/src/otp.c"
|
||||||
|
"${LIBCOTP_SOURCE_DIR}/src/utils/base32.c"
|
||||||
|
"${LIBCOTP_SOURCE_DIR}/src/utils/whmac_openssl.c"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library (_libcotp ${SRCS})
|
||||||
|
|
||||||
|
file(MAKE_DIRECTORY "${LIBCOTP_BINARY_DIR}/include")
|
||||||
|
file(COPY "${LIBCOTP_SOURCE_DIR}/src/" DESTINATION "${LIBCOTP_BINARY_DIR}/include" FILES_MATCHING PATTERN "*.h")
|
||||||
|
target_include_directories(_libcotp SYSTEM BEFORE PUBLIC "${LIBCOTP_BINARY_DIR}/include")
|
||||||
|
target_link_libraries(_libcotp PRIVATE OpenSSL::Crypto)
|
||||||
|
|
||||||
|
add_library(ch_contrib::libcotp ALIAS _libcotp)
|
@ -17,3 +17,4 @@ The following external authenticators and directories are supported:
|
|||||||
- Kerberos [Authenticator](./kerberos.md#external-authenticators-kerberos)
|
- Kerberos [Authenticator](./kerberos.md#external-authenticators-kerberos)
|
||||||
- [SSL X.509 authentication](./ssl-x509.md#ssl-external-authentication)
|
- [SSL X.509 authentication](./ssl-x509.md#ssl-external-authentication)
|
||||||
- HTTP [Authenticator](./http.md)
|
- HTTP [Authenticator](./http.md)
|
||||||
|
- [Time-based one-time password](./totp.md)
|
||||||
|
69
docs/en/operations/external-authenticators/totp.md
Normal file
69
docs/en/operations/external-authenticators/totp.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
slug: /en/operations/external-authenticators/totp
|
||||||
|
title: "TOTP"
|
||||||
|
---
|
||||||
|
import SelfManaged from '@site/docs/en/_snippets/_self_managed_only_no_roadmap.md';
|
||||||
|
|
||||||
|
<SelfManaged />
|
||||||
|
|
||||||
|
Time-Based One-Time Password (TOTP) can be used to authenticate ClickHouse users by generating temporary access codes that are valid for a limited time.
|
||||||
|
In current implementation it is a standalone authentication method, rather than a second factor for password-based authentication.
|
||||||
|
This TOTP authentication method aligns with [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238) standards, making it compatible with popular TOTP applications like Google Authenticator, 1Password and similar tools.
|
||||||
|
|
||||||
|
## TOTP Authentication Configuration {#totp-auth-configuration}
|
||||||
|
|
||||||
|
To enable TOTP authentication for a user, configure the `time_based_one_time_password` section in `users.xml`. This section defines the TOTP settings, such as secret, validity period, number of digits, and hash algorithm.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
```xml
|
||||||
|
<clickhouse>
|
||||||
|
<!-- ... -->
|
||||||
|
<users>
|
||||||
|
<my_user>
|
||||||
|
<!-- TOTP authentication configuration -->
|
||||||
|
<time_based_one_time_password>
|
||||||
|
<secret>JBSWY3DPEHPK3PXP</secret> <!-- Base32-encoded TOTP secret -->
|
||||||
|
<period>30</period> <!-- Optional: OTP validity period in seconds -->
|
||||||
|
<digits>6</digits> <!-- Optional: Number of digits in the OTP -->
|
||||||
|
<algorithm>SHA1</algorithm> <!-- Optional: Hash algorithm: SHA1, SHA256, SHA512 -->
|
||||||
|
</time_based_one_time_password>
|
||||||
|
</my_user>
|
||||||
|
</users>
|
||||||
|
</clickhouse>
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
- secret - (Required) The base32-encoded secret key used to generate TOTP codes.
|
||||||
|
- period - Optional. Sets the validity period of each OTP in seconds. Must be a positive number not exceeding 120. Default is 30.
|
||||||
|
- digits - Optional. Specifies the number of digits in each OTP. Must be between 4 and 10. Default is 6.
|
||||||
|
- algorithm - Optional. Defines the hash algorithm for generating OTPs. Supported values are SHA1, SHA256, and SHA512. Default is SHA1.
|
||||||
|
|
||||||
|
Generating a TOTP Secret
|
||||||
|
|
||||||
|
To generate a TOTP-compatible secret for use with ClickHouse, run the following command in the terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ base32 -w32 < /dev/urandom | head -1
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will produce a base32-encoded secret that can be added to the secret field in users.xml.
|
||||||
|
|
||||||
|
To enable TOTP for a specific user, replace any existing password-based fields (like `password` or `password_sha256_hex`) with the `time_based_one_time_password` section. Only one authentication method is allowed per user, so TOTP cannot be combined with other methods such as password or LDAP.
|
||||||
|
|
||||||
|
The [qrencode](https://linux.die.net/man/1/qrencode) tool can be used to generate a QR code for the TOTP secret.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ qrencode -t ansiutf8 'otpauth://totp/ClickHouse?issuer=ClickHouse&secret=JBSWY3DPEHPK3PXP'
|
||||||
|
```
|
||||||
|
|
||||||
|
After configuring TOTP for a user, one-time password can be used as a password for a user.
|
||||||
|
|
||||||
|
## Enabling TOTP Authentication using SQL {#enabling-totp-auth-using-sql}
|
||||||
|
|
||||||
|
When SQL-driven Access Control and Account Management is enabled, TOTP authentication can be set for users via SQL:
|
||||||
|
|
||||||
|
```SQL
|
||||||
|
CREATE USER my_user IDENTIFIED WITH one_time_password BY 'JBSWY3DPEHPK3PXP';
|
||||||
|
```
|
||||||
|
|
||||||
|
Values for `period`, `digits`, and `algorithm` will be set to their default values.
|
@ -32,6 +32,24 @@
|
|||||||
If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
|
If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
|
||||||
Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
|
Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
|
||||||
|
|
||||||
|
If you want to specify secret for one time passwords, place it in 'time_based_one_time_password' element.
|
||||||
|
Example:
|
||||||
|
<time_based_one_time_password>
|
||||||
|
<secret>ALLT7V6M3NNSQXMEMPEAUSX77OOUTBSO</secret>
|
||||||
|
<period>30</period>
|
||||||
|
<digits>6</digits>
|
||||||
|
<algorithm>SHA1</algorithm>
|
||||||
|
</time_based_one_time_password>
|
||||||
|
|
||||||
|
secret - base32 encoded secret key
|
||||||
|
period - password validity duration in seconds, default is 30
|
||||||
|
digits - number of digits in password, default is 6
|
||||||
|
algorithm - hash algorithm: SHA1/SHA256/SHA512, default is SHA1
|
||||||
|
|
||||||
|
How to generate secret:
|
||||||
|
Execute: echo `base32 < /dev/urandom | head -c32`
|
||||||
|
More info: https://datatracker.ietf.org/doc/html/rfc6238
|
||||||
|
|
||||||
If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
|
If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
|
||||||
place its name in 'server' element inside 'ldap' element.
|
place its name in 'server' element inside 'ldap' element.
|
||||||
Example: <ldap><server>my_ldap_server</server></ldap>
|
Example: <ldap><server>my_ldap_server</server></ldap>
|
||||||
|
@ -611,7 +611,7 @@ AuthResult AccessControl::authenticate(const Credentials & credentials, const Po
|
|||||||
tryLogCurrentException(getLogger(), "from: " + address.toString() + ", user: " + credentials.getUserName() + ": Authentication failed", LogsLevel::information);
|
tryLogCurrentException(getLogger(), "from: " + address.toString() + ", user: " + credentials.getUserName() + ": Authentication failed", LogsLevel::information);
|
||||||
|
|
||||||
WriteBufferFromOwnString message;
|
WriteBufferFromOwnString message;
|
||||||
message << credentials.getUserName() << ": Authentication failed: password is incorrect, or there is no user with such name.";
|
message << credentials.getUserName() << ": Authentication failed: password is incorrect, or there is no user with such name";
|
||||||
|
|
||||||
/// Better exception message for usability.
|
/// Better exception message for usability.
|
||||||
/// It is typical when users install ClickHouse, type some password and instantly forget it.
|
/// It is typical when users install ClickHouse, type some password and instantly forget it.
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
#include <Common/SSHWrapper.h>
|
#include <Common/SSHWrapper.h>
|
||||||
#include <Common/typeid_cast.h>
|
#include <Common/typeid_cast.h>
|
||||||
#include <Access/Common/SSLCertificateSubjects.h>
|
#include <Access/Common/SSLCertificateSubjects.h>
|
||||||
|
#include <Access/Common/OneTimePassword.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
@ -136,6 +137,12 @@ namespace
|
|||||||
{
|
{
|
||||||
return checkPasswordDoubleSHA1(basic_credentials->getPassword(), authentication_method.getPasswordHashBinary());
|
return checkPasswordDoubleSHA1(basic_credentials->getPassword(), authentication_method.getPasswordHashBinary());
|
||||||
}
|
}
|
||||||
|
case AuthenticationType::ONE_TIME_PASSWORD:
|
||||||
|
{
|
||||||
|
return checkOneTimePassword(
|
||||||
|
/* password */ basic_credentials->getPassword(),
|
||||||
|
/* secret */ authentication_method.getOneTimePassword());
|
||||||
|
}
|
||||||
case AuthenticationType::LDAP:
|
case AuthenticationType::LDAP:
|
||||||
{
|
{
|
||||||
return external_authenticators.checkLDAPCredentials(authentication_method.getLDAPServerName(), *basic_credentials);
|
return external_authenticators.checkLDAPCredentials(authentication_method.getLDAPServerName(), *basic_credentials);
|
||||||
|
@ -138,6 +138,10 @@ void AuthenticationData::setPassword(const String & password_, bool validate)
|
|||||||
setPasswordHashBinary(Util::encodeDoubleSHA1(password_), validate);
|
setPasswordHashBinary(Util::encodeDoubleSHA1(password_), validate);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case AuthenticationType::ONE_TIME_PASSWORD:
|
||||||
|
setOneTimePassword(password_, OneTimePasswordParams{}, validate);
|
||||||
|
return;
|
||||||
|
|
||||||
case AuthenticationType::BCRYPT_PASSWORD:
|
case AuthenticationType::BCRYPT_PASSWORD:
|
||||||
case AuthenticationType::NO_PASSWORD:
|
case AuthenticationType::NO_PASSWORD:
|
||||||
case AuthenticationType::LDAP:
|
case AuthenticationType::LDAP:
|
||||||
@ -154,6 +158,15 @@ void AuthenticationData::setPassword(const String & password_, bool validate)
|
|||||||
throw Exception(ErrorCodes::NOT_IMPLEMENTED, "setPassword(): authentication type {} not supported", toString(type));
|
throw Exception(ErrorCodes::NOT_IMPLEMENTED, "setPassword(): authentication type {} not supported", toString(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AuthenticationData::setOneTimePassword(const String & password_, OneTimePasswordParams config, bool validate)
|
||||||
|
{
|
||||||
|
if (type != AuthenticationType::ONE_TIME_PASSWORD)
|
||||||
|
throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot specify one-time password for authentication type {}", toString(type));
|
||||||
|
|
||||||
|
otp_secret.emplace(password_, config);
|
||||||
|
setPasswordHashBinary(Util::stringToDigest(otp_secret->key), validate);
|
||||||
|
}
|
||||||
|
|
||||||
void AuthenticationData::setPasswordBcrypt(const String & password_, int workfactor_, bool validate)
|
void AuthenticationData::setPasswordBcrypt(const String & password_, int workfactor_, bool validate)
|
||||||
{
|
{
|
||||||
if (type != AuthenticationType::BCRYPT_PASSWORD)
|
if (type != AuthenticationType::BCRYPT_PASSWORD)
|
||||||
@ -164,9 +177,13 @@ void AuthenticationData::setPasswordBcrypt(const String & password_, int workfac
|
|||||||
|
|
||||||
String AuthenticationData::getPassword() const
|
String AuthenticationData::getPassword() const
|
||||||
{
|
{
|
||||||
if (type != AuthenticationType::PLAINTEXT_PASSWORD)
|
if (type == AuthenticationType::PLAINTEXT_PASSWORD)
|
||||||
throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot decode the password");
|
|
||||||
return String(password_hash.data(), password_hash.data() + password_hash.size());
|
return String(password_hash.data(), password_hash.data() + password_hash.size());
|
||||||
|
|
||||||
|
if (type == AuthenticationType::ONE_TIME_PASSWORD && otp_secret)
|
||||||
|
return otp_secret->key;
|
||||||
|
|
||||||
|
throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot decode the password for authentication type {}", type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -210,6 +227,12 @@ void AuthenticationData::setPasswordHashBinary(const Digest & hash, bool validat
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case AuthenticationType::ONE_TIME_PASSWORD:
|
||||||
|
{
|
||||||
|
password_hash = hash;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
case AuthenticationType::SHA256_PASSWORD:
|
case AuthenticationType::SHA256_PASSWORD:
|
||||||
{
|
{
|
||||||
if (hash.size() != 32)
|
if (hash.size() != 32)
|
||||||
@ -312,6 +335,12 @@ std::shared_ptr<ASTAuthenticationData> AuthenticationData::toAST() const
|
|||||||
node->children.push_back(std::make_shared<ASTLiteral>(getPassword()));
|
node->children.push_back(std::make_shared<ASTLiteral>(getPassword()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case AuthenticationType::ONE_TIME_PASSWORD:
|
||||||
|
{
|
||||||
|
node->contains_password = true;
|
||||||
|
node->children.push_back(std::make_shared<ASTLiteral>(getPassword()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
case AuthenticationType::SHA256_PASSWORD:
|
case AuthenticationType::SHA256_PASSWORD:
|
||||||
{
|
{
|
||||||
node->contains_hash = true;
|
node->contains_hash = true;
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#include <Access/Common/AuthenticationType.h>
|
#include <Access/Common/AuthenticationType.h>
|
||||||
#include <Access/Common/HTTPAuthenticationScheme.h>
|
#include <Access/Common/HTTPAuthenticationScheme.h>
|
||||||
#include <Access/Common/SSLCertificateSubjects.h>
|
#include <Access/Common/SSLCertificateSubjects.h>
|
||||||
|
#include <Access/Common/OneTimePassword.h>
|
||||||
#include <Common/SSHWrapper.h>
|
#include <Common/SSHWrapper.h>
|
||||||
#include <Interpreters/Context_fwd.h>
|
#include <Interpreters/Context_fwd.h>
|
||||||
#include <Parsers/Access/ASTAuthenticationData.h>
|
#include <Parsers/Access/ASTAuthenticationData.h>
|
||||||
@ -36,6 +37,9 @@ public:
|
|||||||
/// Returns the password. Allowed to use only for Type::PLAINTEXT_PASSWORD.
|
/// Returns the password. Allowed to use only for Type::PLAINTEXT_PASSWORD.
|
||||||
String getPassword() const;
|
String getPassword() const;
|
||||||
|
|
||||||
|
void setOneTimePassword(const String & password_, OneTimePasswordParams config, bool validate);
|
||||||
|
const OneTimePasswordSecret & getOneTimePassword() const { return otp_secret.value(); }
|
||||||
|
|
||||||
/// Sets the password as a string of hexadecimal digits.
|
/// Sets the password as a string of hexadecimal digits.
|
||||||
void setPasswordHashHex(const String & hash, bool validate);
|
void setPasswordHashHex(const String & hash, bool validate);
|
||||||
String getPasswordHashHex() const;
|
String getPasswordHashHex() const;
|
||||||
@ -99,6 +103,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
AuthenticationType type = AuthenticationType::NO_PASSWORD;
|
AuthenticationType type = AuthenticationType::NO_PASSWORD;
|
||||||
Digest password_hash;
|
Digest password_hash;
|
||||||
|
std::optional<OneTimePasswordSecret> otp_secret;
|
||||||
String ldap_server_name;
|
String ldap_server_name;
|
||||||
String kerberos_realm;
|
String kerberos_realm;
|
||||||
SSLCertificateSubjects ssl_certificate_subjects;
|
SSLCertificateSubjects ssl_certificate_subjects;
|
||||||
|
@ -42,6 +42,11 @@ const AuthenticationTypeInfo & AuthenticationTypeInfo::get(AuthenticationType ty
|
|||||||
static const auto info = make_info(Keyword::DOUBLE_SHA1_PASSWORD, true);
|
static const auto info = make_info(Keyword::DOUBLE_SHA1_PASSWORD, true);
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
case AuthenticationType::ONE_TIME_PASSWORD:
|
||||||
|
{
|
||||||
|
static const auto info = make_info(Keyword::ONE_TIME_PASSWORD, true);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
case AuthenticationType::LDAP:
|
case AuthenticationType::LDAP:
|
||||||
{
|
{
|
||||||
static const auto info = make_info(Keyword::LDAP);
|
static const auto info = make_info(Keyword::LDAP);
|
||||||
|
@ -21,6 +21,10 @@ enum class AuthenticationType : uint8_t
|
|||||||
/// This kind of hash is used by the `mysql_native_password` authentication plugin.
|
/// This kind of hash is used by the `mysql_native_password` authentication plugin.
|
||||||
DOUBLE_SHA1_PASSWORD,
|
DOUBLE_SHA1_PASSWORD,
|
||||||
|
|
||||||
|
/// A secret is stored instead of a password
|
||||||
|
/// Users are authenticated using time-based one-time passwords
|
||||||
|
ONE_TIME_PASSWORD,
|
||||||
|
|
||||||
/// Password is checked by a [remote] LDAP server. Connection will be made at each authentication attempt.
|
/// Password is checked by a [remote] LDAP server. Connection will be made at each authentication attempt.
|
||||||
LDAP,
|
LDAP,
|
||||||
|
|
||||||
|
@ -3,3 +3,7 @@ include("${ClickHouse_SOURCE_DIR}/cmake/dbms_glob_sources.cmake")
|
|||||||
add_headers_and_sources(clickhouse_common_access .)
|
add_headers_and_sources(clickhouse_common_access .)
|
||||||
add_library(clickhouse_common_access ${clickhouse_common_access_headers} ${clickhouse_common_access_sources})
|
add_library(clickhouse_common_access ${clickhouse_common_access_headers} ${clickhouse_common_access_sources})
|
||||||
target_link_libraries(clickhouse_common_access PUBLIC clickhouse_common_io)
|
target_link_libraries(clickhouse_common_access PUBLIC clickhouse_common_io)
|
||||||
|
|
||||||
|
if (TARGET ch_contrib::libcotp)
|
||||||
|
target_link_libraries (clickhouse_common_access PRIVATE ch_contrib::libcotp)
|
||||||
|
endif()
|
||||||
|
161
src/Access/Common/OneTimePassword.cpp
Normal file
161
src/Access/Common/OneTimePassword.cpp
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
#include <Access/Common/OneTimePassword.h>
|
||||||
|
#include <Common/Exception.h>
|
||||||
|
#include <Common/ErrorCodes.h>
|
||||||
|
#include <Common/logger_useful.h>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include <Poco/String.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#if USE_SSL
|
||||||
|
|
||||||
|
#include <cotp.h>
|
||||||
|
|
||||||
|
constexpr int TOTP_SHA512 = SHA512;
|
||||||
|
constexpr int TOTP_SHA256 = SHA256;
|
||||||
|
constexpr int TOTP_SHA1 = SHA1;
|
||||||
|
#undef SHA512
|
||||||
|
#undef SHA256
|
||||||
|
#undef SHA1
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace DB
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace ErrorCodes
|
||||||
|
{
|
||||||
|
extern const int BAD_ARGUMENTS;
|
||||||
|
extern const int LOGICAL_ERROR;
|
||||||
|
extern const int SUPPORT_IS_DISABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the secret contains only valid base32 characters.
|
||||||
|
/// The secret may contain spaces, which are ignored and lower-case characters, which are converted to upper-case.
|
||||||
|
String normalizeOneTimePasswordSecret(const String & secret)
|
||||||
|
{
|
||||||
|
static const UInt8 b32_alphabet[] = u8"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||||
|
static const UInt8 b32_lower_alphabet[] = u8"abcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
constexpr static UInt8 CHAR_IS_VALID = 1;
|
||||||
|
constexpr static UInt8 CHAR_IS_LOWER = 2;
|
||||||
|
std::array<UInt8, 128> table = {};
|
||||||
|
for (const auto * p = b32_alphabet; *p; p++)
|
||||||
|
table[*p] = CHAR_IS_VALID;
|
||||||
|
for (const auto * p = b32_lower_alphabet; *p; p++)
|
||||||
|
table[*p] = CHAR_IS_LOWER;
|
||||||
|
|
||||||
|
String result = secret;
|
||||||
|
size_t i = 0;
|
||||||
|
size_t n = 0;
|
||||||
|
for (; i < secret.size(); ++i)
|
||||||
|
{
|
||||||
|
if (secret[i] == ' ' || secret[i] == '=')
|
||||||
|
continue;
|
||||||
|
size_t idx = static_cast<UInt8>(secret[i]);
|
||||||
|
if (idx >= table.size() || table[idx] == 0)
|
||||||
|
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid character in base32 secret: '{}'", secret[i]);
|
||||||
|
if (table[idx] == CHAR_IS_VALID)
|
||||||
|
result[n] = secret[i];
|
||||||
|
if (table[idx] == CHAR_IS_LOWER)
|
||||||
|
result[n] = std::toupper(secret[i]);
|
||||||
|
++n;
|
||||||
|
}
|
||||||
|
result.resize(n);
|
||||||
|
|
||||||
|
if (result.empty())
|
||||||
|
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Empty secret for one-time password");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string_view toString(OneTimePasswordParams::Algorithm algorithm)
|
||||||
|
{
|
||||||
|
switch (algorithm)
|
||||||
|
{
|
||||||
|
case OneTimePasswordParams::Algorithm::SHA1: return "SHA1";
|
||||||
|
case OneTimePasswordParams::Algorithm::SHA256: return "SHA256";
|
||||||
|
case OneTimePasswordParams::Algorithm::SHA512: return "SHA512";
|
||||||
|
}
|
||||||
|
throw Exception(ErrorCodes::LOGICAL_ERROR, "Unknown algorithm for one-time password: {}", static_cast<UInt32>(algorithm));
|
||||||
|
}
|
||||||
|
|
||||||
|
static OneTimePasswordParams::Algorithm hashingAlgorithmFromString(const String & algorithm_name)
|
||||||
|
{
|
||||||
|
for (auto alg : {OneTimePasswordParams::Algorithm::SHA1, OneTimePasswordParams::Algorithm::SHA256, OneTimePasswordParams::Algorithm::SHA512})
|
||||||
|
{
|
||||||
|
if (Poco::toUpper(algorithm_name) == toString(alg))
|
||||||
|
return alg;
|
||||||
|
}
|
||||||
|
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Unknown algorithm for one-time password: '{}'", algorithm_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
OneTimePasswordParams::OneTimePasswordParams(Int32 num_digits_, Int32 period_, const String & algorithm_name_)
|
||||||
|
{
|
||||||
|
if (num_digits_)
|
||||||
|
num_digits = num_digits_;
|
||||||
|
if (period_)
|
||||||
|
period = period_;
|
||||||
|
if (!algorithm_name_.empty())
|
||||||
|
algorithm = hashingAlgorithmFromString(algorithm_name_);
|
||||||
|
|
||||||
|
if (num_digits < 4 || 10 < num_digits)
|
||||||
|
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid number of digits for one-time password: {}", num_digits);
|
||||||
|
if (period <= 0 || 120 < period)
|
||||||
|
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid period for one-time password: {}", period);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view OneTimePasswordParams::getAlgorithmName() const { return toString(algorithm); }
|
||||||
|
|
||||||
|
|
||||||
|
OneTimePasswordSecret::OneTimePasswordSecret(const String & key_, OneTimePasswordParams params_)
|
||||||
|
: key(normalizeOneTimePasswordSecret(key_)), params(params_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
String getOneTimePasswordSecretLink(const OneTimePasswordSecret & secret)
|
||||||
|
{
|
||||||
|
if (secret.params == OneTimePasswordParams{})
|
||||||
|
return fmt::format("otpauth://totp/ClickHouse?issuer=ClickHouse&secret={}", secret.key);
|
||||||
|
|
||||||
|
return fmt::format("otpauth://totp/ClickHouse?issuer=ClickHouse&secret={}&digits={}&period={}&algorithm={}",
|
||||||
|
secret.key, secret.params.num_digits, secret.params.period, toString(secret.params.algorithm));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CStringDeleter { void operator()(char * ptr) const { std::free(ptr); } };
|
||||||
|
|
||||||
|
String getOneTimePassword(const String & secret [[ maybe_unused ]], const OneTimePasswordParams & config [[ maybe_unused ]], UInt64 current_time [[ maybe_unused ]])
|
||||||
|
{
|
||||||
|
#if USE_SSL
|
||||||
|
int sha_algo = config.algorithm == OneTimePasswordParams::Algorithm::SHA512 ? TOTP_SHA512
|
||||||
|
: config.algorithm == OneTimePasswordParams::Algorithm::SHA256 ? TOTP_SHA256
|
||||||
|
: TOTP_SHA1;
|
||||||
|
|
||||||
|
cotp_error_t error;
|
||||||
|
auto result = std::unique_ptr<char, CStringDeleter>(get_totp_at(secret.c_str(), current_time, config.num_digits, config.period, sha_algo, &error));
|
||||||
|
|
||||||
|
if (result == nullptr || (error != NO_ERROR && error != VALID))
|
||||||
|
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Error while retrieving one-time password, code: {}",
|
||||||
|
static_cast<std::underlying_type_t<cotp_error_t>>(error));
|
||||||
|
return String(result.get(), strlen(result.get()));
|
||||||
|
#else
|
||||||
|
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "One-time password support is disabled, because ClickHouse was built without openssl library");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool checkOneTimePassword(const String & password, const OneTimePasswordSecret & secret)
|
||||||
|
{
|
||||||
|
if (password.size() != static_cast<size_t>(secret.params.num_digits)
|
||||||
|
|| !std::all_of(password.begin(), password.end(), isdigit))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto current_time = static_cast<UInt64>(std::time(nullptr));
|
||||||
|
for (int delta : {0, -1, 1})
|
||||||
|
{
|
||||||
|
if (password == getOneTimePassword(secret.key, secret.params, current_time + delta * secret.params.period))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
src/Access/Common/OneTimePassword.h
Normal file
40
src/Access/Common/OneTimePassword.h
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <base/types.h>
|
||||||
|
|
||||||
|
namespace DB
|
||||||
|
{
|
||||||
|
|
||||||
|
struct OneTimePasswordParams
|
||||||
|
{
|
||||||
|
Int32 num_digits = 6;
|
||||||
|
Int32 period = 30;
|
||||||
|
|
||||||
|
enum class Algorithm : UInt8
|
||||||
|
{
|
||||||
|
SHA1,
|
||||||
|
SHA256,
|
||||||
|
SHA512,
|
||||||
|
} algorithm = Algorithm::SHA1;
|
||||||
|
|
||||||
|
explicit OneTimePasswordParams(Int32 num_digits_ = {}, Int32 period_ = {}, const String & algorithm_name_ = {});
|
||||||
|
|
||||||
|
bool operator==(const OneTimePasswordParams &) const = default;
|
||||||
|
|
||||||
|
std::string_view getAlgorithmName() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OneTimePasswordSecret
|
||||||
|
{
|
||||||
|
String key;
|
||||||
|
OneTimePasswordParams params;
|
||||||
|
|
||||||
|
explicit OneTimePasswordSecret(
|
||||||
|
const String & key_,
|
||||||
|
OneTimePasswordParams params_ = OneTimePasswordParams{});
|
||||||
|
};
|
||||||
|
|
||||||
|
String getOneTimePasswordSecretLink(const OneTimePasswordSecret & secret);
|
||||||
|
bool checkOneTimePassword(const String & password, const OneTimePasswordSecret & secret);
|
||||||
|
|
||||||
|
}
|
@ -7,6 +7,8 @@
|
|||||||
#include <Access/SettingsProfile.h>
|
#include <Access/SettingsProfile.h>
|
||||||
#include <Access/AccessControl.h>
|
#include <Access/AccessControl.h>
|
||||||
#include <Access/resolveSetting.h>
|
#include <Access/resolveSetting.h>
|
||||||
|
#include <Access/Common/AuthenticationType.h>
|
||||||
|
#include <Access/Common/OneTimePassword.h>
|
||||||
#include <Access/AccessChangesNotifier.h>
|
#include <Access/AccessChangesNotifier.h>
|
||||||
#include <Dictionaries/IDictionary.h>
|
#include <Dictionaries/IDictionary.h>
|
||||||
#include <Common/Config/ConfigReloader.h>
|
#include <Common/Config/ConfigReloader.h>
|
||||||
@ -127,6 +129,7 @@ namespace
|
|||||||
String user_config = "users." + user_name;
|
String user_config = "users." + user_name;
|
||||||
bool has_no_password = config.has(user_config + ".no_password");
|
bool has_no_password = config.has(user_config + ".no_password");
|
||||||
bool has_password_plaintext = config.has(user_config + ".password");
|
bool has_password_plaintext = config.has(user_config + ".password");
|
||||||
|
bool has_otp_secret = config.has(user_config + ".time_based_one_time_password");
|
||||||
bool has_password_sha256_hex = config.has(user_config + ".password_sha256_hex");
|
bool has_password_sha256_hex = config.has(user_config + ".password_sha256_hex");
|
||||||
bool has_password_double_sha1_hex = config.has(user_config + ".password_double_sha1_hex");
|
bool has_password_double_sha1_hex = config.has(user_config + ".password_double_sha1_hex");
|
||||||
bool has_ldap = config.has(user_config + ".ldap");
|
bool has_ldap = config.has(user_config + ".ldap");
|
||||||
@ -142,11 +145,11 @@ namespace
|
|||||||
bool has_http_auth = config.has(http_auth_config);
|
bool has_http_auth = config.has(http_auth_config);
|
||||||
|
|
||||||
size_t num_password_fields = has_no_password + has_password_plaintext + has_password_sha256_hex + has_password_double_sha1_hex
|
size_t num_password_fields = has_no_password + has_password_plaintext + has_password_sha256_hex + has_password_double_sha1_hex
|
||||||
+ has_ldap + has_kerberos + has_certificates + has_ssh_keys + has_http_auth;
|
+ has_ldap + has_kerberos + has_certificates + has_ssh_keys + has_http_auth + has_otp_secret;
|
||||||
|
|
||||||
if (num_password_fields > 1)
|
if (num_password_fields > 1)
|
||||||
throw Exception(ErrorCodes::BAD_ARGUMENTS, "More than one field of 'password', 'password_sha256_hex', "
|
throw Exception(ErrorCodes::BAD_ARGUMENTS, "More than one field of 'password', 'password_sha256_hex', "
|
||||||
"'password_double_sha1_hex', 'no_password', 'ldap', 'kerberos', 'ssl_certificates', 'ssh_keys', "
|
"'password_double_sha1_hex', 'no_password', 'time_based_one_time_password', 'ldap', 'kerberos', 'ssl_certificates', 'ssh_keys', "
|
||||||
"'http_authentication' are used to specify authentication info for user {}. "
|
"'http_authentication' are used to specify authentication info for user {}. "
|
||||||
"Must be only one of them.", user_name);
|
"Must be only one of them.", user_name);
|
||||||
|
|
||||||
@ -170,6 +173,15 @@ namespace
|
|||||||
user->authentication_methods.emplace_back(AuthenticationType::DOUBLE_SHA1_PASSWORD);
|
user->authentication_methods.emplace_back(AuthenticationType::DOUBLE_SHA1_PASSWORD);
|
||||||
user->authentication_methods.back().setPasswordHashHex(config.getString(user_config + ".password_double_sha1_hex"), validate);
|
user->authentication_methods.back().setPasswordHashHex(config.getString(user_config + ".password_double_sha1_hex"), validate);
|
||||||
}
|
}
|
||||||
|
else if (has_otp_secret)
|
||||||
|
{
|
||||||
|
String secret = config.getString(user_config + ".time_based_one_time_password.secret", "");
|
||||||
|
OneTimePasswordParams otp_config(
|
||||||
|
config.getInt(user_config + ".time_based_one_time_password.digits", {}),
|
||||||
|
config.getInt(user_config + ".time_based_one_time_password.period", {}),
|
||||||
|
config.getString(user_config + ".time_based_one_time_password.algorithm", {}));
|
||||||
|
user->authentication_methods.emplace_back(AuthenticationType::ONE_TIME_PASSWORD).setOneTimePassword(secret, otp_config, validate);
|
||||||
|
}
|
||||||
else if (has_ldap)
|
else if (has_ldap)
|
||||||
{
|
{
|
||||||
bool has_ldap_server = config.has(user_config + ".ldap.server");
|
bool has_ldap_server = config.has(user_config + ".ldap.server");
|
||||||
@ -269,7 +281,7 @@ namespace
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
user->authentication_methods.emplace_back();
|
user->authentication_methods.emplace_back(AuthenticationType::NO_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto & authentication_method : user->authentication_methods)
|
for (const auto & authentication_method : user->authentication_methods)
|
||||||
|
@ -85,6 +85,7 @@ ColumnsDescription SessionLogElement::getColumnsDescription()
|
|||||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::PLAINTEXT_PASSWORD),
|
AUTH_TYPE_NAME_AND_VALUE(AuthType::PLAINTEXT_PASSWORD),
|
||||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::SHA256_PASSWORD),
|
AUTH_TYPE_NAME_AND_VALUE(AuthType::SHA256_PASSWORD),
|
||||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::DOUBLE_SHA1_PASSWORD),
|
AUTH_TYPE_NAME_AND_VALUE(AuthType::DOUBLE_SHA1_PASSWORD),
|
||||||
|
AUTH_TYPE_NAME_AND_VALUE(AuthType::ONE_TIME_PASSWORD),
|
||||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::LDAP),
|
AUTH_TYPE_NAME_AND_VALUE(AuthType::LDAP),
|
||||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::JWT),
|
AUTH_TYPE_NAME_AND_VALUE(AuthType::JWT),
|
||||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::KERBEROS),
|
AUTH_TYPE_NAME_AND_VALUE(AuthType::KERBEROS),
|
||||||
@ -94,7 +95,7 @@ ColumnsDescription SessionLogElement::getColumnsDescription()
|
|||||||
AUTH_TYPE_NAME_AND_VALUE(AuthType::HTTP),
|
AUTH_TYPE_NAME_AND_VALUE(AuthType::HTTP),
|
||||||
});
|
});
|
||||||
#undef AUTH_TYPE_NAME_AND_VALUE
|
#undef AUTH_TYPE_NAME_AND_VALUE
|
||||||
static_assert(static_cast<int>(AuthenticationType::MAX) == 11);
|
static_assert(static_cast<int>(AuthenticationType::MAX) == 12);
|
||||||
|
|
||||||
auto interface_type_column = std::make_shared<DataTypeEnum8>(
|
auto interface_type_column = std::make_shared<DataTypeEnum8>(
|
||||||
DataTypeEnum8::Values
|
DataTypeEnum8::Values
|
||||||
|
@ -104,6 +104,12 @@ void ASTAuthenticationData::formatImpl(const FormatSettings & settings, FormatSt
|
|||||||
password = true;
|
password = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case AuthenticationType::ONE_TIME_PASSWORD:
|
||||||
|
{
|
||||||
|
prefix = "BY";
|
||||||
|
password = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case AuthenticationType::JWT:
|
case AuthenticationType::JWT:
|
||||||
{
|
{
|
||||||
prefix = "CLAIMS";
|
prefix = "CLAIMS";
|
||||||
|
@ -563,6 +563,7 @@ namespace DB
|
|||||||
MR_MACROS(DOUBLE_SHA1_PASSWORD, "DOUBLE_SHA1_PASSWORD") \
|
MR_MACROS(DOUBLE_SHA1_PASSWORD, "DOUBLE_SHA1_PASSWORD") \
|
||||||
MR_MACROS(IS_OBJECT_ID, "IS_OBJECT_ID") \
|
MR_MACROS(IS_OBJECT_ID, "IS_OBJECT_ID") \
|
||||||
MR_MACROS(NO_PASSWORD, "NO_PASSWORD") \
|
MR_MACROS(NO_PASSWORD, "NO_PASSWORD") \
|
||||||
|
MR_MACROS(ONE_TIME_PASSWORD, "ONE_TIME_PASSWORD") \
|
||||||
MR_MACROS(PART_MOVE_TO_SHARD, "PART_MOVE_TO_SHARD") \
|
MR_MACROS(PART_MOVE_TO_SHARD, "PART_MOVE_TO_SHARD") \
|
||||||
MR_MACROS(PLAINTEXT_PASSWORD, "PLAINTEXT_PASSWORD") \
|
MR_MACROS(PLAINTEXT_PASSWORD, "PLAINTEXT_PASSWORD") \
|
||||||
MR_MACROS(SHA256_HASH, "SHA256_HASH") \
|
MR_MACROS(SHA256_HASH, "SHA256_HASH") \
|
||||||
|
@ -149,6 +149,16 @@ void StorageSystemUsers::fillData(MutableColumns & res_columns, ContextPtr conte
|
|||||||
{
|
{
|
||||||
auth_params_json.set("realm", auth_data.getKerberosRealm());
|
auth_params_json.set("realm", auth_data.getKerberosRealm());
|
||||||
}
|
}
|
||||||
|
else if (auth_data.getType() == AuthenticationType::ONE_TIME_PASSWORD)
|
||||||
|
{
|
||||||
|
const auto & secret = auth_data.getOneTimePassword();
|
||||||
|
if (secret.params != OneTimePasswordParams{})
|
||||||
|
{
|
||||||
|
auth_params_json.set("algorithm", toString(secret.params.algorithm));
|
||||||
|
auth_params_json.set("num_digits", toString(secret.params.num_digits));
|
||||||
|
auth_params_json.set("period", toString(secret.params.period));
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (auth_data.getType() == AuthenticationType::SSL_CERTIFICATE)
|
else if (auth_data.getType() == AuthenticationType::SSL_CERTIFICATE)
|
||||||
{
|
{
|
||||||
Poco::JSON::Array::Ptr common_names = new Poco::JSON::Array();
|
Poco::JSON::Array::Ptr common_names = new Poco::JSON::Array();
|
||||||
|
@ -341,14 +341,15 @@
|
|||||||
"NULL"
|
"NULL"
|
||||||
"NULLS"
|
"NULLS"
|
||||||
"OFFSET"
|
"OFFSET"
|
||||||
"ON"
|
|
||||||
"ON DELETE"
|
"ON DELETE"
|
||||||
"ON UPDATE"
|
"ON UPDATE"
|
||||||
"ON VOLUME"
|
"ON VOLUME"
|
||||||
|
"ON"
|
||||||
|
"ONE_TIME_PASSWORD"
|
||||||
"ONLY"
|
"ONLY"
|
||||||
"OPTIMIZE TABLE"
|
"OPTIMIZE TABLE"
|
||||||
"OR"
|
|
||||||
"OR REPLACE"
|
"OR REPLACE"
|
||||||
|
"OR"
|
||||||
"ORDER BY"
|
"ORDER BY"
|
||||||
"OUTER"
|
"OUTER"
|
||||||
"OVER"
|
"OVER"
|
||||||
|
0
tests/integration/test_totp_auth/__init__.py
Normal file
0
tests/integration/test_totp_auth/__init__.py
Normal file
0
tests/integration/test_totp_auth/config/.gitkeep
Normal file
0
tests/integration/test_totp_auth/config/.gitkeep
Normal file
150
tests/integration/test_totp_auth/test_totp.py
Normal file
150
tests/integration/test_totp_auth/test_totp.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from helpers.cluster import ClickHouseCluster
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
TOTP_SECRET = base64.b32encode(random.randbytes(random.randint(3, 64))).decode()
|
||||||
|
|
||||||
|
cluster = ClickHouseCluster(__file__)
|
||||||
|
node = cluster.add_instance(
|
||||||
|
"node",
|
||||||
|
user_configs=["config/users.xml"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_config(totp_secret):
|
||||||
|
config = f"""
|
||||||
|
<clickhouse>
|
||||||
|
<profiles>
|
||||||
|
<default>
|
||||||
|
</default>
|
||||||
|
</profiles>
|
||||||
|
<users>
|
||||||
|
<totuser>
|
||||||
|
<time_based_one_time_password>
|
||||||
|
<secret>{totp_secret}</secret>
|
||||||
|
<period>10</period>
|
||||||
|
<digits>9</digits>
|
||||||
|
<algorithm>SHA256</algorithm>
|
||||||
|
</time_based_one_time_password>
|
||||||
|
|
||||||
|
<access_management>1</access_management>
|
||||||
|
<networks replace="replace">
|
||||||
|
<ip>::/0</ip>
|
||||||
|
</networks>
|
||||||
|
<profile>default</profile>
|
||||||
|
<quota>default</quota>
|
||||||
|
</totuser>
|
||||||
|
</users>
|
||||||
|
</clickhouse>
|
||||||
|
""".lstrip()
|
||||||
|
|
||||||
|
with open(os.path.join(SCRIPT_DIR, "config/users.xml"), "w") as f:
|
||||||
|
f.write(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def started_cluster():
|
||||||
|
try:
|
||||||
|
create_config(TOTP_SECRET)
|
||||||
|
cluster.start()
|
||||||
|
yield cluster
|
||||||
|
finally:
|
||||||
|
cluster.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_totp(
|
||||||
|
secret, interval=30, digits=6, sha_version=hashlib.sha1, timepoint=None
|
||||||
|
):
|
||||||
|
key = base64.b32decode(secret, casefold=True)
|
||||||
|
time_step = int(timepoint or time.time() / interval)
|
||||||
|
msg = struct.pack(">Q", time_step)
|
||||||
|
hmac_hash = hmac.new(key, msg, sha_version).digest()
|
||||||
|
offset = hmac_hash[-1] & 0x0F
|
||||||
|
binary_code = struct.unpack(">I", hmac_hash[offset : offset + 4])[0] & 0x7FFFFFFF
|
||||||
|
otp = binary_code % (10**digits)
|
||||||
|
return f"{otp:0{digits}d}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_time_password(started_cluster):
|
||||||
|
query_text = "SELECT currentUser() || toString(42)"
|
||||||
|
totuser_secret = {
|
||||||
|
"secret": TOTP_SECRET,
|
||||||
|
"interval": 10,
|
||||||
|
"digits": 9,
|
||||||
|
"sha_version": hashlib.sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
old_password = generate_totp(
|
||||||
|
**totuser_secret, timepoint=time.time() - 3 * totuser_secret["interval"]
|
||||||
|
)
|
||||||
|
assert "AUTHENTICATION_FAILED" in node.query_and_get_error(
|
||||||
|
query_text, user="totuser", password=old_password
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "totuser42\n" == node.query(
|
||||||
|
query_text, user="totuser", password=generate_totp(**totuser_secret)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "totuser42\n" == node.query(
|
||||||
|
query_text, user="totuser", password=generate_totp(**totuser_secret)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "CREATE USER totuser IDENTIFIED WITH one_time_password" in node.query(
|
||||||
|
"SHOW CREATE USER totuser",
|
||||||
|
user="totuser",
|
||||||
|
password=generate_totp(**totuser_secret),
|
||||||
|
)
|
||||||
|
|
||||||
|
for bad_secret, error_message in [
|
||||||
|
("i11egalbase32", "Invalid character in*secret"),
|
||||||
|
("abc$d", "Invalid character in*secret"),
|
||||||
|
(" ", "Empty secret"),
|
||||||
|
(" =", "Empty secret"),
|
||||||
|
("", "Empty secret"),
|
||||||
|
]:
|
||||||
|
err_resp = node.query_and_get_error(
|
||||||
|
f"CREATE USER user2 IDENTIFIED WITH one_time_password BY '{bad_secret}'",
|
||||||
|
user="totuser",
|
||||||
|
password=generate_totp(**totuser_secret),
|
||||||
|
)
|
||||||
|
assert fnmatch(err_resp, f"*{error_message}*BAD_ARGUMENTS*"), err_resp
|
||||||
|
|
||||||
|
# lowercase secret with spaces is allowed
|
||||||
|
node.query(
|
||||||
|
"CREATE USER user2 IDENTIFIED WITH one_time_password BY 'inwg sy3l jbxx k43f biaa'",
|
||||||
|
user="totuser",
|
||||||
|
password=generate_totp(**totuser_secret),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "user242\n" == node.query(
|
||||||
|
query_text,
|
||||||
|
user="user2",
|
||||||
|
password=generate_totp("INWGSY3LJBXXK43FBIAA===="),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = node.query(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
name,
|
||||||
|
auth_type[1],
|
||||||
|
JSONExtractString(auth_params[1], 'algorithm'),
|
||||||
|
JSONExtractString(auth_params[1], 'num_digits'),
|
||||||
|
JSONExtractString(auth_params[1], 'period')
|
||||||
|
FROM system.users WHERE name IN ('totuser', 'user2')
|
||||||
|
ORDER BY 1
|
||||||
|
""",
|
||||||
|
user="totuser",
|
||||||
|
password=generate_totp(**totuser_secret),
|
||||||
|
).splitlines()
|
||||||
|
assert resp[0].startswith("totuser\tone_time_password\tSHA256\t9\t10"), resp
|
||||||
|
assert resp[1].startswith("user2\tone_time_password"), resp
|
@ -1150,7 +1150,7 @@ CREATE TABLE system.users
|
|||||||
`name` String,
|
`name` String,
|
||||||
`id` UUID,
|
`id` UUID,
|
||||||
`storage` String,
|
`storage` String,
|
||||||
`auth_type` Array(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_type` Array(Enum8('no_password' = 0, 'plaintext_password' = 1, 'sha256_password' = 2, 'double_sha1_password' = 3, 'one_time_password' = 4, 'ldap' = 5, 'kerberos' = 6, 'ssl_certificate' = 7, 'bcrypt_password' = 8, 'ssh_key' = 9, 'http' = 10, 'jwt' = 11)),
|
||||||
`auth_params` Array(String),
|
`auth_params` Array(String),
|
||||||
`host_ip` Array(String),
|
`host_ip` Array(String),
|
||||||
`host_names` Array(String),
|
`host_names` Array(String),
|
||||||
|
@ -16,12 +16,12 @@ user
|
|||||||
MySQL: Authentication failed
|
MySQL: Authentication failed
|
||||||
default
|
default
|
||||||
password
|
password
|
||||||
default: Authentication failed: password is incorrect, or there is no user with such name.
|
default: Authentication failed: password is incorrect, or there is no user with such name
|
||||||
default
|
default
|
||||||
history_file
|
history_file
|
||||||
Cannot create file: /no/such/dir/.history
|
Cannot create file: /no/such/dir/.history
|
||||||
root overrides
|
root overrides
|
||||||
foo: Authentication failed: password is incorrect, or there is no user with such name.
|
foo: Authentication failed: password is incorrect, or there is no user with such name
|
||||||
default
|
default
|
||||||
default
|
default
|
||||||
foo: Authentication failed: password is incorrect, or there is no user with such name.
|
foo: Authentication failed: password is incorrect, or there is no user with such name
|
||||||
|
@ -117,7 +117,7 @@ echo 'user'
|
|||||||
$CLICKHOUSE_CLIENT --config $CONFIG --connection test_user -q 'select currentUser()' |& grep -F -o 'MySQL: Authentication failed'
|
$CLICKHOUSE_CLIENT --config $CONFIG --connection test_user -q 'select currentUser()' |& grep -F -o 'MySQL: Authentication failed'
|
||||||
$CLICKHOUSE_CLIENT --config $CONFIG --connection test_user --user default -q 'select currentUser()'
|
$CLICKHOUSE_CLIENT --config $CONFIG --connection test_user --user default -q 'select currentUser()'
|
||||||
echo 'password'
|
echo 'password'
|
||||||
$CLICKHOUSE_CLIENT --config $CONFIG --connection test_password -q 'select currentUser()' |& grep -F -o 'default: Authentication failed: password is incorrect, or there is no user with such name.'
|
$CLICKHOUSE_CLIENT --config $CONFIG --connection test_password -q 'select currentUser()' |& grep -F -o 'default: Authentication failed: password is incorrect, or there is no user with such name'
|
||||||
$CLICKHOUSE_CLIENT --config $CONFIG --connection test_password --password "" -q 'select currentUser()'
|
$CLICKHOUSE_CLIENT --config $CONFIG --connection test_password --password "" -q 'select currentUser()'
|
||||||
echo 'history_file'
|
echo 'history_file'
|
||||||
$CLICKHOUSE_CLIENT --progress off --interactive --config $CONFIG --connection test_history_file -q 'select 1' </dev/null |& grep -F -o 'Cannot create file: /no/such/dir/.history'
|
$CLICKHOUSE_CLIENT --progress off --interactive --config $CONFIG --connection test_history_file -q 'select 1' </dev/null |& grep -F -o 'Cannot create file: /no/such/dir/.history'
|
||||||
@ -126,9 +126,9 @@ $CLICKHOUSE_CLIENT --progress off --interactive --config $CONFIG --connection te
|
|||||||
unset CLICKHOUSE_USER
|
unset CLICKHOUSE_USER
|
||||||
unset CLICKHOUSE_PASSWORD
|
unset CLICKHOUSE_PASSWORD
|
||||||
echo 'root overrides'
|
echo 'root overrides'
|
||||||
$CLICKHOUSE_CLIENT --config $CONFIG_ROOT_OVERRIDES --connection incorrect_auth -q 'select currentUser()' |& grep -F -o 'foo: Authentication failed: password is incorrect, or there is no user with such name.'
|
$CLICKHOUSE_CLIENT --config $CONFIG_ROOT_OVERRIDES --connection incorrect_auth -q 'select currentUser()' |& grep -F -o 'foo: Authentication failed: password is incorrect, or there is no user with such name'
|
||||||
$CLICKHOUSE_CLIENT --config $CONFIG_ROOT_OVERRIDES --connection incorrect_auth --user "default" --password "" -q 'select currentUser()'
|
$CLICKHOUSE_CLIENT --config $CONFIG_ROOT_OVERRIDES --connection incorrect_auth --user "default" --password "" -q 'select currentUser()'
|
||||||
$CLICKHOUSE_CLIENT --config $CONFIG_ROOT_OVERRIDES --connection default -q 'select currentUser()'
|
$CLICKHOUSE_CLIENT --config $CONFIG_ROOT_OVERRIDES --connection default -q 'select currentUser()'
|
||||||
$CLICKHOUSE_CLIENT --config $CONFIG_ROOT_OVERRIDES --connection default --user foo -q 'select currentUser()' |& grep -F -o 'foo: Authentication failed: password is incorrect, or there is no user with such name.'
|
$CLICKHOUSE_CLIENT --config $CONFIG_ROOT_OVERRIDES --connection default --user foo -q 'select currentUser()' |& grep -F -o 'foo: Authentication failed: password is incorrect, or there is no user with such name'
|
||||||
|
|
||||||
rm -f "${CONFIG:?}"
|
rm -f "${CONFIG:?}"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
personal_ws-1.1 en 2984
|
personal_ws-1.1 en 3156
|
||||||
AArch
|
AArch
|
||||||
ACLs
|
ACLs
|
||||||
ALTERs
|
ALTERs
|
||||||
@ -1020,6 +1020,7 @@ TINYINT
|
|||||||
TLSv
|
TLSv
|
||||||
TMAX
|
TMAX
|
||||||
TMIN
|
TMIN
|
||||||
|
TOTP
|
||||||
TPCH
|
TPCH
|
||||||
TSDB
|
TSDB
|
||||||
TSVRaw
|
TSVRaw
|
||||||
@ -1106,7 +1107,6 @@ URLHash
|
|||||||
URLHierarchy
|
URLHierarchy
|
||||||
URLPathHierarchy
|
URLPathHierarchy
|
||||||
USearch
|
USearch
|
||||||
USearch
|
|
||||||
UTCTimestamp
|
UTCTimestamp
|
||||||
UUIDNumToString
|
UUIDNumToString
|
||||||
UUIDStringToNum
|
UUIDStringToNum
|
||||||
@ -2469,6 +2469,7 @@ punycodeEncode
|
|||||||
pushdown
|
pushdown
|
||||||
pwrite
|
pwrite
|
||||||
py
|
py
|
||||||
|
qrencode
|
||||||
qryn
|
qryn
|
||||||
quantile
|
quantile
|
||||||
quantileBFloat
|
quantileBFloat
|
||||||
@ -2977,6 +2978,7 @@ topLevelDomain
|
|||||||
topLevelDomainRFC
|
topLevelDomainRFC
|
||||||
topk
|
topk
|
||||||
topkweighted
|
topkweighted
|
||||||
|
totp
|
||||||
tpcds
|
tpcds
|
||||||
tpch
|
tpch
|
||||||
transactionID
|
transactionID
|
||||||
|
Loading…
Reference in New Issue
Block a user