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:
Matej Ferencevic 2018-07-27 10:54:20 +02:00
parent 7e92a7f41c
commit 2ecb660790
22 changed files with 1044 additions and 31 deletions

View File

@ -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)

View File

@ -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

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@ -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.

View File

@ -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;

View 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

View File

@ -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.

View File

@ -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;

View File

@ -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;
};

View File

@ -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

View File

@ -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> &params,
bool in_explicit_transaction);
auth::Auth *auth_ = nullptr;
integrations::kafka::Streams *kafka_streams_ = nullptr;
private:

View File

@ -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,

View File

@ -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
View 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();
}

View File

@ -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_;
};