Add VALID UNTIL clause for users

This commit is contained in:
Nikolay Degterinsky 2023-06-22 01:35:11 +00:00
parent c15e7b93cb
commit 0d2b9fd0e5
11 changed files with 166 additions and 4 deletions

View File

@ -14,6 +14,7 @@ CREATE USER [IF NOT EXISTS | OR REPLACE] name1 [ON CLUSTER cluster_name1]
[, name2 [ON CLUSTER cluster_name2] ...] [, 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'}] [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] [HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
[VALID UNTIL datetime]
[DEFAULT ROLE role [,...]] [DEFAULT ROLE role [,...]]
[DEFAULT DATABASE database | NONE] [DEFAULT DATABASE database | NONE]
[GRANTEES {user | role | ANY | NONE} [,...] [EXCEPT {user | role} [,...]]] [GRANTEES {user | role | ANY | NONE} [,...] [EXCEPT {user | role} [,...]]]
@ -135,6 +136,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. 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 a user. It accepts a string as a parameter. It is recommended to use the `YYYY-MM-DD [hh:mm:ss] [timezone]` format for datetime.
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 ## 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: 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

@ -514,6 +514,14 @@ bool IAccessStorage::areCredentialsValid(
if (credentials.getUserName() != user.getName()) if (credentials.getUserName() != user.getName())
return false; 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); 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); const auto & other_user = typeid_cast<const User &>(other);
return (auth_data == other_user.auth_data) && (allowed_client_hosts == other_user.allowed_client_hosts) 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) && (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_) void User::setName(const String & name_)

View File

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

View File

