mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-25 09:02:00 +00:00
Add VALID UNTIL clause for users
This commit is contained in:
parent
c15e7b93cb
commit
0d2b9fd0e5
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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_)
|
||||
|
@ -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>(); }
|
||||
|
@ -10,6 +10,10 @@
|
||||
#include <Interpreters/Context.h>
|
||||
#include <Interpreters/executeDDLQueryOnCluster.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
|
||||
@ -28,6 +32,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)
|
||||
@ -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<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)
|
||||
{
|
||||
@ -148,7 +176,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;
|
||||
};
|
||||
|
||||
@ -167,7 +197,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));
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -58,6 +58,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;
|
||||
|
@ -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<ASTSettingsProfileElements> settings;
|
||||
std::shared_ptr<ASTRolesOrUsersSet> grantees;
|
||||
std::shared_ptr<ASTDatabaseOrNone> 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;
|
||||
}
|
||||
}
|
||||
|
0
tests/integration/test_user_valid_until/__init__.py
Normal file
0
tests/integration/test_user_valid_until/__init__.py
Normal file
68
tests/integration/test_user_valid_until/test.py
Normal file
68
tests/integration/test_user_valid_until/test.py
Normal 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"
|
||||
)
|
Loading…
Reference in New Issue
Block a user