chore: implement User and UserRegistry classes (#1693)

This commit is contained in:
Kostas Kyrimis 2023-08-15 11:57:21 +02:00 committed by GitHub
parent 82c3690e75
commit e22c131b7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 403 additions and 2 deletions

View File

@ -25,7 +25,7 @@ add_library(dragonfly_lib channel_store.cc command_registry.cc
zset_family.cc version.cc bitops_family.cc container_utils.cc io_utils.cc
serializer_commons.cc journal/serializer.cc journal/executor.cc journal/streamer.cc
top_keys.cc multi_command_squasher.cc hll_family.cc cluster/cluster_config.cc
cluster/cluster_family.cc)
cluster/cluster_family.cc acl/user.cc acl/user_registry.cc)
cxx_link(dragonfly_lib dfly_transaction dfly_facade redis_lib aws_lib strings_lib html_lib
@ -61,6 +61,7 @@ cxx_test(hll_family_test dfly_test_lib LABELS DFLY)
cxx_test(search/search_family_test dfly_test_lib LABELS DFLY)
cxx_test(cluster/cluster_config_test dfly_test_lib LABELS DFLY)
cxx_test(cluster/cluster_family_test dfly_test_lib LABELS DFLY)
cxx_test(acl/user_registry_test dfly_test_lib LABELS DFLY)
@ -69,4 +70,4 @@ add_dependencies(check_dfly dragonfly_test json_family_test list_family_test
generic_family_test memcache_parser_test rdb_test journal_test
redis_parser_test snapshot_test stream_family_test string_family_test
bitops_family_test set_family_test zset_family_test hll_family_test
cluster_config_test cluster_family_test)
cluster_config_test cluster_family_test user_registry_test)

77
src/server/acl/user.cc Normal file
View File

@ -0,0 +1,77 @@
// Copyright 2023, DragonflyDB authors. All rights reserved.
// See LICENSE for licensing terms.
//
#include "server/acl/user.h"
#include <xxhash.h>
namespace dfly {
User::User() {
// acl_categories_ = AclCat::ACL_CATEGORY_ADMIN;
}
void User::Update(UpdateRequest&& req) {
if (req.password) {
SetPassword(*req.password);
}
if (req.plus_acl_categories) {
SetAclCategories(*req.plus_acl_categories);
}
if (req.minus_acl_categories) {
UnsetAclCategories(*req.minus_acl_categories);
}
if (req.is_active) {
SetIsActive(*req.is_active);
}
}
void User::SetPassword(std::string_view password) {
password_ = HashPassword(password);
}
bool User::HasPassword(std::string_view password) const {
if (!password_) {
if (password == "nopass") {
return true;
}
return false;
}
// hash password and compare
return *password_ == HashPassword(password);
}
void User::SetAclCategories(uint64_t cat) {
acl_categories_ |= cat;
}
void User::UnsetAclCategories(uint64_t cat) {
SetAclCategories(cat);
acl_categories_ ^= cat;
}
uint32_t User::AclCategory() const {
return acl_categories_;
}
// For ACL commands
// void SetAclCommand()
// void AclCommand() const;
void User::SetIsActive(bool is_active) {
is_active_ = is_active;
}
bool User::IsActive() const {
return is_active_;
}
uint32_t User::HashPassword(std::string_view password) const {
return XXH3_64bits(password.data(), password.size());
}
} // namespace dfly

168
src/server/acl/user.h Normal file
View File

