Initial implementation of authentication
Reviewers: teon.banek, buda Reviewed By: teon.banek Subscribers: mtomic, pullbot Differential Revision: https://phabricator.memgraph.io/D1488
This commit is contained in:
parent
7e92a7f41c
commit
2ecb660790
@ -249,3 +249,13 @@ import_external_library(librdkafka STATIC
|
||||
import_library(librdkafka++ STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/librdkafka/lib/librdkafka++.a
|
||||
librdkafka-proj)
|
||||
|
||||
# Setup libbcrypt
|
||||
import_external_library(libbcrypt STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/libbcrypt/bcrypt.a
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/libbcrypt
|
||||
CONFIGURE_COMMAND sed s/-Wcast-align// -i ${CMAKE_CURRENT_SOURCE_DIR}/libbcrypt/crypt_blowfish/Makefile
|
||||
BUILD_COMMAND make -C ${CMAKE_CURRENT_SOURCE_DIR}/libbcrypt
|
||||
CC=${CMAKE_C_COMPILER}
|
||||
CXX=${CMAKE_CXX_COMPILER}
|
||||
INSTALL_COMMAND true)
|
||||
|
@ -91,6 +91,11 @@ clone git://deps.memgraph.io/glog.git glog $glog_tag
|
||||
gflags_tag="b37ceb03a0e56c9f15ce80409438a555f8a67b7c" # custom version (May 6, 2017)
|
||||
clone git://deps.memgraph.io/gflags.git gflags $gflags_tag
|
||||
|
||||
# libbcrypt
|
||||
# git clone https://github.com/rg3/libbcrypt
|
||||
libbcrypt_tag="8aa32ad94ebe06b76853b0767c910c9fbf7ccef4" # custom version (Dec 16, 2016)
|
||||
clone git://deps.memgraph.io/libbcrypt.git libbcrypt $libbcrypt_tag
|
||||
|
||||
# neo4j
|
||||
wget -nv http://deps.memgraph.io/neo4j-community-3.2.3-unix.tar.gz -O neo4j.tar.gz
|
||||
tar -xzf neo4j.tar.gz
|
||||
|
@ -7,6 +7,7 @@ add_subdirectory(integrations)
|
||||
add_subdirectory(io)
|
||||
add_subdirectory(telemetry)
|
||||
add_subdirectory(communication)
|
||||
add_subdirectory(auth)
|
||||
|
||||
# all memgraph src files
|
||||
set(memgraph_src_files
|
||||
@ -191,7 +192,7 @@ set(MEMGRAPH_ALL_LIBS stdc++fs Threads::Threads fmt cppitertools
|
||||
antlr_opencypher_parser_lib dl glog gflags capnp kj
|
||||
${Boost_IOSTREAMS_LIBRARY_RELEASE}
|
||||
${Boost_SERIALIZATION_LIBRARY_RELEASE}
|
||||
mg-utils mg-io mg-integrations mg-requests mg-communication)
|
||||
mg-utils mg-io mg-integrations mg-requests mg-communication mg-auth)
|
||||
|
||||
if (USE_LTALLOC)
|
||||
list(APPEND MEMGRAPH_ALL_LIBS ltalloc)
|
||||
|
8
src/auth/CMakeLists.txt
Normal file
8
src/auth/CMakeLists.txt
Normal file
@ -0,0 +1,8 @@
|
||||
set(auth_src_files
|
||||
auth.cpp
|
||||
crypto.cpp
|
||||
models.cpp)
|
||||
|
||||
add_library(mg-auth STATIC ${auth_src_files})
|
||||
target_link_libraries(mg-auth json libbcrypt)
|
||||
target_link_libraries(mg-auth mg-utils)
|
138
src/auth/auth.cpp
Normal file
138
src/auth/auth.cpp
Normal file
@ -0,0 +1,138 @@
|
||||
#include "auth/auth.hpp"
|
||||
|
||||
#include "utils/string.hpp"
|
||||
|
||||
namespace auth {
|
||||
|
||||
const std::string kUserPrefix = "user:";
|
||||
const std::string kRolePrefix = "role:";
|
||||
const std::string kLinkPrefix = "link:";
|
||||
|
||||
/**
|
||||
* All data stored in the `Auth` storage is stored in an underlying
|
||||
* `storage::KVStore`. Because we are using a key-value store to store the data,
|
||||
* the data has to be encoded. The encoding used is as follows:
|
||||
*
|
||||
* User: key="user:<username>", value="<json_encoded_members_of_user>"
|
||||
* Role: key="role:<rolename>", value="<json_endoded_members_of_role>"
|
||||
*
|
||||
* The User->Role relationship isn't stored in the `User` encoded data because
|
||||
* we want to be able to delete/modify a Role and have it automatically be
|
||||
* removed/modified in all linked users. Because of that we store the links to
|
||||
* the role as a foreign-key like mapping in the KVStore. It is saved as
|
||||
* follows:
|
||||
*
|
||||
* key="link:<username>", value="<rolename>"
|
||||
*/
|
||||
|
||||
Auth::Auth(const std::string &storage_directory)
|
||||
: storage_(storage_directory) {}
|
||||
|
||||
std::experimental::optional<User> Auth::Authenticate(
|
||||
const std::string &username, const std::string &password) {
|
||||
auto user = GetUser(username);
|
||||
if (!user) return std::experimental::nullopt;
|
||||
if (!user->CheckPassword(password)) return std::experimental::nullopt;
|
||||
return user;
|
||||
}
|
||||
|
||||
std::experimental::optional<User> Auth::GetUser(const std::string &username) {
|
||||
auto existing_user = storage_.Get(kUserPrefix + username);
|
||||
if (!existing_user) return std::experimental::nullopt;
|
||||
|
||||
nlohmann::json data;
|
||||
try {
|
||||
data = nlohmann::json::parse(*existing_user);
|
||||
} catch (const nlohmann::json::parse_error &e) {
|
||||
throw utils::BasicException("Couldn't load user data!");
|
||||
}
|
||||
|
||||
auto user = User::Deserialize(data);
|
||||
auto link = storage_.Get(kLinkPrefix + username);
|
||||
|
||||
if (link) {
|
||||
auto role = GetRole(*link);
|
||||
if (role) {
|
||||
user.SetRole(*role);
|
||||
}
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
bool Auth::SaveUser(const User &user) {
|
||||
if (!storage_.Put(kUserPrefix + user.username(), user.Serialize().dump())) {
|
||||
return false;
|
||||
}
|
||||
if (user.role()) {
|
||||
return storage_.Put(kLinkPrefix + user.username(), user.role()->rolename());
|
||||
} else {
|
||||
return storage_.Delete(kLinkPrefix + user.username());
|
||||
}
|
||||
}
|
||||
|
||||
std::experimental::optional<User> Auth::AddUser(const std::string &username) {
|
||||
auto existing_user = GetUser(username);
|
||||
if (existing_user) return std::experimental::nullopt;
|
||||
auto new_user = User(username);
|
||||
if (!SaveUser(new_user)) return std::experimental::nullopt;
|
||||
return new_user;
|
||||
}
|
||||
|
||||
bool Auth::RemoveUser(const std::string &username) {
|
||||
if (!storage_.Get(kUserPrefix + username)) return false;
|
||||
if (!storage_.Delete(kLinkPrefix + username)) return false;
|
||||
return storage_.Delete(kUserPrefix + username);
|
||||
}
|
||||
|
||||
bool Auth::HasUsers() {
|
||||
for (auto it = storage_.begin(); it != storage_.end(); ++it) {
|
||||
if (utils::StartsWith(it->first, kUserPrefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::experimental::optional<Role> Auth::GetRole(const std::string &rolename) {
|
||||
auto existing_role = storage_.Get(kRolePrefix + rolename);
|
||||
if (!existing_role) return std::experimental::nullopt;
|
||||
|
||||
nlohmann::json data;
|
||||
try {
|
||||
data = nlohmann::json::parse(*existing_role);
|
||||
} catch (const nlohmann::json::parse_error &e) {
|
||||
throw utils::BasicException("Couldn't load role data!");
|
||||
}
|
||||
|
||||
return Role::Deserialize(data);
|
||||
}
|
||||
|
||||
bool Auth::SaveRole(const Role &role) {
|
||||
return storage_.Put(kRolePrefix + role.rolename(), role.Serialize().dump());
|
||||
}
|
||||
|
||||
std::experimental::optional<Role> Auth::AddRole(const std::string &rolename) {
|
||||
auto existing_role = GetRole(rolename);
|
||||
if (existing_role) return std::experimental::nullopt;
|
||||
auto new_role = Role(rolename);
|
||||
if (!SaveRole(new_role)) return std::experimental::nullopt;
|
||||
return new_role;
|
||||
}
|
||||
|
||||
bool Auth::RemoveRole(const std::string &rolename) {
|
||||
if (!storage_.Get(kRolePrefix + rolename)) return false;
|
||||
std::vector<std::string> links;
|
||||
for (auto it = storage_.begin(); it != storage_.end(); ++it) {
|
||||
if (utils::StartsWith(it->first, kLinkPrefix) && it->second == rolename) {
|
||||
links.push_back(it->first);
|
||||
}
|
||||
}
|
||||
for (const auto &link : links) {
|
||||
storage_.Delete(link);
|
||||
}
|
||||
return storage_.Delete(kRolePrefix + rolename);
|
||||
}
|
||||
|
||||
std::mutex &Auth::WithLock() { return lock_; }
|
||||
|
||||
} // namespace auth
|
52
src/auth/auth.hpp
Normal file
52
src/auth/auth.hpp
Normal file
@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <experimental/optional>
|
||||
#include <mutex>
|
||||
|
||||
#include "auth/models.hpp"
|
||||
#include "storage/kvstore.hpp"
|
||||
|
||||
namespace auth {
|
||||
|
||||
/**
|
||||
* This class serves as the main Authentication/Authorization storage.
|
||||
* It provides functions for managing Users, Roles and Permissions.
|
||||
* NOTE: The functions in this class aren't thread safe. Use the `WithLock` lock
|
||||
* if you want to have safe modifications of the storage.
|
||||
*/
|
||||
class Auth final {
|
||||
public:
|
||||
Auth(const std::string &storage_directory);
|
||||
|
||||
std::experimental::optional<User> Authenticate(const std::string &username,
|
||||
const std::string &password);
|
||||
|
||||
std::experimental::optional<User> GetUser(const std::string &username);
|
||||
|
||||
bool SaveUser(const User &user);
|
||||
|
||||
std::experimental::optional<User> AddUser(const std::string &username);
|
||||
|
||||
bool RemoveUser(const std::string &username);
|
||||
|
||||
bool HasUsers();
|
||||
|
||||
std::experimental::optional<Role> GetRole(const std::string &rolename);
|
||||
|
||||
bool SaveRole(const Role &role);
|
||||
|
||||
std::experimental::optional<Role> AddRole(const std::string &rolename);
|
||||
|
||||
bool RemoveRole(const std::string &rolename);
|
||||
|
||||
std::mutex &WithLock();
|
||||
|
||||
private:
|
||||
storage::KVStore storage_;
|
||||
// Even though the `storage::KVStore` class is guaranteed to be thread-safe we
|
||||
// use a mutex to lock all operations on the `User` and `Role` storage because
|
||||
// some operations on the users and/or roles may require more than one
|
||||
// operation on the storage.
|
||||
std::mutex lock_;
|
||||
};
|
||||
} // namespace auth
|
35
src/auth/crypto.cpp
Normal file
35
src/auth/crypto.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
#include "auth/crypto.hpp"
|
||||
|
||||
#include <libbcrypt/bcrypt.h>
|
||||
|
||||
#include "utils/exceptions.hpp"
|
||||
|
||||
namespace auth {
|
||||
|
||||
const std::string EncryptPassword(const std::string &password) {
|
||||
char salt[BCRYPT_HASHSIZE];
|
||||
char hash[BCRYPT_HASHSIZE];
|
||||
|
||||
// We use `-1` as the workfactor for `bcrypt_gensalt` to let it fall back to
|
||||
// its default value of `12`. Increasing the workfactor increases the time
|
||||
// needed to generate the salt.
|
||||
if (bcrypt_gensalt(-1, salt) != 0) {
|
||||
throw utils::BasicException("Couldn't generate hashing salt!");
|
||||
}
|
||||
|
||||
if (bcrypt_hashpw(password.c_str(), salt, hash) != 0) {
|
||||
throw utils::BasicException("Couldn't hash password!");
|
||||
}
|
||||
|
||||
return std::string(hash);
|
||||
}
|
||||
|
||||
bool VerifyPassword(const std::string &password, const std::string &hash) {
|
||||
int ret = bcrypt_checkpw(password.c_str(), hash.c_str());
|
||||
if (ret == -1) {
|
||||
throw utils::BasicException("Couldn't check password!");
|
||||
}
|
||||
return ret == 0;
|
||||
}
|
||||
|
||||
} // namespace auth
|
11
src/auth/crypto.hpp
Normal file
11
src/auth/crypto.hpp
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace auth {
|
||||
|
||||
const std::string EncryptPassword(const std::string &password);
|
||||
|
||||
bool VerifyPassword(const std::string &password, const std::string &hash);
|
||||
|
||||
} // namespace auth
|
167
src/auth/models.cpp
Normal file
167
src/auth/models.cpp
Normal file
@ -0,0 +1,167 @@
|
||||
#include "auth/models.hpp"
|
||||
|
||||
#include "auth/crypto.hpp"
|
||||
#include "utils/cast.hpp"
|
||||
#include "utils/exceptions.hpp"
|
||||
|
||||
namespace auth {
|
||||
|
||||
Permissions::Permissions(uint64_t grants, uint64_t denies) {
|
||||
// The deny bitmask has higher priority than the grant bitmask.
|
||||
denies_ = denies;
|
||||
// Mask out the grant bitmask to make sure that it is correct.
|
||||
grants_ = grants & (~denies);
|
||||
}
|
||||
|
||||
PermissionLevel Permissions::Has(Permission permission) const {
|
||||
// Check for the deny first because it has greater priority than a grant.
|
||||
if (denies_ & utils::UnderlyingCast(permission)) {
|
||||
return PermissionLevel::Deny;
|
||||
} else if (grants_ & utils::UnderlyingCast(permission)) {
|
||||
return PermissionLevel::Grant;
|
||||
}
|
||||
return PermissionLevel::Neutral;
|
||||
}
|
||||
|
||||
void Permissions::Grant(Permission permission) {
|
||||
// Remove the possible deny.
|
||||
denies_ &= ~utils::UnderlyingCast(permission);
|
||||
// Now we grant the permission.
|
||||
grants_ |= utils::UnderlyingCast(permission);
|
||||
}
|
||||
|
||||
void Permissions::Revoke(Permission permission) {
|
||||
// Remove the possible grant.
|
||||
grants_ &= ~utils::UnderlyingCast(permission);
|
||||
// Remove the possible deny.
|
||||
denies_ &= ~utils::UnderlyingCast(permission);
|
||||
}
|
||||
|
||||
void Permissions::Deny(Permission permission) {
|
||||
// First deny the permission.
|
||||
denies_ |= utils::UnderlyingCast(permission);
|
||||
// Remove the possible grant.
|
||||
grants_ &= ~utils::UnderlyingCast(permission);
|
||||
}
|
||||
|
||||
nlohmann::json Permissions::Serialize() const {
|
||||
nlohmann::json data = nlohmann::json::object();
|
||||
data["grants"] = grants_;
|
||||
data["denies"] = denies_;
|
||||
return data;
|
||||
}
|
||||
|
||||
Permissions Permissions::Deserialize(const nlohmann::json &data) {
|
||||
if (!data.is_object()) {
|
||||
throw utils::BasicException("Couldn't load permissions data!");
|
||||
}
|
||||
if (!data["grants"].is_number_unsigned() ||
|
||||
!data["denies"].is_number_unsigned()) {
|
||||
throw utils::BasicException("Couldn't load permissions data!");
|
||||
}
|
||||
return {data["grants"], data["denies"]};
|
||||
}
|
||||
|
||||
uint64_t Permissions::grants() const { return grants_; }
|
||||
uint64_t Permissions::denies() const { return denies_; }
|
||||
|
||||
bool operator==(const Permissions &first, const Permissions &second) {
|
||||
return first.grants() == second.grants() && first.denies() == second.denies();
|
||||
}
|
||||
|
||||
bool operator!=(const Permissions &first, const Permissions &second) {
|
||||
return !(first == second);
|
||||
}
|
||||
|
||||
Role::Role(const std::string &rolename) : rolename_(rolename) {}
|
||||
|
||||
Role::Role(const std::string &rolename, const Permissions &permissions)
|
||||
: rolename_(rolename), permissions_(permissions) {}
|
||||
|
||||
const std::string &Role::rolename() const { return rolename_; }
|
||||
const Permissions &Role::permissions() const { return permissions_; }
|
||||
Permissions &Role::permissions() { return permissions_; }
|
||||
|
||||
nlohmann::json Role::Serialize() const {
|
||||
nlohmann::json data = nlohmann::json::object();
|
||||
data["rolename"] = rolename_;
|
||||
data["permissions"] = permissions_.Serialize();
|
||||
return data;
|
||||
}
|
||||
|
||||
Role Role::Deserialize(const nlohmann::json &data) {
|
||||
if (!data.is_object()) {
|
||||
throw utils::BasicException("Couldn't load role data!");
|
||||
}
|
||||
if (!data["rolename"].is_string() || !data["permissions"].is_object()) {
|
||||
throw utils::BasicException("Couldn't load role data!");
|
||||
}
|
||||
auto permissions = Permissions::Deserialize(data["permissions"]);
|
||||
return {data["rolename"], permissions};
|
||||
}
|
||||
|
||||
bool operator==(const Role &first, const Role &second) {
|
||||
return first.rolename_ == second.rolename_ &&
|
||||
first.permissions_ == second.permissions_;
|
||||
}
|
||||
|
||||
User::User(const std::string &username) : username_(username) {}
|
||||
|
||||
User::User(const std::string &username, const std::string &password_hash,
|
||||
const Permissions &permissions)
|
||||
: username_(username),
|
||||
password_hash_(password_hash),
|
||||
permissions_(permissions) {}
|
||||
|
||||
bool User::CheckPassword(const std::string &password) {
|
||||
return VerifyPassword(password, password_hash_);
|
||||
}
|
||||
|
||||
void User::UpdatePassword(const std::string &password) {
|
||||
password_hash_ = EncryptPassword(password);
|
||||
}
|
||||
|
||||
void User::SetRole(const Role &role) { role_.emplace(role); }
|
||||
|
||||
const Permissions User::GetPermissions() const {
|
||||
if (role_) {
|
||||
return Permissions(permissions_.grants() | role_->permissions().grants(),
|
||||
permissions_.denies() | role_->permissions().denies());
|
||||
}
|
||||
return permissions_;
|
||||
}
|
||||
|
||||
const std::string &User::username() const { return username_; }
|
||||
|
||||
Permissions &User::permissions() { return permissions_; }
|
||||
|
||||
std::experimental::optional<Role> User::role() const { return role_; }
|
||||
|
||||
nlohmann::json User::Serialize() const {
|
||||
nlohmann::json data = nlohmann::json::object();
|
||||
data["username"] = username_;
|
||||
data["password_hash"] = password_hash_;
|
||||
data["permissions"] = permissions_.Serialize();
|
||||
// The role shouldn't be serialized here, it is stored as a foreign key.
|
||||
return data;
|
||||
}
|
||||
|
||||
User User::Deserialize(const nlohmann::json &data) {
|
||||
if (!data.is_object()) {
|
||||
throw utils::BasicException("Couldn't load user data!");
|
||||
}
|
||||
if (!data["username"].is_string() || !data["password_hash"].is_string() ||
|
||||
!data["permissions"].is_object()) {
|
||||
throw utils::BasicException("Couldn't load user data!");
|
||||
}
|
||||
auto permissions = Permissions::Deserialize(data["permissions"]);
|
||||
return {data["username"], data["password_hash"], permissions};
|
||||
}
|
||||
|
||||
bool operator==(const User &first, const User &second) {
|
||||
return first.username_ == second.username_ &&
|
||||
first.password_hash_ == second.password_hash_ &&
|
||||
first.permissions_ == second.permissions_ &&
|
||||
first.role_ == second.role_;
|
||||
}
|
||||
} // namespace auth
|
117
src/auth/models.hpp
Normal file
117
src/auth/models.hpp
Normal file
@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include <experimental/optional>
|
||||
#include <string>
|
||||
|
||||
#include <json/json.hpp>
|
||||
|
||||
namespace auth {
|
||||
|
||||
// TODO (mferencevic): Add permissions for admin actions.
|
||||
enum class Permission : uint64_t {
|
||||
Read = 0x00000001,
|
||||
|
||||
Create = 0x00000002,
|
||||
|
||||
Update = 0x00000004,
|
||||
|
||||
Delete = 0x00000008,
|
||||
};
|
||||
|
||||
enum class PermissionLevel {
|
||||
Grant,
|
||||
Neutral,
|
||||
Deny,
|
||||
};
|
||||
|
||||
// TODO (mferencevic): Add string conversions to/from permissions.
|
||||
|
||||
class Permissions final {
|
||||
public:
|
||||
Permissions(uint64_t grants = 0, uint64_t denies = 0);
|
||||
|
||||
PermissionLevel Has(Permission permission) const;
|
||||
|
||||
void Grant(Permission permission);
|
||||
|
||||
void Revoke(Permission permission);
|
||||
|
||||
void Deny(Permission permission);
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
static Permissions Deserialize(const nlohmann::json &data);
|
||||
|
||||
uint64_t grants() const;
|
||||
uint64_t denies() const;
|
||||
|
||||
private:
|
||||
uint64_t grants_{0};
|
||||
uint64_t denies_{0};
|
||||
};
|
||||
|
||||
bool operator==(const Permissions &first, const Permissions &second);
|
||||
|
||||
bool operator!=(const Permissions &first, const Permissions &second);
|
||||
|
||||
class Role final {
|
||||
public:
|
||||
Role(const std::string &rolename);
|
||||
|
||||
Role(const std::string &rolename, const Permissions &permissions);
|
||||
|
||||
const std::string &rolename() const;
|
||||
const Permissions &permissions() const;
|
||||
Permissions &permissions();
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
static Role Deserialize(const nlohmann::json &data);
|
||||
|
||||
friend bool operator==(const Role &first, const Role &second);
|
||||
|
||||
private:
|
||||
std::string rolename_;
|
||||
Permissions permissions_;
|
||||
};
|
||||
|
||||
bool operator==(const Role &first, const Role &second);
|
||||
|
||||
// TODO (mferencevic): Implement password strength enforcement.
|
||||
// TODO (mferencevic): Implement password expiry.
|
||||
class User final {
|
||||
public:
|
||||
User(const std::string &username);
|
||||
|
||||
User(const std::string &username, const std::string &password_hash,
|
||||
const Permissions &permissions);
|
||||
|
||||
bool CheckPassword(const std::string &password);
|
||||
|
||||
void UpdatePassword(const std::string &password);
|
||||
|
||||
void SetRole(const Role &role);
|
||||
|
||||
const Permissions GetPermissions() const;
|
||||
|
||||
const std::string &username() const;
|
||||
|
||||
Permissions &permissions();
|
||||
|
||||
std::experimental::optional<Role> role() const;
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
static User Deserialize(const nlohmann::json &data);
|
||||
|
||||
friend bool operator==(const User &first, const User &second);
|
||||
|
||||
private:
|
||||
std::string username_;
|
||||
std::string password_hash_;
|
||||
Permissions permissions_;
|
||||
std::experimental::optional<Role> role_;
|
||||
};
|
||||
|
||||
bool operator==(const User &first, const User &second);
|
||||
} // namespace auth
|
@ -60,6 +60,10 @@ class Session {
|
||||
/** Aborts currently running query. */
|
||||
virtual void Abort() = 0;
|
||||
|
||||
/** Return `true` if the user was successfully authenticated. */
|
||||
virtual bool Authenticate(const std::string &username,
|
||||
const std::string &password) = 0;
|
||||
|
||||
/**
|
||||
* Executes the session after data has been read into the buffer.
|
||||
* Goes through the bolt states in order to execute commands from the client.
|
||||
|
@ -6,6 +6,7 @@
|
||||
#include "communication/bolt/v1/codes.hpp"
|
||||
#include "communication/bolt/v1/state.hpp"
|
||||
#include "communication/bolt/v1/value.hpp"
|
||||
#include "communication/exceptions.hpp"
|
||||
#include "utils/likely.hpp"
|
||||
|
||||
namespace communication::bolt {
|
||||
@ -59,6 +60,33 @@ State StateInitRun(Session &session) {
|
||||
LOG(INFO) << fmt::format("Client connected '{}'", client_name.ValueString())
|
||||
<< std::endl;
|
||||
|
||||
// Get authentication data.
|
||||
auto &data = metadata.ValueMap();
|
||||
if (!data.count("scheme") || !data.count("principal") ||
|
||||
!data.count("credentials")) {
|
||||
LOG(WARNING) << "The client didn't supply authentication information!";
|
||||
return State::Close;
|
||||
}
|
||||
if (data["scheme"].ValueString() != "basic") {
|
||||
LOG(WARNING) << "Unsupported authentication scheme: "
|
||||
<< data["scheme"].ValueString();
|
||||
return State::Close;
|
||||
}
|
||||
|
||||
// Authenticate the user.
|
||||
if (!session.Authenticate(data["principal"].ValueString(),
|
||||
data["credentials"].ValueString())) {
|
||||
if (!session.encoder_.MessageFailure(
|
||||
{{"code", "Memgraph.ClientError.Security.Unauthenticated"},
|
||||
{"message", "Authentication failure"}})) {
|
||||
DLOG(WARNING) << "Couldn't send failure message to the client!";
|
||||
}
|
||||
// Throw an exception to indicate to the network stack that the session
|
||||
// should be closed and cleaned up.
|
||||
throw SessionClosedException("The client is not authenticated!");
|
||||
}
|
||||
|
||||
// Return success.
|
||||
if (!session.encoder_.MessageSuccess()) {
|
||||
DLOG(WARNING) << "Couldn't send success message to the client!";
|
||||
return State::Close;
|
||||
|
14
src/communication/exceptions.hpp
Normal file
14
src/communication/exceptions.hpp
Normal file
@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils/exceptions.hpp"
|
||||
|
||||
namespace communication {
|
||||
|
||||
/**
|
||||
* This exception is thrown to indicate to the communication stack that the
|
||||
* session is closed and that cleanup should be performed.
|
||||
*/
|
||||
class SessionClosedException : public utils::BasicException {
|
||||
using utils::BasicException::BasicException;
|
||||
};
|
||||
} // namespace communication
|
@ -15,22 +15,14 @@
|
||||
|
||||
#include "communication/buffer.hpp"
|
||||
#include "communication/context.hpp"
|
||||
#include "communication/exceptions.hpp"
|
||||
#include "communication/helpers.hpp"
|
||||
#include "io/network/socket.hpp"
|
||||
#include "io/network/stream_buffer.hpp"
|
||||
#include "utils/exceptions.hpp"
|
||||
#include "utils/thread/sync.hpp"
|
||||
|
||||
namespace communication {
|
||||
|
||||
/**
|
||||
* This exception is thrown to indicate to the communication stack that the
|
||||
* session is closed and that cleanup should be performed.
|
||||
*/
|
||||
class SessionClosedException : public utils::BasicException {
|
||||
using utils::BasicException::BasicException;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is used to provide input to user sessions. All sessions used with the
|
||||
* network stack should use this class as their input stream.
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include <gflags/gflags.h>
|
||||
#include <glog/logging.h>
|
||||
|
||||
#include "auth/auth.hpp"
|
||||
#include "communication/bolt/v1/session.hpp"
|
||||
#include "config.hpp"
|
||||
#include "database/distributed_graph_db.hpp"
|
||||
@ -67,6 +68,8 @@ DECLARE_string(durability_directory);
|
||||
struct SessionData {
|
||||
database::GraphDb &db;
|
||||
query::Interpreter interpreter{db};
|
||||
auth::Auth auth{
|
||||
std::experimental::filesystem::path(FLAGS_durability_directory) / "auth"};
|
||||
};
|
||||
|
||||
class BoltSession final
|
||||
@ -78,7 +81,8 @@ class BoltSession final
|
||||
: communication::bolt::Session<communication::InputStream,
|
||||
communication::OutputStream>(
|
||||
input_stream, output_stream),
|
||||
transaction_engine_(data.db, data.interpreter) {}
|
||||
transaction_engine_(data.db, data.interpreter),
|
||||
auth_(&data.auth) {}
|
||||
|
||||
using communication::bolt::Session<communication::InputStream,
|
||||
communication::OutputStream>::TEncoder;
|
||||
@ -118,6 +122,13 @@ class BoltSession final
|
||||
|
||||
void Abort() override { transaction_engine_.Abort(); }
|
||||
|
||||
bool Authenticate(const std::string &username,
|
||||
const std::string &password) override {
|
||||
if (!auth_->HasUsers()) return true;
|
||||
user_ = auth_->Authenticate(username, password);
|
||||
return !!user_;
|
||||
}
|
||||
|
||||
private:
|
||||
// Wrapper around TEncoder which converts TypedValue to Value
|
||||
// before forwarding the calls to original TEncoder.
|
||||
@ -139,6 +150,8 @@ class BoltSession final
|
||||
};
|
||||
|
||||
query::TransactionEngine transaction_engine_;
|
||||
auth::Auth *auth_;
|
||||
std::experimental::optional<auth::User> user_;
|
||||
};
|
||||
|
||||
using ServerT = communication::Server<BoltSession, SessionData>;
|
||||
@ -292,6 +305,7 @@ void SingleNodeMain() {
|
||||
LOG(ERROR) << e.what();
|
||||
}
|
||||
|
||||
session_data.interpreter.auth_ = &session_data.auth;
|
||||
session_data.interpreter.kafka_streams_ = &kafka_streams;
|
||||
|
||||
ServerContext context;
|
||||
|
@ -5,11 +5,13 @@
|
||||
#include "query/frontend/semantic/symbol_table.hpp"
|
||||
#include "query/parameters.hpp"
|
||||
|
||||
namespace integrations {
|
||||
namespace kafka {
|
||||
namespace auth {
|
||||
class Auth;
|
||||
} // namespace auth
|
||||
|
||||
namespace integrations::kafka {
|
||||
class Streams;
|
||||
} // namespace kafka
|
||||
} // namespace integrations
|
||||
} // namespace integrations::kafka
|
||||
|
||||
namespace query {
|
||||
|
||||
@ -32,6 +34,7 @@ class Context {
|
||||
bool is_index_created_ = false;
|
||||
int64_t timestamp_{-1};
|
||||
|
||||
auth::Auth *auth_ = nullptr;
|
||||
integrations::kafka::Streams *kafka_streams_ = nullptr;
|
||||
};
|
||||
|
||||
|
@ -64,6 +64,7 @@ Interpreter::Results Interpreter::operator()(
|
||||
ctx.timestamp_ = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
ctx.auth_ = auth_;
|
||||
ctx.kafka_streams_ = kafka_streams_;
|
||||
|
||||
// query -> stripped query
|
||||
|
@ -20,11 +20,13 @@ namespace distributed {
|
||||
class PlanDispatcher;
|
||||
}
|
||||
|
||||
namespace integrations {
|
||||
namespace kafka {
|
||||
namespace auth {
|
||||
class Auth;
|
||||
} // namespace auth
|
||||
|
||||
namespace integrations::kafka {
|
||||
class Streams;
|
||||
} // namespace kafka
|
||||
} // namespace integrations
|
||||
} // namespace integrations::kafka
|
||||
|
||||
namespace query {
|
||||
|
||||
@ -165,6 +167,7 @@ class Interpreter {
|
||||
const std::map<std::string, TypedValue> ¶ms,
|
||||
bool in_explicit_transaction);
|
||||
|
||||
auth::Auth *auth_ = nullptr;
|
||||
integrations::kafka::Streams *kafka_streams_ = nullptr;
|
||||
|
||||
private:
|
||||
|
@ -16,6 +16,7 @@
|
||||
#include "boost/serialization/export.hpp"
|
||||
#include "glog/logging.h"
|
||||
|
||||
#include "auth/auth.hpp"
|
||||
#include "communication/result_stream_faker.hpp"
|
||||
#include "database/distributed_graph_db.hpp"
|
||||
#include "database/graph_db_accessor.hpp"
|
||||
@ -34,6 +35,7 @@
|
||||
#include "utils/algorithm.hpp"
|
||||
#include "utils/exceptions.hpp"
|
||||
#include "utils/hashing/fnv.hpp"
|
||||
#include "utils/string.hpp"
|
||||
#include "utils/thread/sync.hpp"
|
||||
|
||||
DEFINE_HIDDEN_int32(remote_pull_sleep_micros, 10,
|
||||
@ -3903,20 +3905,59 @@ WITHOUT_SINGLE_INPUT(ModifyUser)
|
||||
|
||||
class ModifyUserCursor : public Cursor {
|
||||
public:
|
||||
bool Pull(Frame &frame, Context &context) override {
|
||||
if (context.in_explicit_transaction_) {
|
||||
ModifyUserCursor(const ModifyUser &self) : self_(self) {}
|
||||
|
||||
bool Pull(Frame &frame, Context &ctx) override {
|
||||
if (ctx.in_explicit_transaction_) {
|
||||
throw UserModificationInMulticommandTxException();
|
||||
}
|
||||
ExpressionEvaluator evaluator(frame, &context, GraphView::OLD);
|
||||
throw utils::NotYetImplemented("user auth");
|
||||
ExpressionEvaluator evaluator(frame, &ctx, GraphView::OLD);
|
||||
|
||||
TypedValue password_tv = self_.password()->Accept(evaluator);
|
||||
if (password_tv.type() != TypedValue::Type::String) {
|
||||
throw QueryRuntimeException(fmt::format(
|
||||
"Password must be a string, not '{}'", password_tv.type()));
|
||||
}
|
||||
|
||||
// All of the following operations are done with a lock.
|
||||
std::lock_guard<std::mutex> guard(ctx.auth_->WithLock());
|
||||
|
||||
std::experimental::optional<auth::User> user;
|
||||
if (self_.is_create()) {
|
||||
// Create a new user.
|
||||
user = ctx.auth_->AddUser(self_.username());
|
||||
if (!user) {
|
||||
throw QueryRuntimeException(
|
||||
fmt::format("User '{}' already exists!", self_.username()));
|
||||
}
|
||||
} else {
|
||||
// Update an existing user.
|
||||
auto user = ctx.auth_->GetUser(self_.username());
|
||||
if (!user) {
|
||||
throw QueryRuntimeException(
|
||||
fmt::format("User '{}' doesn't exist!", self_.username()));
|
||||
}
|
||||
}
|
||||
|
||||
// Set the password and save the user.
|
||||
user->UpdatePassword(password_tv.Value<std::string>());
|
||||
if (!ctx.auth_->SaveUser(*user)) {
|
||||
throw QueryRuntimeException(
|
||||
fmt::format("Couldn't save user '{}'!", self_.username()));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Reset() override { throw utils::NotYetImplemented("user auth"); }
|
||||
void Reset() override {}
|
||||
|
||||
private:
|
||||
const ModifyUser &self_;
|
||||
};
|
||||
|
||||
std::unique_ptr<Cursor> ModifyUser::MakeCursor(
|
||||
database::GraphDbAccessor &db) const {
|
||||
return std::make_unique<ModifyUserCursor>();
|
||||
database::GraphDbAccessor &) const {
|
||||
return std::make_unique<ModifyUserCursor>(*this);
|
||||
}
|
||||
|
||||
bool DropUser::Accept(HierarchicalLogicalOperatorVisitor &visitor) {
|
||||
@ -3927,21 +3968,51 @@ WITHOUT_SINGLE_INPUT(DropUser)
|
||||
|
||||
class DropUserCursor : public Cursor {
|
||||
public:
|
||||
DropUserCursor() {}
|
||||
DropUserCursor(const DropUser &self) : self_(self) {}
|
||||
|
||||
bool Pull(Frame &, Context &ctx) override {
|
||||
if (ctx.in_explicit_transaction_) {
|
||||
throw UserModificationInMulticommandTxException();
|
||||
}
|
||||
throw utils::NotYetImplemented("user auth");
|
||||
|
||||
// All of the following operations are done with a lock.
|
||||
std::lock_guard<std::mutex> guard(ctx.auth_->WithLock());
|
||||
|
||||
// Check if all users exist.
|
||||
for (const auto &username : self_.usernames()) {
|
||||
auto user = ctx.auth_->GetUser(username);
|
||||
if (!user) {
|
||||
throw QueryRuntimeException(
|
||||
fmt::format("User '{}' doesn't exist!", username));
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all users.
|
||||
std::vector<std::string> failed;
|
||||
for (const auto &username : self_.usernames()) {
|
||||
if (!ctx.auth_->RemoveUser(username)) {
|
||||
failed.push_back(username);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for failures.
|
||||
if (failed.size() > 0) {
|
||||
throw QueryRuntimeException(fmt::format("Couldn't remove users: '{}'!",
|
||||
utils::Join(failed, "', '")));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void Reset() override { throw utils::NotYetImplemented("user auth"); }
|
||||
void Reset() override {}
|
||||
|
||||
private:
|
||||
const DropUser &self_;
|
||||
};
|
||||
|
||||
std::unique_ptr<Cursor> DropUser::MakeCursor(
|
||||
database::GraphDbAccessor &db) const {
|
||||
return std::make_unique<DropUserCursor>();
|
||||
database::GraphDbAccessor &) const {
|
||||
return std::make_unique<DropUserCursor>(*this);
|
||||
}
|
||||
|
||||
CreateStream::CreateStream(std::string stream_name, Expression *stream_uri,
|
||||
|
@ -302,3 +302,8 @@ target_link_libraries(${test_prefix}utils_timestamp mg-utils)
|
||||
|
||||
add_unit_test(utils_watchdog.cpp)
|
||||
target_link_libraries(${test_prefix}utils_watchdog mg-utils)
|
||||
|
||||
# Test mg-auth
|
||||
|
||||
add_unit_test(auth.cpp)
|
||||
target_link_libraries(${test_prefix}auth mg-auth kvstore_lib)
|
||||
|
329
tests/unit/auth.cpp
Normal file
329
tests/unit/auth.cpp
Normal file
@ -0,0 +1,329 @@
|
||||
#include <iostream>
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <glog/logging.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "auth/auth.hpp"
|
||||
#include "auth/crypto.hpp"
|
||||
#include "utils/cast.hpp"
|
||||
#include "utils/file.hpp"
|
||||
|
||||
using namespace auth;
|
||||
namespace fs = std::experimental::filesystem;
|
||||
|
||||
class AuthWithStorage : public ::testing::Test {
|
||||
protected:
|
||||
virtual void SetUp() { utils::EnsureDir(test_folder_); }
|
||||
|
||||
virtual void TearDown() { fs::remove_all(test_folder_); }
|
||||
|
||||
fs::path test_folder_{
|
||||
fs::temp_directory_path() /
|
||||
("unit_auth_test_" + std::to_string(static_cast<int>(getpid())))};
|
||||
|
||||
Auth auth{test_folder_};
|
||||
};
|
||||
|
||||
TEST_F(AuthWithStorage, AddRole) {
|
||||
ASSERT_TRUE(auth.AddRole("admin"));
|
||||
ASSERT_TRUE(auth.AddRole("user"));
|
||||
ASSERT_FALSE(auth.AddRole("admin"));
|
||||
}
|
||||
|
||||
TEST_F(AuthWithStorage, RemoveRole) {
|
||||
ASSERT_TRUE(auth.AddRole("admin"));
|
||||
ASSERT_TRUE(auth.RemoveRole("admin"));
|
||||
ASSERT_FALSE(auth.RemoveRole("user"));
|
||||
ASSERT_FALSE(auth.RemoveRole("user"));
|
||||
}
|
||||
|
||||
TEST_F(AuthWithStorage, AddUser) {
|
||||
ASSERT_FALSE(auth.HasUsers());
|
||||
ASSERT_TRUE(auth.AddUser("test"));
|
||||
ASSERT_TRUE(auth.HasUsers());
|
||||
ASSERT_TRUE(auth.AddUser("test2"));
|
||||
ASSERT_FALSE(auth.AddUser("test"));
|
||||
}
|
||||
|
||||
TEST_F(AuthWithStorage, RemoveUser) {
|
||||
ASSERT_FALSE(auth.HasUsers());
|
||||
ASSERT_TRUE(auth.AddUser("test"));
|
||||
ASSERT_TRUE(auth.HasUsers());
|
||||
ASSERT_TRUE(auth.RemoveUser("test"));
|
||||
ASSERT_FALSE(auth.HasUsers());
|
||||
ASSERT_FALSE(auth.RemoveUser("test2"));
|
||||
ASSERT_FALSE(auth.RemoveUser("test"));
|
||||
ASSERT_FALSE(auth.HasUsers());
|
||||
}
|
||||
|
||||
TEST_F(AuthWithStorage, Authenticate) {
|
||||
ASSERT_FALSE(auth.HasUsers());
|
||||
|
||||
auto user = auth.AddUser("test");
|
||||
ASSERT_NE(user, std::experimental::nullopt);
|
||||
ASSERT_TRUE(auth.HasUsers());
|
||||
|
||||
ASSERT_THROW(auth.Authenticate("test", "123"), utils::BasicException);
|
||||
|
||||
user->UpdatePassword("123");
|
||||
ASSERT_TRUE(auth.SaveUser(*user));
|
||||
|
||||
ASSERT_NE(auth.Authenticate("test", "123"), std::experimental::nullopt);
|
||||
|
||||
ASSERT_EQ(auth.Authenticate("test", "456"), std::experimental::nullopt);
|
||||
ASSERT_NE(auth.Authenticate("test", "123"), std::experimental::nullopt);
|
||||
}
|
||||
|
||||
TEST_F(AuthWithStorage, UserRolePermissions) {
|
||||
ASSERT_FALSE(auth.HasUsers());
|
||||
ASSERT_TRUE(auth.AddUser("test"));
|
||||
ASSERT_TRUE(auth.HasUsers());
|
||||
|
||||
auto user = auth.GetUser("test");
|
||||
ASSERT_NE(user, std::experimental::nullopt);
|
||||
|
||||
// Test initial user permissions.
|
||||
ASSERT_EQ(user->permissions().Has(Permission::Read),
|
||||
PermissionLevel::Neutral);
|
||||
ASSERT_EQ(user->permissions().Has(Permission::Create),
|
||||
PermissionLevel::Neutral);
|
||||
ASSERT_EQ(user->permissions().Has(Permission::Update),
|
||||
PermissionLevel::Neutral);
|
||||
ASSERT_EQ(user->permissions().Has(Permission::Delete),
|
||||
PermissionLevel::Neutral);
|
||||
ASSERT_EQ(user->permissions(), user->GetPermissions());
|
||||
|
||||
// Change one user permission.
|
||||
user->permissions().Grant(Permission::Read);
|
||||
|
||||
// Check permissions.
|
||||
ASSERT_EQ(user->permissions().Has(Permission::Read), PermissionLevel::Grant);
|
||||
ASSERT_EQ(user->permissions().Has(Permission::Create),
|
||||
PermissionLevel::Neutral);
|
||||
ASSERT_EQ(user->permissions().Has(Permission::Update),
|
||||
PermissionLevel::Neutral);
|
||||
ASSERT_EQ(user->permissions().Has(Permission::Delete),
|
||||
PermissionLevel::Neutral);
|
||||
ASSERT_EQ(user->permissions(), user->GetPermissions());
|
||||
|
||||
// Create role.
|
||||
ASSERT_TRUE(auth.AddRole("admin"));
|
||||
auto role = auth.GetRole("admin");
|
||||
ASSERT_NE(role, std::experimental::nullopt);
|
||||
|
||||
// Assign permissions to role and role to user.
|
||||
role->permissions().Grant(Permission::Delete);
|
||||
user->SetRole(*role);
|
||||
|
||||
// Check permissions.
|
||||
{
|
||||
auto permissions = user->GetPermissions();
|
||||
ASSERT_EQ(permissions.Has(Permission::Read), PermissionLevel::Grant);
|
||||
ASSERT_EQ(permissions.Has(Permission::Delete), PermissionLevel::Grant);
|
||||
ASSERT_EQ(permissions.Has(Permission::Create), PermissionLevel::Neutral);
|
||||
ASSERT_EQ(permissions.Has(Permission::Update), PermissionLevel::Neutral);
|
||||
}
|
||||
|
||||
// Add explicit deny to role.
|
||||
role->permissions().Deny(Permission::Read);
|
||||
user->SetRole(*role);
|
||||
|
||||
// Check permissions.
|
||||
{
|
||||
auto permissions = user->GetPermissions();
|
||||
ASSERT_EQ(permissions.Has(Permission::Read), PermissionLevel::Deny);
|
||||
ASSERT_EQ(permissions.Has(Permission::Delete), PermissionLevel::Grant);
|
||||
ASSERT_EQ(permissions.Has(Permission::Create), PermissionLevel::Neutral);
|
||||
ASSERT_EQ(permissions.Has(Permission::Update), PermissionLevel::Neutral);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AuthWithStorage, RoleManipulations) {
|
||||
{
|
||||
auto user1 = auth.AddUser("user1");
|
||||
ASSERT_TRUE(user1);
|
||||
auto role1 = auth.AddRole("role1");
|
||||
ASSERT_TRUE(role1);
|
||||
user1->SetRole(*role1);
|
||||
auth.SaveUser(*user1);
|
||||
|
||||
auto user2 = auth.AddUser("user2");
|
||||
ASSERT_TRUE(user2);
|
||||
auto role2 = auth.AddRole("role2");
|
||||
ASSERT_TRUE(role2);
|
||||
user2->SetRole(*role2);
|
||||
auth.SaveUser(*user2);
|
||||
}
|
||||
|
||||
{
|
||||
auto user1 = auth.GetUser("user1");
|
||||
ASSERT_TRUE(user1);
|
||||
auto role1 = user1->role();
|
||||
ASSERT_TRUE(role1);
|
||||
ASSERT_EQ(role1->rolename(), "role1");
|
||||
|
||||
auto user2 = auth.GetUser("user2");
|
||||
ASSERT_TRUE(user2);
|
||||
auto role2 = user2->role();
|
||||
ASSERT_TRUE(role2);
|
||||
ASSERT_EQ(role2->rolename(), "role2");
|
||||
}
|
||||
|
||||
ASSERT_TRUE(auth.RemoveRole("role1"));
|
||||
|
||||
{
|
||||
auto user1 = auth.GetUser("user1");
|
||||
ASSERT_TRUE(user1);
|
||||
auto role = user1->role();
|
||||
ASSERT_FALSE(role);
|
||||
|
||||
auto user2 = auth.GetUser("user2");
|
||||
ASSERT_TRUE(user2);
|
||||
auto role2 = user2->role();
|
||||
ASSERT_TRUE(role2);
|
||||
ASSERT_EQ(role2->rolename(), "role2");
|
||||
}
|
||||
|
||||
{
|
||||
auto role1 = auth.AddRole("role1");
|
||||
ASSERT_TRUE(role1);
|
||||
}
|
||||
|
||||
{
|
||||
auto user1 = auth.GetUser("user1");
|
||||
ASSERT_TRUE(user1);
|
||||
auto role1 = user1->role();
|
||||
ASSERT_FALSE(role1);
|
||||
|
||||
auto user2 = auth.GetUser("user2");
|
||||
ASSERT_TRUE(user2);
|
||||
auto role2 = user2->role();
|
||||
ASSERT_TRUE(role2);
|
||||
ASSERT_EQ(role2->rolename(), "role2");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(AuthWithoutStorage, Permissions) {
|
||||
Permissions permissions;
|
||||
ASSERT_EQ(permissions.grants(), 0);
|
||||
ASSERT_EQ(permissions.denies(), 0);
|
||||
|
||||
permissions.Grant(Permission::Read);
|
||||
ASSERT_EQ(permissions.Has(Permission::Read), PermissionLevel::Grant);
|
||||
ASSERT_EQ(permissions.grants(), utils::UnderlyingCast(Permission::Read));
|
||||
ASSERT_EQ(permissions.denies(), 0);
|
||||
|
||||
permissions.Revoke(Permission::Read);
|
||||
ASSERT_EQ(permissions.Has(Permission::Read), PermissionLevel::Neutral);
|
||||
ASSERT_EQ(permissions.grants(), 0);
|
||||
ASSERT_EQ(permissions.denies(), 0);
|
||||
|
||||
permissions.Deny(Permission::Read);
|
||||
ASSERT_EQ(permissions.Has(Permission::Read), PermissionLevel::Deny);
|
||||
ASSERT_EQ(permissions.denies(), utils::UnderlyingCast(Permission::Read));
|
||||
ASSERT_EQ(permissions.grants(), 0);
|
||||
|
||||
permissions.Grant(Permission::Read);
|
||||
ASSERT_EQ(permissions.Has(Permission::Read), PermissionLevel::Grant);
|
||||
ASSERT_EQ(permissions.grants(), utils::UnderlyingCast(Permission::Read));
|
||||
ASSERT_EQ(permissions.denies(), 0);
|
||||
|
||||
permissions.Deny(Permission::Create);
|
||||
ASSERT_EQ(permissions.Has(Permission::Read), PermissionLevel::Grant);
|
||||
ASSERT_EQ(permissions.Has(Permission::Create), PermissionLevel::Deny);
|
||||
ASSERT_EQ(permissions.Has(Permission::Update), PermissionLevel::Neutral);
|
||||
ASSERT_EQ(permissions.grants(), utils::UnderlyingCast(Permission::Read));
|
||||
ASSERT_EQ(permissions.denies(), utils::UnderlyingCast(Permission::Create));
|
||||
|
||||
permissions.Grant(Permission::Delete);
|
||||
ASSERT_EQ(permissions.Has(Permission::Read), PermissionLevel::Grant);
|
||||
ASSERT_EQ(permissions.Has(Permission::Create), PermissionLevel::Deny);
|
||||
ASSERT_EQ(permissions.Has(Permission::Update), PermissionLevel::Neutral);
|
||||
ASSERT_EQ(permissions.Has(Permission::Delete), PermissionLevel::Grant);
|
||||
ASSERT_EQ(permissions.grants(),
|
||||
utils::UnderlyingCast(Permission::Read) |
|
||||
utils::UnderlyingCast(Permission::Delete));
|
||||
ASSERT_EQ(permissions.denies(), utils::UnderlyingCast(Permission::Create));
|
||||
|
||||
permissions.Revoke(Permission::Delete);
|
||||
ASSERT_EQ(permissions.Has(Permission::Read), PermissionLevel::Grant);
|
||||
ASSERT_EQ(permissions.Has(Permission::Create), PermissionLevel::Deny);
|
||||
ASSERT_EQ(permissions.Has(Permission::Update), PermissionLevel::Neutral);
|
||||
ASSERT_EQ(permissions.Has(Permission::Delete), PermissionLevel::Neutral);
|
||||
ASSERT_EQ(permissions.grants(), utils::UnderlyingCast(Permission::Read));
|
||||
ASSERT_EQ(permissions.denies(), utils::UnderlyingCast(Permission::Create));
|
||||
}
|
||||
|
||||
TEST(AuthWithoutStorage, PermissionsMaskTest) {
|
||||
Permissions p1(0, 0);
|
||||
ASSERT_EQ(p1.grants(), 0);
|
||||
ASSERT_EQ(p1.denies(), 0);
|
||||
|
||||
Permissions p2(1, 0);
|
||||
ASSERT_EQ(p2.grants(), 1);
|
||||
ASSERT_EQ(p2.denies(), 0);
|
||||
|
||||
Permissions p3(1, 1);
|
||||
ASSERT_EQ(p3.grants(), 0);
|
||||
ASSERT_EQ(p3.denies(), 1);
|
||||
|
||||
Permissions p4(3, 2);
|
||||
ASSERT_EQ(p4.grants(), 1);
|
||||
ASSERT_EQ(p4.denies(), 2);
|
||||
}
|
||||
|
||||
TEST(AuthWithoutStorage, UserSerializeDeserialize) {
|
||||
auto user = User("test");
|
||||
user.permissions().Grant(Permission::Read);
|
||||
user.permissions().Deny(Permission::Update);
|
||||
user.UpdatePassword("world");
|
||||
|
||||
auto data = user.Serialize();
|
||||
|
||||
auto output = User::Deserialize(data);
|
||||
ASSERT_EQ(user, output);
|
||||
}
|
||||
|
||||
TEST(AuthWithoutStorage, RoleSerializeDeserialize) {
|
||||
auto role = Role("test");
|
||||
role.permissions().Grant(Permission::Read);
|
||||
role.permissions().Deny(Permission::Update);
|
||||
|
||||
auto data = role.Serialize();
|
||||
|
||||
auto output = Role::Deserialize(data);
|
||||
ASSERT_EQ(role, output);
|
||||
}
|
||||
|
||||
TEST_F(AuthWithStorage, UserWithRoleSerializeDeserialize) {
|
||||
auto role = auth.AddRole("test");
|
||||
ASSERT_TRUE(role);
|
||||
role->permissions().Grant(Permission::Read);
|
||||
role->permissions().Deny(Permission::Update);
|
||||
auth.SaveRole(*role);
|
||||
|
||||
auto user = auth.AddUser("test");
|
||||
ASSERT_TRUE(user);
|
||||
user->permissions().Grant(Permission::Read);
|
||||
user->permissions().Deny(Permission::Update);
|
||||
user->UpdatePassword("world");
|
||||
user->SetRole(*role);
|
||||
auth.SaveUser(*user);
|
||||
|
||||
auto new_user = auth.GetUser("test");
|
||||
ASSERT_TRUE(new_user);
|
||||
ASSERT_EQ(*user, *new_user);
|
||||
}
|
||||
|
||||
TEST(AuthWithoutStorage, Crypto) {
|
||||
auto hash = EncryptPassword("hello");
|
||||
ASSERT_TRUE(VerifyPassword("hello", hash));
|
||||
ASSERT_FALSE(VerifyPassword("hello1", hash));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
@ -50,6 +50,11 @@ class TestSession : public Session<TestInputStream, TestOutputStream> {
|
||||
|
||||
void Abort() override {}
|
||||
|
||||
bool Authenticate(const std::string &username,
|
||||
const std::string &password) override {
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string query_;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user