1
0
Fork 0
mirror of https://github.com/dragonflydb/dragonfly.git synced 2024-12-14 11:58:02 +00:00

chore: small acl compat changes (#3177)

* print categories and commands in lower case instead of capital case
* fix a bug of default user inheriting the wrong acl rules on new connections
* move keys position to be after password when printed from an acl command
This commit is contained in:
Kostas Kyrimis 2024-06-17 09:40:41 +03:00 committed by GitHub
parent 805c024fc2
commit 8d82581c96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 130 additions and 90 deletions

View file

@ -24,4 +24,10 @@ struct AclKeys {
bool all_keys = false;
};
struct UserCredentials {
uint32_t acl_categories{0};
std::vector<uint64_t> acl_commands;
AclKeys keys;
};
} // namespace dfly::acl

View file

@ -110,7 +110,6 @@ class ConnectionContext {
// TODO fix inherit actual values from default
std::string authed_username{"default"};
uint32_t acl_categories{dfly::acl::ALL};
std::vector<uint64_t> acl_commands;
// keys
dfly::acl::AclKeys keys{{}, true};

View file

@ -67,16 +67,16 @@ void AclFamily::List(CmdArgList args, ConnectionContext* cntx) {
const std::string_view pass = user.Password();
const std::string password = pass == "nopass" ? "nopass" : PrettyPrintSha(pass);
const std::string acl_cat_and_commands =
AclCatAndCommandToString(user.CatChanges(), user.CmdChanges());
const std::string acl_keys = AclKeysToString(user.Keys());
const std::string maybe_space_com = acl_keys.empty() ? "" : " ";
const std::string acl_cat_and_commands =
AclCatAndCommandToString(user.CatChanges(), user.CmdChanges());
using namespace std::string_view_literals;
absl::StrAppend(&buffer, username, " ", user.IsActive() ? "on "sv : "off "sv, password, " ",
acl_cat_and_commands, maybe_space_com, acl_keys);
acl_keys, maybe_space_com, acl_cat_and_commands);
cntx->SendSimpleString(buffer);
}
@ -199,15 +199,17 @@ std::string AclFamily::RegistryToString() const {
const std::string_view pass = user.Password();
const std::string password =
pass == "nopass" ? "nopass " : absl::StrCat("#", PrettyPrintSha(pass, true), " ");
const std::string acl_cat_and_commands =
AclCatAndCommandToString(user.CatChanges(), user.CmdChanges());
const std::string acl_keys = AclKeysToString(user.Keys());
const std::string maybe_space = acl_keys.empty() ? "" : " ";
const std::string acl_cat_and_commands =
AclCatAndCommandToString(user.CatChanges(), user.CmdChanges());
using namespace std::string_view_literals;
absl::StrAppend(&result, command, username, " ", user.IsActive() ? "ON "sv : "OFF "sv, password,
acl_cat_and_commands, maybe_space, acl_keys, "\n");
acl_keys, maybe_space, acl_cat_and_commands, "\n");
}
return result;

View file

