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
|
import_library(librdkafka++ STATIC
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/librdkafka/lib/librdkafka++.a
|
${CMAKE_CURRENT_SOURCE_DIR}/librdkafka/lib/librdkafka++.a
|
||||||
librdkafka-proj)
|
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)
|
gflags_tag="b37ceb03a0e56c9f15ce80409438a555f8a67b7c" # custom version (May 6, 2017)
|
||||||
clone git://deps.memgraph.io/gflags.git gflags $gflags_tag
|
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
|
# neo4j
|
||||||
wget -nv http://deps.memgraph.io/neo4j-community-3.2.3-unix.tar.gz -O neo4j.tar.gz
|
wget -nv http://deps.memgraph.io/neo4j-community-3.2.3-unix.tar.gz -O neo4j.tar.gz
|
||||||
tar -xzf neo4j.tar.gz
|
tar -xzf neo4j.tar.gz
|
||||||
|
@ -7,6 +7,7 @@ add_subdirectory(integrations)
|
|||||||
add_subdirectory(io)
|
add_subdirectory(io)
|
||||||
add_subdirectory(telemetry)
|
add_subdirectory(telemetry)
|
||||||
add_subdirectory(communication)
|
add_subdirectory(communication)
|
||||||
|
add_subdirectory(auth)
|
||||||
|
|
||||||
# all memgraph src files
|
# all memgraph src files
|
||||||
set(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
|
antlr_opencypher_parser_lib dl glog gflags capnp kj
|
||||||
${Boost_IOSTREAMS_LIBRARY_RELEASE}
|
${Boost_IOSTREAMS_LIBRARY_RELEASE}
|
||||||
${Boost_SERIALIZATION_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)
|
if (USE_LTALLOC)
|
||||||
list(APPEND MEMGRAPH_ALL_LIBS 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. */
|
/** Aborts currently running query. */
|
||||||
virtual void Abort() = 0;
|
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.
|
* Executes the session after data has been read into the buffer.
|
||||||
* Goes through the bolt states in order to execute commands from the client.
|
* 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/codes.hpp"
|
||||||
#include "communication/bolt/v1/state.hpp"
|
#include "communication/bolt/v1/state.hpp"
|
||||||
#include "communication/bolt/v1/value.hpp"
|
#include "communication/bolt/v1/value.hpp"
|
||||||
|
#include "communication/exceptions.hpp"
|
||||||
#include "utils/likely.hpp"
|
#include "utils/likely.hpp"
|
||||||
|
|
||||||
namespace communication::bolt {
|
namespace communication::bolt {
|
||||||
@ -59,6 +60,33 @@ State StateInitRun(Session &session) {
|
|||||||
LOG(INFO) << fmt::format("Client connected '{}'", client_name.ValueString())
|
LOG(INFO) << fmt::format("Client connected '{}'", client_name.ValueString())
|
||||||
<< std::endl;
|
<< 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()) {
|
if (!session.encoder_.MessageSuccess()) {
|
||||||
DLOG(WARNING) << "Couldn't send success message to the client!";
|
DLOG(WARNING) << "Couldn't send success message to the client!";
|
||||||
return State::Close;
|
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/buffer.hpp"
|
||||||
#include "communication/context.hpp"
|
#include "communication/context.hpp"
|
||||||
|
#include "communication/exceptions.hpp"
|
||||||
#include "communication/helpers.hpp"
|
#include "communication/helpers.hpp"
|
||||||
#include "io/network/socket.hpp"
|
#include "io/network/socket.hpp"
|
||||||
#include "io/network/stream_buffer.hpp"
|
#include "io/network/stream_buffer.hpp"
|
||||||
#include "utils/exceptions.hpp"
|
|
||||||
#include "utils/thread/sync.hpp"
|
#include "utils/thread/sync.hpp"
|
||||||
|
|
||||||
namespace communication {
|
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
|
* This is used to provide input to user sessions. All sessions used with the
|
||||||
* network stack should use this class as their input stream.
|
* network stack should use this class as their input stream.
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
#include <gflags/gflags.h>
|
#include <gflags/gflags.h>
|
||||||
#include <glog/logging.h>
|
#include <glog/logging.h>
|
||||||
|
|
||||||
|
#include "auth/auth.hpp"
|
||||||
#include "communication/bolt/v1/session.hpp"
|
#include "communication/bolt/v1/session.hpp"
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
#include "database/distributed_graph_db.hpp"
|
#include "database/distributed_graph_db.hpp"
|
||||||
@ -67,6 +68,8 @@ DECLARE_string(durability_directory);
|
|||||||
struct SessionData {
|
struct SessionData {
|
||||||
database::GraphDb &db;
|
database::GraphDb &db;
|
||||||
query::Interpreter interpreter{db};
|
query::Interpreter interpreter{db};
|
||||||
|
auth::Auth auth{
|
||||||
|
std::experimental::filesystem::path(FLAGS_durability_directory) / "auth"};
|
||||||
};
|
};
|
||||||
|
|
||||||
class BoltSession final
|
class BoltSession final
|
||||||
@ -78,7 +81,8 @@ class BoltSession final
|
|||||||
: communication::bolt::Session<communication::InputStream,
|
: communication::bolt::Session<communication::InputStream,
|
||||||
communication::OutputStream>(
|
communication::OutputStream>(
|
||||||
input_stream, output_stream),
|
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,
|
using communication::bolt::Session<communication::InputStream,
|
||||||
communication::OutputStream>::TEncoder;
|
communication::OutputStream>::TEncoder;
|
||||||
@ -118,6 +122,13 @@ class BoltSession final
|
|||||||
|
|
||||||
void Abort() override { transaction_engine_.Abort(); }
|
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:
|
private:
|
||||||
// Wrapper around TEncoder which converts TypedValue to Value
|
// Wrapper around TEncoder which converts TypedValue to Value
|
||||||
// before forwarding the calls to original TEncoder.
|
// before forwarding the calls to original TEncoder.
|
||||||
@ -139,6 +150,8 @@ class BoltSession final
|
|||||||
};
|
};
|
||||||
|
|
||||||
query::TransactionEngine transaction_engine_;
|
query::TransactionEngine transaction_engine_;
|
||||||
|
auth::Auth *auth_;
|
||||||
|
std::experimental::optional<auth::User> user_;
|
||||||
};
|
};
|
||||||
|
|
||||||
using ServerT = communication::Server<BoltSession, SessionData>;
|
using ServerT = communication::Server<BoltSession, SessionData>;
|
||||||
@ -292,6 +305,7 @@ void SingleNodeMain() {
|
|||||||
LOG(ERROR) << e.what();
|
LOG(ERROR) << e.what();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session_data.interpreter.auth_ = &session_data.auth;
|
||||||
session_data.interpreter.kafka_streams_ = &kafka_streams;
|
session_data.interpreter.kafka_streams_ = &kafka_streams;
|
||||||
|
|
||||||
ServerContext context;
|
ServerContext context;
|
||||||
|
@ -5,11 +5,13 @@
|
|||||||
#include "query/frontend/semantic/symbol_table.hpp"
|
#include "query/frontend/semantic/symbol_table.hpp"
|
||||||
#include "query/parameters.hpp"
|
#include "query/parameters.hpp"
|
||||||
|
|
||||||
namespace integrations {
|
namespace auth {
|
||||||
namespace kafka {
|
class Auth;
|
||||||
|
} // namespace auth
|
||||||
|
|
||||||
|
namespace integrations::kafka {
|
||||||
class Streams;
|
class Streams;
|
||||||
} // namespace kafka
|
} // namespace integrations::kafka
|
||||||
} // namespace integrations
|
|
||||||
|
|
||||||
namespace query {
|
namespace query {
|
||||||
|
|
||||||
@ -32,6 +34,7 @@ class Context {
|
|||||||
bool is_index_created_ = false;
|
bool is_index_created_ = false;
|
||||||
int64_t timestamp_{-1};
|
int64_t timestamp_{-1};
|
||||||
|
|
||||||
|
auth::Auth *auth_ = nullptr;
|
||||||
integrations::kafka::Streams *kafka_streams_ = nullptr;
|
integrations::kafka::Streams *kafka_streams_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ Interpreter::Results Interpreter::operator()(
|
|||||||
ctx.timestamp_ = std::chrono::duration_cast<std::chrono::milliseconds>(
|
ctx.timestamp_ = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
std::chrono::system_clock::now().time_since_epoch())
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
.count();
|
.count();
|
||||||
|
ctx.auth_ = auth_;
|
||||||
ctx.kafka_streams_ = kafka_streams_;
|
ctx.kafka_streams_ = kafka_streams_;
|
||||||
|
|
||||||
// query -> stripped query
|
// query -> stripped query
|
||||||
|
@ -20,11 +20,13 @@ namespace distributed {
|
|||||||
class PlanDispatcher;
|
class PlanDispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace integrations {
|
namespace auth {
|
||||||
namespace kafka {
|
class Auth;
|
||||||
|
} // namespace auth
|
||||||
|
|
||||||
|
namespace integrations::kafka {
|
||||||
class Streams;
|
class Streams;
|
||||||
} // namespace kafka
|
} // namespace integrations::kafka
|
||||||
} // namespace integrations
|
|
||||||
|
|
||||||
namespace query {
|
namespace query {
|
||||||
|
|
||||||
@ -165,6 +167,7 @@ class Interpreter {
|
|||||||
const std::map<std::string, TypedValue> ¶ms,
|
const std::map<std::string, TypedValue> ¶ms,
|
||||||
bool in_explicit_transaction);
|
bool in_explicit_transaction);
|
||||||
|
|
||||||
|
auth::Auth *auth_ = nullptr;
|
||||||
integrations::kafka::Streams *kafka_streams_ = nullptr;
|
integrations::kafka::Streams *kafka_streams_ = nullptr;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
#include "boost/serialization/export.hpp"
|
#include "boost/serialization/export.hpp"
|
||||||
#include "glog/logging.h"
|
#include "glog/logging.h"
|
||||||
|
|
||||||
|
#include "auth/auth.hpp"
|
||||||
#include "communication/result_stream_faker.hpp"
|
#include "communication/result_stream_faker.hpp"
|
||||||
#include "database/distributed_graph_db.hpp"
|
#include "database/distributed_graph_db.hpp"
|
||||||
#include "database/graph_db_accessor.hpp"
|
#include "database/graph_db_accessor.hpp"
|
||||||
@ -34,6 +35,7 @@
|
|||||||
#include "utils/algorithm.hpp"
|
#include "utils/algorithm.hpp"
|
||||||
#include "utils/exceptions.hpp"
|
#include "utils/exceptions.hpp"
|
||||||
#include "utils/hashing/fnv.hpp"
|
#include "utils/hashing/fnv.hpp"
|
||||||
|
#include "utils/string.hpp"
|
||||||
#include "utils/thread/sync.hpp"
|
#include "utils/thread/sync.hpp"
|
||||||
|
|
||||||
DEFINE_HIDDEN_int32(remote_pull_sleep_micros, 10,
|
DEFINE_HIDDEN_int32(remote_pull_sleep_micros, 10,
|
||||||
@ -3903,20 +3905,59 @@ WITHOUT_SINGLE_INPUT(ModifyUser)
|
|||||||
|
|
||||||
class ModifyUserCursor : public Cursor {
|
class ModifyUserCursor : public Cursor {
|
||||||
public:
|
public:
|
||||||
bool Pull(Frame &frame, Context &context) override {
|
ModifyUserCursor(const ModifyUser &self) : self_(self) {}
|
||||||
if (context.in_explicit_transaction_) {
|
|
||||||
|
bool Pull(Frame &frame, Context &ctx) override {
|
||||||
|
if (ctx.in_explicit_transaction_) {
|
||||||
throw UserModificationInMulticommandTxException();
|
throw UserModificationInMulticommandTxException();
|
||||||
}
|
}
|
||||||
ExpressionEvaluator evaluator(frame, &context, GraphView::OLD);
|
ExpressionEvaluator evaluator(frame, &ctx, GraphView::OLD);
|
||||||
throw utils::NotYetImplemented("user auth");
|
|
||||||
|
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(
|
std::unique_ptr<Cursor> ModifyUser::MakeCursor(
|
||||||
database::GraphDbAccessor &db) const {
|
database::GraphDbAccessor &) const {
|
||||||
return std::make_unique<ModifyUserCursor>();
|
return std::make_unique<ModifyUserCursor>(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DropUser::Accept(HierarchicalLogicalOperatorVisitor &visitor) {
|
bool DropUser::Accept(HierarchicalLogicalOperatorVisitor &visitor) {
|
||||||
@ -3927,21 +3968,51 @@ WITHOUT_SINGLE_INPUT(DropUser)
|
|||||||
|
|
||||||
class DropUserCursor : public Cursor {
|
class DropUserCursor : public Cursor {
|
||||||
public:
|
public:
|
||||||
DropUserCursor() {}
|
DropUserCursor(const DropUser &self) : self_(self) {}
|
||||||
|
|
||||||
bool Pull(Frame &, Context &ctx) override {
|
bool Pull(Frame &, Context &ctx) override {
|
||||||
if (ctx.in_explicit_transaction_) {
|
if (ctx.in_explicit_transaction_) {
|
||||||
throw UserModificationInMulticommandTxException();
|
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(
|
std::unique_ptr<Cursor> DropUser::MakeCursor(
|
||||||
database::GraphDbAccessor &db) const {
|
database::GraphDbAccessor &) const {
|
||||||
return std::make_unique<DropUserCursor>();
|
return std::make_unique<DropUserCursor>(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateStream::CreateStream(std::string stream_name, Expression *stream_uri,
|
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)
|
add_unit_test(utils_watchdog.cpp)
|
||||||
target_link_libraries(${test_prefix}utils_watchdog mg-utils)
|
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 {}
|
void Abort() override {}
|
||||||
|
|
||||||
|
bool Authenticate(const std::string &username,
|
||||||
|
const std::string &password) override {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string query_;
|
std::string query_;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user