@ -10,6 +10,10 @@
#include <Interpreters/Context.h> #include <Interpreters/Context.h>
#include <Interpreters/executeDDLQueryOnCluster.h> #include <Interpreters/executeDDLQueryOnCluster.h>
#include <boost/range/algorithm/copy.hpp> #include <boost/range/algorithm/copy.hpp>
#include <Interpreters/evaluateConstantExpression.h>
#include <Storages/checkAndGetLiteralArgument.h>
#include <IO/parseDateTimeBestEffort.h>
#include <IO/ReadBufferFromString.h>
namespace DB namespace DB
@ -28,6 +32,7 @@ namespace
const std::optional<RolesOrUsersSet> & override_default_roles, const std::optional<RolesOrUsersSet> & override_default_roles,
const std::optional<SettingsProfileElements> & override_settings, const std::optional<SettingsProfileElements> & override_settings,
const std::optional<RolesOrUsersSet> & override_grantees, const std::optional<RolesOrUsersSet> & override_grantees,
const std::optional<time_t> & valid_until,
bool allow_implicit_no_password, bool allow_implicit_no_password,
bool allow_no_password, bool allow_no_password,
bool allow_plaintext_password) bool allow_plaintext_password)
@ -61,6 +66,9 @@ namespace
} }
} }
if (valid_until)
user.valid_until = *valid_until;
if (override_name && !override_name->host_pattern.empty()) if (override_name && !override_name->host_pattern.empty())
{ {
user.allowed_client_hosts = AllowedClientHosts{}; user.allowed_client_hosts = AllowedClientHosts{};
@ -116,6 +124,26 @@ BlockIO InterpreterCreateUserQuery::execute()
if (query.auth_data) if (query.auth_data)
auth_data = AuthenticationData::fromAST(*query.auth_data, getContext(), !query.attach); 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; std::optional<RolesOrUsersSet> default_roles_from_query;
if (query.default_roles) if (query.default_roles)
{ {
@ -148,7 +176,9 @@ BlockIO InterpreterCreateUserQuery::execute()
auto update_func = [&](const AccessEntityPtr & entity) -> AccessEntityPtr auto update_func = [&](const AccessEntityPtr & entity) -> AccessEntityPtr
{ {
auto updated_user = typeid_cast<std::shared_ptr<User>>(entity->clone()); 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; return updated_user;
}; };
@ -167,7 +197,9 @@ BlockIO InterpreterCreateUserQuery::execute()
for (const auto & name : *query.names) for (const auto & name : *query.names)
{ {
auto new_user = std::make_shared<User>(); 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)); new_users.emplace_back(std::move(new_user));
} }
@ -201,7 +233,7 @@ void InterpreterCreateUserQuery::updateUserFromQuery(User & user, const ASTCreat
if (query.auth_data) if (query.auth_data)
auth_data = AuthenticationData::fromAST(*query.auth_data, {}, !query.attach); 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/ASTRolesOrUsersSet.h>
#include <Parsers/Access/ASTSettingsProfileElement.h> #include <Parsers/Access/ASTSettingsProfileElement.h>
#include <Parsers/Access/ASTRowPolicyName.h> #include <Parsers/Access/ASTRowPolicyName.h>
#include <Parsers/ASTLiteral.h>
#include <Parsers/ExpressionListParsers.h> #include <Parsers/ExpressionListParsers.h>
#include <Parsers/formatAST.h> #include <Parsers/formatAST.h>
#include <Parsers/parseQuery.h> #include <Parsers/parseQuery.h>
@ -65,6 +66,13 @@ namespace
if (user.auth_data.getType() != AuthenticationType::NO_PASSWORD) if (user.auth_data.getType() != AuthenticationType::NO_PASSWORD)
query->auth_data = user.auth_data.toAST(); 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 (!user.settings.empty())
{ {
if (attach_mode) if (attach_mode)

View File

@ -24,6 +24,11 @@ namespace
auth_data.format(settings); 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) void formatHosts(const char * prefix, const AllowedClientHosts & hosts, const IAST::FormatSettings & settings)
{ {
@ -216,6 +221,9 @@ void ASTCreateUserQuery::formatImpl(const FormatSettings & format, FormatState &
if (auth_data) if (auth_data)
formatAuthenticationData(*auth_data, format); formatAuthenticationData(*auth_data, format);
if (valid_until)
formatValidUntil(*valid_until, format);
if (hosts) if (hosts)
formatHosts(nullptr, *hosts, format); formatHosts(nullptr, *hosts, format);
if (add_hosts) if (add_hosts)

View File

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

View File

@ -363,6 +363,19 @@ namespace
return true; 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);
});
}
} }
@ -413,6 +426,7 @@ bool ParserCreateUserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expec
std::shared_ptr<ASTSettingsProfileElements> settings; std::shared_ptr<ASTSettingsProfileElements> settings;
std::shared_ptr<ASTRolesOrUsersSet> grantees; std::shared_ptr<ASTRolesOrUsersSet> grantees;
std::shared_ptr<ASTDatabaseOrNone> default_database; std::shared_ptr<ASTDatabaseOrNone> default_database;
ASTPtr valid_until;
String cluster; String cluster;
while (true) while (true)
@ -427,6 +441,11 @@ bool ParserCreateUserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expec
} }
} }
if (!valid_until)
{
parseValidUntil(pos, expected, valid_until);
}
AllowedClientHosts new_hosts; AllowedClientHosts new_hosts;
if (parseHosts(pos, expected, "", new_hosts)) if (parseHosts(pos, expected, "", new_hosts))
{ {
@ -514,10 +533,14 @@ bool ParserCreateUserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expec
query->settings = std::move(settings); query->settings = std::move(settings);
query->grantees = std::move(grantees); query->grantees = std::move(grantees);
query->default_database = std::move(default_database); query->default_database = std::move(default_database);
query->valid_until = std::move(valid_until);
if (query->auth_data) if (query->auth_data)
query->children.push_back(query->auth_data); query->children.push_back(query->auth_data);
if (query->valid_until)
query->children.push_back(query->valid_until);
return true; return true;
} }
} }

View File

@ -0,0 +1,68 @@
import pytest
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 invalid 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"
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 \\'2000-01-01 22:03:40\\'\n"
)