@ -6,6 +6,7 @@
#include "absl/container/flat_hash_map.h"
#include "absl/flags/internal/flag.h"
#include "absl/strings/ascii.h"
#include "absl/strings/str_cat.h"
#include "base/gtest.h"
#include "base/logging.h"
@ -47,15 +48,15 @@ TEST_F(AclFamilyTest, AclSetUser) {
resp = Run({"ACL", "LIST"});
auto vec = resp.GetVec();
EXPECT_THAT(
vec, UnorderedElementsAre("user default on nopass +@ALL ~*", "user vlad off nopass -@ALL"));
vec, UnorderedElementsAre("user default on nopass ~* +@all", "user vlad off nopass -@all"));
resp = Run({"ACL", "SETUSER", "vlad", "+ACL"});
EXPECT_THAT(resp, "OK");
resp = Run({"ACL", "LIST"});
vec = resp.GetVec();
EXPECT_THAT(vec, UnorderedElementsAre("user default on nopass +@ALL ~*",
"user vlad off nopass -@ALL +ACL"));
EXPECT_THAT(vec, UnorderedElementsAre("user default on nopass ~* +@all",
"user vlad off nopass -@all +acl"));
}
TEST_F(AclFamilyTest, AclDelUser) {
@ -82,7 +83,7 @@ TEST_F(AclFamilyTest, AclDelUser) {
EXPECT_THAT(resp, IntArg(0));
resp = Run({"ACL", "LIST"});
EXPECT_THAT(resp.GetString(), "user default on nopass +@ALL ~*");
EXPECT_THAT(resp.GetString(), "user default on nopass ~* +@all");
Run({"ACL", "SETUSER", "michael", "ON"});
Run({"ACL", "SETUSER", "kobe", "ON"});
@ -103,9 +104,9 @@ TEST_F(AclFamilyTest, AclList) {
resp = Run({"ACL", "LIST"});
auto vec = resp.GetVec();
EXPECT_THAT(vec, UnorderedElementsAre("user default on nopass +@ALL ~*",
"user kostas off d74ff0ee8da3b98 -@ALL +@ADMIN",
"user adi off d74ff0ee8da3b98 -@ALL +@FAST"));
EXPECT_THAT(vec, UnorderedElementsAre("user default on nopass ~* +@all",
"user kostas off d74ff0ee8da3b98 -@all +@admin",
"user adi off d74ff0ee8da3b98 -@all +@fast"));
}
TEST_F(AclFamilyTest, AclAuth) {
@ -154,16 +155,18 @@ TEST_F(AclFamilyTest, TestAllCategories) {
resp = Run({"ACL", "LIST"});
EXPECT_THAT(resp.GetVec(),
UnorderedElementsAre("user default on nopass +@ALL ~*",
absl::StrCat("user kostas off nopass -@ALL ", "+@", cat)));
UnorderedElementsAre("user default on nopass ~* +@all",
absl::StrCat("user kostas off nopass -@all ", "+@",
absl::AsciiStrToLower(cat))));
resp = Run({"ACL", "SETUSER", "kostas", absl::StrCat("-@", cat)});
EXPECT_THAT(resp, "OK");
resp = Run({"ACL", "LIST"});
EXPECT_THAT(resp.GetVec(),
UnorderedElementsAre("user default on nopass +@ALL ~*",
absl::StrCat("user kostas off nopass -@ALL ", "-@", cat)));
UnorderedElementsAre("user default on nopass ~* +@all",
absl::StrCat("user kostas off nopass -@all ", "-@",
absl::AsciiStrToLower(cat))));
resp = Run({"ACL", "DELUSER", "kostas"});
EXPECT_THAT(resp, IntArg(1));
@ -201,16 +204,18 @@ TEST_F(AclFamilyTest, TestAllCommands) {
EXPECT_THAT(resp, "OK");
resp = Run({"ACL", "LIST"});
EXPECT_THAT(resp.GetVec(), UnorderedElementsAre("user default on nopass +@ALL ~*",
absl::StrCat("user kostas off nopass -@ALL ",
"+", command_name)));
EXPECT_THAT(resp.GetVec(),
UnorderedElementsAre("user default on nopass ~* +@all",
absl::StrCat("user kostas off nopass -@all ", "+",
absl::AsciiStrToLower(command_name))));
resp = Run({"ACL", "SETUSER", "kostas", absl::StrCat("-", command_name)});
resp = Run({"ACL", "LIST"});
EXPECT_THAT(resp.GetVec(), UnorderedElementsAre("user default on nopass +@ALL ~*",
absl::StrCat("user kostas off nopass ",
"-@ALL ", "-", command_name)));
EXPECT_THAT(resp.GetVec(),
UnorderedElementsAre("user default on nopass ~* +@all",
absl::StrCat("user kostas off nopass ", "-@all ", "-",
absl::AsciiStrToLower(command_name))));
resp = Run({"ACL", "DELUSER", "kostas"});
EXPECT_THAT(resp, IntArg(1));
@ -259,7 +264,7 @@ TEST_F(AclFamilyTest, TestGetUser) {
EXPECT_THAT(vec[2], "passwords");
EXPECT_TRUE(vec[3].GetVec().empty());
EXPECT_THAT(vec[4], "commands");
EXPECT_THAT(vec[5], "+@ALL");
EXPECT_THAT(vec[5], "+@all");
EXPECT_THAT(vec[6], "keys");
EXPECT_THAT(vec[7], "~*");
@ -271,7 +276,7 @@ TEST_F(AclFamilyTest, TestGetUser) {
EXPECT_THAT(kvec[2], "passwords");
EXPECT_TRUE(kvec[3].GetVec().empty());
EXPECT_THAT(kvec[4], "commands");
EXPECT_THAT(kvec[5], "-@ALL +@STRING +HSET");
EXPECT_THAT(kvec[5], "-@all +@string +hset");
}
TEST_F(AclFamilyTest, TestDryRun) {

View file

@ -25,12 +25,12 @@ namespace {
std::string AclCatToString(uint32_t acl_category, User::Sign sign) {
std::string res = sign == User::Sign::PLUS ? "+@" : "-@";
if (acl_category == acl::ALL) {
absl::StrAppend(&res, "ALL");
absl::StrAppend(&res, "all");
return res;
}
const auto& index = CategoryToIdx().at(acl_category);
absl::StrAppend(&res, REVERSE_CATEGORY_INDEX_TABLE[index]);
absl::StrAppend(&res, absl::AsciiStrToLower(REVERSE_CATEGORY_INDEX_TABLE[index]));
return res;
}
@ -41,7 +41,7 @@ std::string AclCommandToString(size_t family, uint64_t mask, User::Sign sign) {
std::string prefix = (sign == User::Sign::PLUS) ? "+" : "-";
if (mask == ALL_COMMANDS) {
for (const auto& cmd : rev_index[family]) {
absl::StrAppend(&res, prefix, cmd, " ");
absl::StrAppend(&res, prefix, absl::AsciiStrToLower(cmd), " ");
}
res.pop_back();
return res;
@ -53,7 +53,7 @@ std::string AclCommandToString(size_t family, uint64_t mask, User::Sign sign) {
mask = mask >> 1;
}
--pos;
absl::StrAppend(&res, prefix, rev_index[family][pos]);
absl::StrAppend(&res, prefix, absl::AsciiStrToLower(rev_index[family][pos]));
return res;
}

View file

@ -29,7 +29,7 @@ bool UserRegistry::RemoveUser(std::string_view username) {
return registry_.erase(username);
}
UserRegistry::UserCredentials UserRegistry::GetCredentials(std::string_view username) const {
UserCredentials UserRegistry::GetCredentials(std::string_view username) const {
std::shared_lock<fb2::SharedMutex> lock(mu_);
auto it = registry_.find(username);
if (it == registry_.end()) {

View file

@ -42,12 +42,6 @@ class UserRegistry {
// kills already existing connections from the removed user
bool RemoveUser(std::string_view username);
struct UserCredentials {
uint32_t acl_categories{0};
std::vector<uint64_t> acl_commands;
AclKeys keys;
};
// Acquires a read lock
UserCredentials GetCredentials(std::string_view username) const;

View file

@ -6,6 +6,7 @@
#include "base/logging.h"
#include "core/heap_size.h"
#include "facade/acl_commands_def.h"
#include "server/acl/acl_commands_def.h"
#include "server/command_registry.h"
#include "server/engine_shard_set.h"
@ -80,18 +81,31 @@ const CommandId* StoredCmd::Cid() const {
return cid_;
}
ConnectionContext::ConnectionContext(::io::Sink* stream, facade::Connection* owner)
ConnectionContext::ConnectionContext(::io::Sink* stream, facade::Connection* owner,
acl::UserCredentials cred)
: facade::ConnectionContext(stream, owner) {
if (owner) {
skip_acl_validation = owner->IsPrivileged();
}
acl_commands = std::vector<uint64_t>(acl::NumberOfFamilies(), acl::ALL_COMMANDS);
keys = std::move(cred.keys);
if (cred.acl_commands.empty()) {
acl_commands = std::vector<uint64_t>(acl::NumberOfFamilies(), acl::NONE_COMMANDS);
} else {
acl_commands = std::move(cred.acl_commands);
}
}
ConnectionContext::ConnectionContext(const ConnectionContext* owner, Transaction* tx,
facade::CapturingReplyBuilder* crb)
: facade::ConnectionContext(nullptr, nullptr), transaction{tx} {
acl_commands = std::vector<uint64_t>(acl::NumberOfFamilies(), acl::ALL_COMMANDS);
if (owner) {
acl_commands = owner->acl_commands;
keys = owner->keys;
skip_acl_validation = owner->skip_acl_validation;
} else {
acl_commands = std::vector<uint64_t>(acl::NumberOfFamilies(), acl::NONE_COMMANDS);
}
if (tx) { // If we have a carrier transaction, this context is used for squashing
DCHECK(owner);
conn_state.db_index = owner->conn_state.db_index;

View file

@ -8,6 +8,7 @@
#include <absl/container/flat_hash_set.h>
#include "acl/acl_commands_def.h"
#include "facade/acl_commands_def.h"
#include "facade/conn_context.h"
#include "facade/reply_capture.h"
#include "server/common.h"
@ -265,7 +266,7 @@ struct ConnectionState {
class ConnectionContext : public facade::ConnectionContext {
public:
ConnectionContext(::io::Sink* stream, facade::Connection* owner);
ConnectionContext(::io::Sink* stream, facade::Connection* owner, dfly::acl::UserCredentials cred);
ConnectionContext(const ConnectionContext* owner, Transaction* tx,
facade::CapturingReplyBuilder* crb);

View file

@ -1568,10 +1568,12 @@ bool RequirePrivilegedAuth() {
facade::ConnectionContext* Service::CreateContext(util::FiberSocketBase* peer,
facade::Connection* owner) {
ConnectionContext* res = new ConnectionContext{peer, owner};
auto cred = user_registry_.GetCredentials("default");
ConnectionContext* res = new ConnectionContext{peer, owner, std::move(cred)};
if (peer->IsUDS()) {
res->req_auth = false;
res->skip_acl_validation = true;
} else if (owner->IsPrivileged() && RequirePrivilegedAuth()) {
res->req_auth = !GetPassword().empty();
} else if (!owner->IsPrivileged()) {

View file

@ -557,7 +557,7 @@ error_code Replica::InitiateDflySync() {
error_code Replica::ConsumeRedisStream() {
base::IoBuf io_buf(16_KB);
io::NullSink null_sink; // we never reply back on the commands.
ConnectionContext conn_context{&null_sink, nullptr};
ConnectionContext conn_context{&null_sink, nullptr, {}};
conn_context.is_replicating = true;
conn_context.journal_emulated = true;
conn_context.skip_acl_validation = true;

View file

@ -1644,7 +1644,6 @@ void ServerFamily::Auth(CmdArgList args, ConnectionContext* cntx) {
if (is_authorized) {
cntx->authed_username = username;
auto cred = registry->GetCredentials(username);
cntx->acl_categories = cred.acl_categories;
cntx->acl_commands = cred.acl_commands;
cntx->keys = std::move(cred.keys);
cntx->authenticated = true;
@ -2586,7 +2585,7 @@ void ServerFamily::ReplicaOf(CmdArgList args, ConnectionContext* cntx) {
void ServerFamily::Replicate(string_view host, string_view port) {
io::NullSink sink;
ConnectionContext ctxt{&sink, nullptr};
ConnectionContext ctxt{&sink, nullptr, {}};
ctxt.skip_acl_validation = true;
StringVec replicaof_params{string(host), string(port)};

View file

@ -57,7 +57,8 @@ static vector<string> SplitLines(const std::string& src) {
TestConnection::TestConnection(Protocol protocol, io::StringSink* sink)
: facade::Connection(protocol, nullptr, nullptr, nullptr), sink_(sink) {
cc_.reset(new dfly::ConnectionContext(sink_, this));
cc_.reset(new dfly::ConnectionContext(sink_, this, {}));
cc_->skip_acl_validation = true;
SetSocket(ProactorBase::me()->CreateSocket());
OnConnectionStart();
}

View file

@ -12,61 +12,61 @@ from . import dfly_args
@pytest.mark.asyncio
async def test_acl_setuser(async_client):
await async_client.execute_command("ACL SETUSER kostas")
result = await async_client.execute_command("ACL LIST")
result = await async_client.execute_command("ACL list")
assert 2 == len(result)
assert "user kostas off nopass -@ALL" in result
assert "user kostas off nopass -@all" in result
await async_client.execute_command("ACL SETUSER kostas ON")
result = await async_client.execute_command("ACL LIST")
assert "user kostas on nopass -@ALL" in result
result = await async_client.execute_command("ACL list")
assert "user kostas on nopass -@all" in result
await async_client.execute_command("ACL SETUSER kostas +@list +@string +@admin")
result = await async_client.execute_command("ACL LIST")
result = await async_client.execute_command("ACL list")
# TODO consider printing to lowercase
assert "user kostas on nopass -@ALL +@LIST +@STRING +@ADMIN" in result
assert "user kostas on nopass -@all +@list +@string +@admin" in result
await async_client.execute_command("ACL SETUSER kostas -@list -@admin")
result = await async_client.execute_command("ACL LIST")
assert "user kostas on nopass -@ALL +@STRING -@LIST -@ADMIN" in result
result = await async_client.execute_command("ACL list")
assert "user kostas on nopass -@all +@string -@list -@admin" in result
# mix and match
await async_client.execute_command("ACL SETUSER kostas +@list -@string")
result = await async_client.execute_command("ACL LIST")
assert "user kostas on nopass -@ALL -@ADMIN +@LIST -@STRING" in result
result = await async_client.execute_command("ACL list")
assert "user kostas on nopass -@all -@admin +@list -@string" in result
# mix and match interleaved
await async_client.execute_command("ACL SETUSER kostas +@set -@set +@set")
result = await async_client.execute_command("ACL LIST")
assert "user kostas on nopass -@ALL -@ADMIN +@LIST -@STRING +@SET" in result
result = await async_client.execute_command("ACL list")
assert "user kostas on nopass -@all -@admin +@list -@string +@set" in result
await async_client.execute_command("ACL SETUSER kostas +@all")
result = await async_client.execute_command("ACL LIST")
assert "user kostas on nopass -@ADMIN +@LIST -@STRING +@SET +@ALL" in result
result = await async_client.execute_command("ACL list")
assert "user kostas on nopass -@admin +@list -@string +@set +@all" in result
# commands
await async_client.execute_command("ACL SETUSER kostas +set +get +hset")
result = await async_client.execute_command("ACL LIST")
assert "user kostas on nopass -@ADMIN +@LIST -@STRING +@SET +@ALL +SET +GET +HSET" in result
result = await async_client.execute_command("ACL list")
assert "user kostas on nopass -@admin +@list -@string +@set +@all +set +get +hset" in result
await async_client.execute_command("ACL SETUSER kostas -set -get +hset")
result = await async_client.execute_command("ACL LIST")
assert "user kostas on nopass -@ADMIN +@LIST -@STRING +@SET +@ALL -SET -GET +HSET" in result
result = await async_client.execute_command("ACL list")
assert "user kostas on nopass -@admin +@list -@string +@set +@all -set -get +hset" in result
# interleaved
await async_client.execute_command("ACL SETUSER kostas -hset +get -get -@all")
result = await async_client.execute_command("ACL LIST")
assert "user kostas on nopass -@ADMIN +@LIST -@STRING +@SET -SET -HSET -GET -@ALL" in result
result = await async_client.execute_command("ACL list")
assert "user kostas on nopass -@admin +@list -@string +@set -set -hset -get -@all" in result
# interleaved with categories
await async_client.execute_command("ACL SETUSER kostas +@string +get -get +set")
result = await async_client.execute_command("ACL LIST")
assert "user kostas on nopass -@ADMIN +@LIST +@SET -HSET -@ALL +@STRING -GET +SET" in result
result = await async_client.execute_command("ACL list")
assert "user kostas on nopass -@admin +@list +@set -hset -@all +@string -get +set" in result
@pytest.mark.asyncio
async def test_acl_categories(async_client):
await async_client.execute_command(
"ACL SETUSER vlad ON >mypass -@ALL +@string +@list +@connection ~*"
"ACL SETUSER vlad ON >mypass -@all +@string +@list +@connection ~*"
)
result = await async_client.execute_command("AUTH vlad mypass")
@ -116,7 +116,7 @@ async def test_acl_categories(async_client):
@pytest.mark.asyncio
async def test_acl_commands(async_client):
await async_client.execute_command("ACL SETUSER random ON >mypass -@ALL +set +get ~*")
await async_client.execute_command("ACL SETUSER random ON >mypass -@all +set +get ~*")
result = await async_client.execute_command("AUTH random mypass")
assert result == "OK"
@ -177,7 +177,7 @@ async def test_acl_cat_commands_multi_exec_squash(df_local_factory):
for x in range(33):
await client.execute_command(f"SET x{x} {x}")
# ADMIN revokes permissions
# admin revokes permissions
res = await admin_client.execute_command("ACL SETUSER kk -@string")
assert res == b"OK"
@ -330,22 +330,22 @@ async def test_good_acl_file(df_local_factory, tmp_dir):
client = df.client()
await client.execute_command("ACL LOAD")
result = await client.execute_command("ACL LIST")
result = await client.execute_command("ACL list")
assert 2 == len(result)
assert "user MrFoo on ea71c25a7a60224 -@ALL" in result
assert "user default on nopass +@ALL ~*" in result
assert "user MrFoo on ea71c25a7a60224 -@all" in result
assert "user default on nopass ~* +@all" in result
await client.execute_command("ACL DELUSER MrFoo")
await client.execute_command("ACL SETUSER roy ON >mypass +@STRING +HSET")
await client.execute_command("ACL SETUSER shahar >mypass +@SET")
await client.execute_command("ACL SETUSER vlad +@STRING ~foo ~bar*")
await client.execute_command("ACL SETUSER roy ON >mypass +@string +hset")
await client.execute_command("ACL SETUSER shahar >mypass +@set")
await client.execute_command("ACL SETUSER vlad ~foo ~bar* +@string")
result = await client.execute_command("ACL LIST")
result = await client.execute_command("ACL list")
assert 4 == len(result)
assert "user roy on ea71c25a7a60224 -@ALL +@STRING +HSET" in result
assert "user shahar off ea71c25a7a60224 -@ALL +@SET" in result
assert "user vlad off nopass -@ALL +@STRING ~foo ~bar*" in result
assert "user default on nopass +@ALL ~*" in result
assert "user roy on ea71c25a7a60224 -@all +@string +hset" in result
assert "user shahar off ea71c25a7a60224 -@all +@set" in result
assert "user vlad off nopass ~foo ~bar* -@all +@string" in result
assert "user default on nopass ~* +@all" in result
result = await client.execute_command("ACL DELUSER shahar")
assert result == 1
@ -354,11 +354,11 @@ async def test_good_acl_file(df_local_factory, tmp_dir):
result = await client.execute_command("ACL LOAD")
result = await client.execute_command("ACL LIST")
result = await client.execute_command("ACL list")
assert 3 == len(result)
assert "user roy on ea71c25a7a60224 -@ALL +@STRING +HSET" in result
assert "user vlad off nopass -@ALL +@STRING ~foo ~bar*" in result
assert "user default on nopass +@ALL ~*" in result
assert "user roy on ea71c25a7a60224 -@all +@string +hset" in result
assert "user vlad off nopass ~foo ~bar* -@all +@string" in result
assert "user default on nopass ~* +@all" in result
await client.close()
@ -387,7 +387,7 @@ async def test_acl_log(async_client):
res = await async_client.execute_command("SET mykey 22")
with pytest.raises(redis.exceptions.ResponseError):
await async_client.execute_command("HSET mk kk 22")
await async_client.execute_command("hset mk kk 22")
res = await async_client.execute_command("ACL LOG")
assert 1 == len(res)
@ -447,7 +447,7 @@ async def test_require_pass(df_local_factory):
@pytest.mark.asyncio
async def test_set_acl_file(async_client: aioredis.Redis, tmp_dir):
acl_file_content = "USER roy ON #ea71c25a7a602246b4c39824b855678894a96f43bb9b71319c39700a1e045222 +@STRING +@FAST +HSET\nUSER john on nopass +@string"
acl_file_content = "USER roy ON #ea71c25a7a602246b4c39824b855678894a96f43bb9b71319c39700a1e045222 +@string +@fast +hset\nUSER john on nopass +@string"
acl = create_temp_file(acl_file_content, tmp_dir)
@ -455,7 +455,7 @@ async def test_set_acl_file(async_client: aioredis.Redis, tmp_dir):
await async_client.execute_command("ACL LOAD")
result = await async_client.execute_command("ACL LIST")
result = await async_client.execute_command("ACL list")
assert 3 == len(result)
result = await async_client.execute_command("AUTH roy mypass")
@ -536,3 +536,20 @@ async def test_acl_keys(async_client):
# reject because bonus key does not match
with pytest.raises(redis.exceptions.ResponseError):
await async_client.execute_command("ZUNIONSTORE destkey 2 barz1 barz2")
@pytest.mark.asyncio
async def default_user_bug(df_local_factory):
df.start()
client = aioredis.Redis(port=df.port)
await async_client.execute_command("ACL SETUSER default -@all")
await client.close()
client = aioredis.Redis(port=df.port)
with pytest.raises(redis.exceptions.ResponseError):
await client.execute_command("SET foo bar")
await client.close()