diff --git a/docs/en/sql-reference/statements/create/user.md b/docs/en/sql-reference/statements/create/user.md index d168be63c36..b32fa8dbeb0 100644 --- a/docs/en/sql-reference/statements/create/user.md +++ b/docs/en/sql-reference/statements/create/user.md @@ -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] [DEFAULT ROLE role [,...]] [DEFAULT DATABASE database | NONE] [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. ::: +## 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 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: diff --git a/src/Access/IAccessStorage.cpp b/src/Access/IAccessStorage.cpp index 9468e8d220a..cb628c3e559 100644 --- a/src/Access/IAccessStorage.cpp +++ b/src/Access/IAccessStorage.cpp @@ -514,6 +514,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); } diff --git a/src/Access/User.cpp b/src/Access/User.cpp index c5750cdcd6c..3b4055b6b1d 100644 --- a/src/Access/User.cpp +++ b/src/Access/User.cpp @@ -17,7 +17,8 @@ bool User::equal(const IAccessEntity & other) const const auto & other_user = typeid_cast(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_) diff --git a/src/Access/User.h b/src/Access/User.h index 4b4bf90137f..e4ab654dafd 100644 --- a/src/Access/User.h +++ b/src/Access/User.h @@ -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 clone() const override { return cloneImpl(); } diff --git a/src/Interpreters/Access/InterpreterCreateUserQuery.cpp b/src/Interpreters/Access/InterpreterCreateUserQuery.cpp index 165937560cc..fa68b1adc1a 100644 --- a/src/Interpreters/Access/InterpreterCreateUserQuery.cpp +++ b/src/Interpreters/Access/InterpreterCreateUserQuery.cpp @@ -10,6 +10,10 @@ #include #include #include +#include +#include +#include +#include namespace DB @@ -28,6 +32,7 @@ namespace const std::optional & override_default_roles, const std::optional & override_settings, const std::optional & override_grantees, + const std::optional & valid_until, bool allow_implicit_no_password, bool allow_no_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()) { user.allowed_client_hosts = AllowedClientHosts{}; @@ -116,6 +124,26 @@ BlockIO InterpreterCreateUserQuery::execute() if (query.auth_data) auth_data = AuthenticationData::fromAST(*query.auth_data, getContext(), !query.attach); + std::optional valid_until; + if (query.valid_until) + { + const ASTPtr valid_until_literal = evaluateConstantExpressionAsLiteral(query.valid_until, getContext()); + const String valid_until_str = checkAndGetLiteralArgument(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 default_roles_from_query; if (query.default_roles) { @@ -148,7 +176,9 @@ BlockIO InterpreterCreateUserQuery::execute() auto update_func = [&](const AccessEntityPtr & entity) -> AccessEntityPtr { auto updated_user = typeid_cast>(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; }; @@ -167,7 +197,9 @@ BlockIO InterpreterCreateUserQuery::execute() for (const auto & name : *query.names) { auto new_user = std::make_shared(); - 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)); } @@ -201,7 +233,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); } } diff --git a/src/Interpreters/Access/InterpreterShowCreateAccessEntityQuery.cpp b/src/Interpreters/Access/InterpreterShowCreateAccessEntityQuery.cpp index 7292892d3c1..ec2e60b2ef7 100644 --- a/src/Interpreters/Access/InterpreterShowCreateAccessEntityQuery.cpp +++ b/src/Interpreters/Access/InterpreterShowCreateAccessEntityQuery.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -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(out.str()); + } + if (!user.settings.empty()) { if (attach_mode) diff --git a/src/Parsers/Access/ASTCreateUserQuery.cpp b/src/Parsers/Access/ASTCreateUserQuery.cpp index 0611545adf0..d73d6243b8f 100644 --- a/src/Parsers/Access/ASTCreateUserQuery.cpp +++ b/src/Parsers/Access/ASTCreateUserQuery.cpp @@ -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) { @@ -216,6 +221,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) diff --git a/src/Parsers/Access/ASTCreateUserQuery.h b/src/Parsers/Access/ASTCreateUserQuery.h index 62ddbfd0040..f75d9b03de6 100644 --- a/src/Parsers/Access/ASTCreateUserQuery.h +++ b/src/Parsers/Access/ASTCreateUserQuery.h @@ -58,6 +58,8 @@ public: std::shared_ptr default_database; + ASTPtr valid_until; + String getID(char) const override; ASTPtr clone() const override; void formatImpl(const FormatSettings & format, FormatState &, FormatStateStacked) const override; diff --git a/src/Parsers/Access/ParserCreateUserQuery.cpp b/src/Parsers/Access/ParserCreateUserQuery.cpp index 0344fb99c04..550d9756aec 100644 --- a/src/Parsers/Access/ParserCreateUserQuery.cpp +++ b/src/Parsers/Access/ParserCreateUserQuery.cpp @@ -363,6 +363,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); + }); + } } @@ -413,6 +426,7 @@ bool ParserCreateUserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expec std::shared_ptr settings; std::shared_ptr grantees; std::shared_ptr default_database; + ASTPtr valid_until; String cluster; 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; 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->grantees = std::move(grantees); query->default_database = std::move(default_database); + query->valid_until = std::move(valid_until); if (query->auth_data) query->children.push_back(query->auth_data); + if (query->valid_until) + query->children.push_back(query->valid_until); + return true; } } diff --git a/tests/integration/test_user_valid_until/__init__.py b/tests/integration/test_user_valid_until/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/integration/test_user_valid_until/test.py b/tests/integration/test_user_valid_until/test.py new file mode 100644 index 00000000000..787250e6005 --- /dev/null +++ b/tests/integration/test_user_valid_until/test.py @@ -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" + )