@ -0,0 +1,168 @@
// Copyright 2023, DragonflyDB authors. All rights reserved.
// See LICENSE for licensing terms.
//
#pragma once
#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include "absl/container/flat_hash_map.h"
#include "absl/hash/hash.h"
namespace dfly {
class CommandId;
// TODO implement these
//#bool CheckIfCommandAllowed(uint64_t command_id, const CommandId& command);
//#bool CheckIfAclCategoryAllowed(uint64_t command_id, const CommandId& command);
namespace AclCat {
/* There are 21 ACL categories as of redis 7
*
* bit 0: admin
* bit 1: bitmap
* bit 2: blocking
* bit 3: connection
* bit 4: dangerous
* bit 5: geo
* bit 6: hash
* bit 7: list
* bit 8: set
* bit 9: string
* bit 10: sorttedset
* bit 11: hyperloglog
* bit 12: streams
* bit 13: fast
* bit 14: slow
* bit 15: key-space
* bit 16: pubsub
* bit 17: read
* bit 18: write
* bit 19: scripting
* bit 20: transaction
*
* The rest of the bitfield, will contain special values like @all
*
* bits 21..31: tba
*/
enum AclCat {
ACL_CATEGORY_KEYSPACE = 1ULL << 0,
ACL_CATEGORY_READ = 1ULL << 1,
ACL_CATEGORY_WRITE = 1ULL << 2,
ACL_CATEGORY_SET = 1ULL << 3,
ACL_CATEGORY_SORTEDSET = 1ULL << 4,
ACL_CATEGORY_LIST = 1ULL << 5,
ACL_CATEGORY_HASH = 1ULL << 6,
ACL_CATEGORY_STRING = 1ULL << 7,
ACL_CATEGORY_BITMAP = 1ULL << 8,
ACL_CATEGORY_HYPERLOGLOG = 1ULL << 9,
ACL_CATEGORY_GEO = 1ULL << 10,
ACL_CATEGORY_STREAM = 1ULL << 11,
ACL_CATEGORY_PUBSUB = 1ULL << 12,
ACL_CATEGORY_ADMIN = 1ULL << 13,
ACL_CATEGORY_FAST = 1ULL << 14,
ACL_CATEGORY_SLOW = 1ULL << 15,
ACL_CATEGORY_BLOCKING = 1ULL << 16,
ACL_CATEGORY_DANGEROUS = 1ULL << 17,
ACL_CATEGORY_CONNECTION = 1ULL << 18,
ACL_CATEGORY_TRANSACTION = 1ULL << 19,
ACL_CATEGORY_SCRIPTING = 1ULL << 20
};
// Special flag/mask for all
inline constexpr uint32_t ACL_CATEGORY_NONE = 0;
inline constexpr uint32_t ACL_CATEGORY_ALL = std::numeric_limits<uint32_t>::max();
} // namespace AclCat
inline const absl::flat_hash_map<std::string_view, uint32_t> CATEGORY_INDEX_TABLE{
{"KEYSPACE", AclCat::ACL_CATEGORY_KEYSPACE},
{"READ", AclCat::ACL_CATEGORY_READ},
{"WRITE", AclCat::ACL_CATEGORY_WRITE},
{"SET", AclCat::ACL_CATEGORY_SET},
{"SORTED_SET", AclCat::ACL_CATEGORY_SORTEDSET},
{"LIST", AclCat::ACL_CATEGORY_LIST},
{"HASH", AclCat::ACL_CATEGORY_HASH},
{"STRING", AclCat::ACL_CATEGORY_STRING},
{"BITMAP", AclCat::ACL_CATEGORY_BITMAP},
{"HYPERLOG", AclCat::ACL_CATEGORY_HYPERLOGLOG},
{"GEO", AclCat::ACL_CATEGORY_GEO},
{"STREAM", AclCat::ACL_CATEGORY_STREAM},
{"PUBSUB", AclCat::ACL_CATEGORY_PUBSUB},
{"ADMIN", AclCat::ACL_CATEGORY_ADMIN},
{"FAST", AclCat::ACL_CATEGORY_FAST},
{"SLOW", AclCat::ACL_CATEGORY_SLOW},
{"BLOCKING", AclCat::ACL_CATEGORY_BLOCKING},
{"DANGEROUS", AclCat::ACL_CATEGORY_DANGEROUS},
{"CONNECTION", AclCat::ACL_CATEGORY_CONNECTION},
{"TRANSACTION", AclCat::ACL_CATEGORY_TRANSACTION},
{"SCRIPTING", AclCat::ACL_CATEGORY_SCRIPTING}};
class User final {
public:
struct UpdateRequest {
std::optional<std::string> password{};
std::optional<uint32_t> plus_acl_categories{};
std::optional<uint32_t> minus_acl_categories{};
// DATATYPE_BITSET commands;
std::optional<bool> is_active{};
};
/* Used for default user
* password = nopass
* acl_categories = +@all
* is_active = true;
*/
User();
User(const User&) = delete;
User(User&&) = default;
// For single step updates
void Update(UpdateRequest&& req);
bool HasPassword(std::string_view password) const;
uint32_t AclCategory() const;
// TODO
// For ACL commands
// void SetAclCommand()
// void AclCommand() const;
bool IsActive() const;
private:
// For ACL categories
void SetAclCategories(uint64_t cat);
void UnsetAclCategories(uint64_t cat);
// For is_active flag
void SetIsActive(bool is_active);
// Helper function for hashing passwords
uint32_t HashPassword(std::string_view password) const;
// For passwords
void SetPassword(std::string_view password);
// when optional is empty, the special `nopass` password is implied
// password hashed with xx64
std::optional<uint64_t> password_;
uint32_t acl_categories_{AclCat::ACL_CATEGORY_NONE};
// we have at least 221 commands including a bunch of subcommands
// LARGE_BITFIELD_DATATYPE acl_commands_;
// if the user is on/off
bool is_active_{false};
};
} // namespace dfly

View File

