mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-28 18:42:26 +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] ...]
|
[, 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:
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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_)
|
||||||
|
@ -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>(); }
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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