Merge pull request #51261 from evillique/users-valid-until

Add VALID UNTIL clause for users
This commit is contained in:
Vitaly Baranov 2023-09-22 12:38:30 +02:00 committed by GitHub
commit 3a2ca1f69c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 183 additions and 4 deletions

View File

@ -14,6 +14,7 @@ ALTER USER [IF EXISTS] name1 [ON CLUSTER cluster_name1] [RENAME TO new_name1]
[, name2 [ON CLUSTER cluster_name2] [RENAME TO new_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'}]
[[ADD | DROP] HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
[VALID UNTIL datetime]
[DEFAULT ROLE role [,...] | ALL | ALL EXCEPT role [,...] ]
[GRANTEES {user | role | ANY | NONE} [,...] [EXCEPT {user | role} [,...]]]
[SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [READONLY | WRITABLE] | PROFILE 'profile_name'] [,...]

View File

@ -14,6 +14,7 @@ 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'}]
[HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
[VALID UNTIL datetime]
[IN access_storage_type]
[DEFAULT ROLE role [,...]]
[DEFAULT DATABASE database | NONE]
@ -162,6 +163,16 @@ Another way of specifying host is to use `@` syntax following the username. Exam
ClickHouse treats `user_name@'address'` as a username as a whole. Thus, technically you can create multiple users with the same `user_name` and different constructions after `@`. However, we do not recommend to do so.
:::
## VALID UNTIL Clause
Allows you to specify the expiration date and, optionally, the time for user credentials. It accepts a string as a parameter. It is recommended to use the `YYYY-MM-DD [hh:mm:ss] [timezone]` format for datetime. By default, this parameter equals `'infinity'`.
Examples:
- `CREATE USER name1 VALID UNTIL '2025-01-01'`
- `CREATE USER name1 VALID UNTIL '2025-01-01 12:00:00 UTC'`
- `CREATE USER name1 VALID UNTIL 'infinity'`
## GRANTEES Clause
Specifies users or roles which are allowed to receive [privileges](../../../sql-reference/statements/grant.md#grant-privileges) from this user on the condition this user has also all required access granted with [GRANT OPTION](../../../sql-reference/statements/grant.md#grant-privigele-syntax). Options of the `GRANTEES` clause:

View File

@ -556,6 +556,14 @@ bool IAccessStorage::areCredentialsValid(
if (credentials.getUserName() != user.getName())
return false;
if (user.valid_until)
{
const time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
if (now > user.valid_until)
return false;
}
return Authentication::areCredentialsValid(credentials, user.auth_data, external_authenticators);
}

View File

@ -17,7 +17,8 @@ bool User::equal(const IAccessEntity & other) const
const auto & other_user = typeid_cast<const User &>(other);
return (auth_data == other_user.auth_data) && (allowed_client_hosts == other_user.allowed_client_hosts)
&& (access == other_user.access) && (granted_roles == other_user.granted_roles) && (default_roles == other_user.default_roles)
&& (settings == other_user.settings) && (grantees == other_user.grantees) && (default_database == other_user.default_database);
&& (settings == other_user.settings) && (grantees == other_user.grantees) && (default_database == other_user.default_database)
&& (valid_until == other_user.valid_until);
}
void User::setName(const String & name_)

View File

@ -23,6 +23,7 @@ struct User : public IAccessEntity
SettingsProfileElements settings;
RolesOrUsersSet grantees = RolesOrUsersSet::AllTag{};
String default_database;
time_t valid_until = 0;
bool equal(const IAccessEntity & other) const override;
std::shared_ptr<IAccessEntity> clone() const override { return cloneImpl<User>(); }

View File

@ -14,6 +14,10 @@
#include <Parsers/Access/ASTRolesOrUsersSet.h>
#include <Parsers/Access/ASTUserNameWithHost.h>
#include <boost/range/algorithm/copy.hpp>
#include <Interpreters/evaluateConstantExpression.h>
#include <Storages/checkAndGetLiteralArgument.h>
#include <IO/parseDateTimeBestEffort.h>
#include <IO/ReadBufferFromString.h>
namespace DB
@ -33,6 +37,7 @@ namespace
const std::optional<RolesOrUsersSet> & override_default_roles,
const std::optional<SettingsProfileElements> & override_settings,
const std::optional<RolesOrUsersSet> & override_grantees,
const std::optional<time_t> & valid_until,
bool allow_implicit_no_password,
bool allow_no_password,
bool allow_plaintext_password)
@ -66,6 +71,9 @@ namespace
}
}
if (valid_until)
user.valid_until = *valid_until;
if (override_name && !override_name->host_pattern.empty())
{
user.allowed_client_hosts = AllowedClientHosts{};
@ -123,6 +131,26 @@ BlockIO InterpreterCreateUserQuery::execute()
if (query.auth_data)
auth_data = AuthenticationData::fromAST(*query.auth_data, getContext(), !query.attach);
std::optional<time_t> valid_until;
if (query.valid_until)
{
const ASTPtr valid_until_literal = evaluateConstantExpressionAsLiteral(query.valid_until, getContext());
const String valid_until_str = checkAndGetLiteralArgument<String>(valid_until_literal, "valid_until");
time_t time = 0;
if (valid_until_str != "infinity")
{
const auto & time_zone = DateLUT::instance("");
const auto & utc_time_zone = DateLUT::instance("UTC");
ReadBufferFromString in(valid_until_str);
parseDateTimeBestEffort(time, in, time_zone, utc_time_zone);
}
valid_until = time;
}
std::optional<RolesOrUsersSet> default_roles_from_query;
if (query.default_roles)
{
@ -165,7 +193,9 @@ BlockIO InterpreterCreateUserQuery::execute()
auto update_func = [&](const AccessEntityPtr & entity) -> AccessEntityPtr
{
auto updated_user = typeid_cast<std::shared_ptr<User>>(entity->clone());
updateUserFromQueryImpl(*updated_user, query, auth_data, {}, default_roles_from_query, settings_from_query, grantees_from_query, implicit_no_password_allowed, no_password_allowed, plaintext_password_allowed);
updateUserFromQueryImpl(
*updated_user, query, auth_data, {}, default_roles_from_query, settings_from_query, grantees_from_query,
valid_until, implicit_no_password_allowed, no_password_allowed, plaintext_password_allowed);
return updated_user;
};
@ -183,7 +213,9 @@ BlockIO InterpreterCreateUserQuery::execute()
for (const auto & name : *query.names)
{
auto new_user = std::make_shared<User>();
updateUserFromQueryImpl(*new_user, query, auth_data, name, default_roles_from_query, settings_from_query, RolesOrUsersSet::AllTag{}, implicit_no_password_allowed, no_password_allowed, plaintext_password_allowed);
updateUserFromQueryImpl(
*new_user, query, auth_data, name, default_roles_from_query, settings_from_query, RolesOrUsersSet::AllTag{},
valid_until, implicit_no_password_allowed, no_password_allowed, plaintext_password_allowed);
new_users.emplace_back(std::move(new_user));
}
@ -226,7 +258,7 @@ void InterpreterCreateUserQuery::updateUserFromQuery(User & user, const ASTCreat
if (query.auth_data)
auth_data = AuthenticationData::fromAST(*query.auth_data, {}, !query.attach);
updateUserFromQueryImpl(user, query, auth_data, {}, {}, {}, {}, allow_no_password, allow_plaintext_password, true);
updateUserFromQueryImpl(user, query, auth_data, {}, {}, {}, {}, {}, allow_no_password, allow_plaintext_password, true);
}
}

View File

@ -10,6 +10,7 @@
#include <Parsers/Access/ASTRolesOrUsersSet.h>
#include <Parsers/Access/ASTSettingsProfileElement.h>
#include <Parsers/Access/ASTRowPolicyName.h>
#include <Parsers/ASTLiteral.h>
#include <Parsers/ExpressionListParsers.h>
#include <Parsers/formatAST.h>
#include <Parsers/parseQuery.h>
@ -65,6 +66,13 @@ namespace
if (user.auth_data.getType() != AuthenticationType::NO_PASSWORD)
query->auth_data = user.auth_data.toAST();
if (user.valid_until)
{
WriteBufferFromOwnString out;
writeDateTimeText(user.valid_until, out);
query->valid_until = std::make_shared<ASTLiteral>(out.str());
}
if (!user.settings.empty())
{
if (attach_mode)

View File

@ -24,6 +24,11 @@ namespace
auth_data.format(settings);
}
void formatValidUntil(const IAST & valid_until, const IAST::FormatSettings & settings)
{
settings.ostr << (settings.hilite ? IAST::hilite_keyword : "") << " VALID UNTIL " << (settings.hilite ? IAST::hilite_none : "");
valid_until.format(settings);
}
void formatHosts(const char * prefix, const AllowedClientHosts & hosts, const IAST::FormatSettings & settings)
{
@ -221,6 +226,9 @@ void ASTCreateUserQuery::formatImpl(const FormatSettings & format, FormatState &
if (auth_data)
formatAuthenticationData(*auth_data, format);
if (valid_until)
formatValidUntil(*valid_until, format);
if (hosts)
formatHosts(nullptr, *hosts, format);
if (add_hosts)

View File

@ -59,6 +59,8 @@ public:
std::shared_ptr<ASTDatabaseOrNone> default_database;
ASTPtr valid_until;
String getID(char) const override;
ASTPtr clone() const override;
void formatImpl(const FormatSettings & format, FormatState &, FormatStateStacked) const override;

View File

@ -364,6 +364,19 @@ namespace
return true;
});
}
bool parseValidUntil(IParserBase::Pos & pos, Expected & expected, ASTPtr & valid_until)
{
return IParserBase::wrapParseImpl(pos, [&]
{
if (!ParserKeyword{"VALID UNTIL"}.ignore(pos, expected))
return false;
ParserStringAndSubstitution until_p;
return until_p.parse(pos, valid_until, expected);
});
}
}
@ -414,6 +427,7 @@ bool ParserCreateUserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expec
std::shared_ptr<ASTSettingsProfileElements> settings;
std::shared_ptr<ASTRolesOrUsersSet> grantees;
std::shared_ptr<ASTDatabaseOrNone> default_database;
ASTPtr valid_until;
String cluster;
String storage_name;
@ -429,6 +443,11 @@ bool ParserCreateUserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expec
}
}
if (!valid_until)
{
parseValidUntil(pos, expected, valid_until);
}
AllowedClientHosts new_hosts;
if (parseHosts(pos, expected, "", new_hosts))
{
@ -519,11 +538,15 @@ bool ParserCreateUserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expec
query->settings = std::move(settings);
query->grantees = std::move(grantees);
query->default_database = std::move(default_database);
query->valid_until = std::move(valid_until);
query->storage_name = std::move(storage_name);
if (query->auth_data)
query->children.push_back(query->auth_data);
if (query->valid_until)
query->children.push_back(query->valid_until);
return true;
}
}