@ -0,0 +1,53 @@
// Copyright 2023, DragonflyDB authors. All rights reserved.
// See LICENSE for licensing terms.
//
#include "server/acl/user_registry.h"
#include <shared_mutex>
#include "core/fibers.h"
namespace dfly {
void UserRegistry::MaybeAddAndUpdate(std::string_view username, User::UpdateRequest req) {
std::unique_lock<util::SharedMutex> lock(mu_);
auto& user = registry_[username];
user.Update(std::move(req));
}
void UserRegistry::RemoveUser(std::string_view username) {
std::unique_lock<util::SharedMutex> lock(mu_);
registry_.erase(username);
// TODO evict authed connections from user
}
UserRegistry::UserCredentials UserRegistry::GetCredentials(std::string_view username) const {
std::shared_lock<util::SharedMutex> lock(mu_);
auto it = registry_.find(username);
if (it == registry_.end()) {
return {};
}
return {it->second.AclCategory()};
}
bool UserRegistry::IsUserActive(std::string_view username) const {
std::shared_lock<util::SharedMutex> lock(mu_);
auto it = registry_.find(username);
if (it == registry_.end()) {
return false;
}
return it->second.IsActive();
}
bool UserRegistry::AuthUser(std::string_view username, std::string_view password) const {
std::shared_lock<util::SharedMutex> lock(mu_);
const auto& user = registry_.find(username);
if (user == registry_.end()) {
return false;
}
return user->second.HasPassword(password);
}
} // namespace dfly

View File

@ -0,0 +1,57 @@
// Copyright 2022, DragonflyDB authors. All rights reserved.
// See LICENSE for licensing terms.
//
#pragma once
#include <absl/container/flat_hash_map.h>
#include <absl/synchronization/mutex.h>
#include <string>
#include "core/fibers.h"
#include "server/acl/user.h"
namespace dfly {
class UserRegistry {
public:
UserRegistry() = default;
UserRegistry(const UserRegistry&) = delete;
UserRegistry(UserRegistry&&) = delete;
// Acquires a write lock of mu_
// If the user with name `username` does not exist, it's added in the store with
// the exact fields found in req
// If the user exists, the bitfields are updated with a `logical and` operation
// TODO change return time to communicate back results to acl commands
void MaybeAddAndUpdate(std::string_view username, User::UpdateRequest req);
// Acquires a write lock on mu_
// Removes user from the store
// kills already existing connections from the removed user
// TODO change return time to communicate back results to acl commands
void RemoveUser(std::string_view username);
struct UserCredentials {
uint32_t acl_categories{0};
};
// Acquires a read lock
UserCredentials GetCredentials(std::string_view username) const;
// Acquires a read lock
bool IsUserActive(std::string_view username) const;
// Acquires a read lock
// Used by Auth
bool AuthUser(std::string_view username, std::string_view password) const;
private:
absl::flat_hash_map<std::string, User> registry_;
// TODO add abseil mutex attributes
mutable util::SharedMutex mu_;
};
} // namespace dfly

View File

@ -0,0 +1,45 @@
// Copyright 2022, DragonflyDB authors. All rights reserved.
// See LICENSE for licensing terms.
//
#include "server/acl/user_registry.h"
#include <string>
#include <string_view>
#include "base/gtest.h"
#include "base/logging.h"
#include "server/acl/user.h"
using namespace testing;
namespace dfly {
class UserRegistryTest : public Test {};
TEST_F(UserRegistryTest, BasicOp) {
UserRegistry registry;
const std::string username = "kostas";
const std::string pass = "mypass";
User::UpdateRequest req{pass, {}, {}, {}};
registry.MaybeAddAndUpdate(username, std::move(req));
CHECK_EQ(registry.AuthUser(username, pass), true);
CHECK_EQ(registry.IsUserActive(username), false);
CHECK_EQ(registry.GetCredentials(username).acl_categories, AclCat::ACL_CATEGORY_NONE);
const uint32_t set_category = 0 | AclCat::ACL_CATEGORY_LIST | AclCat::ACL_CATEGORY_SET;
req = User::UpdateRequest{{}, set_category, {}, {}};
registry.MaybeAddAndUpdate(username, std::move(req));
auto acl_categories = registry.GetCredentials(username).acl_categories;
CHECK_EQ(acl_categories, set_category);
req = User::UpdateRequest{{}, {}, 0 | AclCat::ACL_CATEGORY_LIST, {}};
registry.MaybeAddAndUpdate(username, std::move(req));
acl_categories = registry.GetCredentials(username).acl_categories;
const uint32_t expected_res = 0 | AclCat::ACL_CATEGORY_SET;
CHECK_EQ(acl_categories, expected_res);
}
} // namespace dfly