View File

@ -0,0 +1,84 @@
import pytest
from datetime import datetime, timedelta
from time import sleep
from helpers.cluster import ClickHouseCluster
cluster = ClickHouseCluster(__file__)
node = cluster.add_instance("node")
@pytest.fixture(scope="module")
def started_cluster():
try:
cluster.start()
yield cluster
finally:
cluster.shutdown()
def test_basic(started_cluster):
# 1. Without VALID UNTIL
node.query("CREATE USER user_basic")
assert node.query("SHOW CREATE USER user_basic") == "CREATE USER user_basic\n"
assert node.query("SELECT 1", user="user_basic") == "1\n"
# 2. With valid VALID UNTIL
node.query("ALTER USER user_basic VALID UNTIL '06/11/2040 08:03:20 Z+3'")
assert (
node.query("SHOW CREATE USER user_basic")
== "CREATE USER user_basic VALID UNTIL \\'2040-11-06 05:03:20\\'\n"
)
assert node.query("SELECT 1", user="user_basic") == "1\n"
# 3. With expired VALID UNTIL
node.query("ALTER USER user_basic VALID UNTIL '06/11/2010 08:03:20 Z+3'")
assert (
node.query("SHOW CREATE USER user_basic")
== "CREATE USER user_basic VALID UNTIL \\'2010-11-06 05:03:20\\'\n"
)
error = "Authentication failed"
assert error in node.query_and_get_error("SELECT 1", user="user_basic")
# 4. Reset VALID UNTIL
node.query("ALTER USER user_basic VALID UNTIL 'infinity'")
assert node.query("SHOW CREATE USER user_basic") == "CREATE USER user_basic\n"
assert node.query("SELECT 1", user="user_basic") == "1\n"
node.query("DROP USER user_basic")
# 5. Make VALID UNTIL expire
until_datetime = datetime.today() + timedelta(0, 10)
until_string = until_datetime.strftime("%Y-%m-%d %H:%M:%S")
node.query(f"CREATE USER user_basic VALID UNTIL '{until_string}'")
assert node.query("SELECT 1", user="user_basic") == "1\n"
sleep(12)
error = "Authentication failed"
assert error in node.query_and_get_error("SELECT 1", user="user_basic")
def test_details(started_cluster):
# 1. Does not do anything
node.query("CREATE USER user_details_infinity VALID UNTIL 'infinity'")
assert (
node.query("SHOW CREATE USER user_details_infinity")
== "CREATE USER user_details_infinity\n"
)
# 2. Time only is not supported
node.query("CREATE USER user_details_time_only VALID UNTIL '22:03:40'")
assert (
node.query("SHOW CREATE USER user_details_time_only")
== "CREATE USER user_details_time_only VALID UNTIL \\'2023-01-01 22:03:40\\'\n"
)