From 94ad18326c499347dcb63d4bc4ba1c13a3660d50 Mon Sep 17 00:00:00 2001 From: Matej Ferencevic <matej.ferencevic@memgraph.io> Date: Tue, 14 Aug 2018 11:34:00 +0200 Subject: [PATCH] Implement leftover Auth queries Reviewers: mtomic, buda Reviewed By: mtomic Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D1535 --- src/auth/auth.cpp | 117 ++++-- src/auth/auth.hpp | 106 +++++- src/auth/crypto.cpp | 8 +- src/auth/exceptions.hpp | 15 + src/auth/models.cpp | 96 ++++- src/auth/models.hpp | 47 ++- src/query/frontend/ast/ast.capnp | 6 +- src/query/frontend/ast/ast.cpp | 12 + src/query/frontend/ast/ast.hpp | 22 +- .../frontend/ast/cypher_main_visitor.cpp | 26 +- .../opencypher/grammar/MemgraphCypher.g4 | 6 +- src/query/plan/operator.cpp | 348 +++++++++++++++--- src/query/plan/operator.lcp | 21 +- src/query/plan/rule_based_planner.hpp | 6 +- src/storage/kvstore.cpp | 32 ++ src/storage/kvstore.hpp | 35 ++ src/storage/kvstore_dummy.cpp | 18 + tests/unit/auth.cpp | 287 +++++++++++---- tests/unit/cypher_main_visitor.cpp | 3 +- tests/unit/kvstore.cpp | 29 ++ 20 files changed, 1045 insertions(+), 195 deletions(-) create mode 100644 src/auth/exceptions.hpp diff --git a/src/auth/auth.cpp b/src/auth/auth.cpp index 01a763fb8..15f50da29 100644 --- a/src/auth/auth.cpp +++ b/src/auth/auth.cpp @@ -1,6 +1,6 @@ #include "auth/auth.hpp" -#include "utils/string.hpp" +#include "auth/exceptions.hpp" namespace auth { @@ -44,7 +44,7 @@ std::experimental::optional<User> Auth::GetUser(const std::string &username) { try { data = nlohmann::json::parse(*existing_user); } catch (const nlohmann::json::parse_error &e) { - throw utils::BasicException("Couldn't load user data!"); + throw AuthException("Couldn't load user data!"); } auto user = User::Deserialize(data); @@ -59,38 +59,59 @@ std::experimental::optional<User> Auth::GetUser(const std::string &username) { return user; } -bool Auth::SaveUser(const User &user) { - if (!storage_.Put(kUserPrefix + user.username(), user.Serialize().dump())) { - return false; - } +void Auth::SaveUser(const User &user) { + bool success = false; if (user.role()) { - return storage_.Put(kLinkPrefix + user.username(), user.role()->rolename()); + success = storage_.PutMultiple( + {{kUserPrefix + user.username(), user.Serialize().dump()}, + {kLinkPrefix + user.username(), user.role()->rolename()}}); } else { - return storage_.Delete(kLinkPrefix + user.username()); + success = storage_.PutAndDeleteMultiple( + {{kUserPrefix + user.username(), user.Serialize().dump()}}, + {kLinkPrefix + user.username()}); + } + if (!success) { + throw AuthException("Couldn't save user '{}'!", user.username()); } } -std::experimental::optional<User> Auth::AddUser(const std::string &username) { +std::experimental::optional<User> Auth::AddUser( + const std::string &username, + const std::experimental::optional<std::string> &password) { auto existing_user = GetUser(username); if (existing_user) return std::experimental::nullopt; + auto existing_role = GetRole(username); + if (existing_role) return std::experimental::nullopt; auto new_user = User(username); - if (!SaveUser(new_user)) return std::experimental::nullopt; + new_user.UpdatePassword(password); + SaveUser(new_user); 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); + std::vector<std::string> keys( + {kLinkPrefix + username, kUserPrefix + username}); + if (!storage_.DeleteMultiple(keys)) { + throw AuthException("Couldn't remove user '{}'!", username); + } + return true; +} + +std::vector<auth::User> Auth::AllUsers() { + std::vector<auth::User> ret; + for (auto it = storage_.begin(kUserPrefix); it != storage_.end(kUserPrefix); + ++it) { + auto user = GetUser(it->first.substr(kUserPrefix.size())); + if (user) { + ret.push_back(*user); + } + } + return ret; } bool Auth::HasUsers() { - for (auto it = storage_.begin(); it != storage_.end(); ++it) { - if (utils::StartsWith(it->first, kUserPrefix)) { - return true; - } - } - return false; + return storage_.begin(kUserPrefix) != storage_.end(kUserPrefix); } std::experimental::optional<Role> Auth::GetRole(const std::string &rolename) { @@ -101,36 +122,74 @@ std::experimental::optional<Role> Auth::GetRole(const std::string &rolename) { try { data = nlohmann::json::parse(*existing_role); } catch (const nlohmann::json::parse_error &e) { - throw utils::BasicException("Couldn't load role data!"); + throw AuthException("Couldn't load role data!"); } return Role::Deserialize(data); } -bool Auth::SaveRole(const Role &role) { - return storage_.Put(kRolePrefix + role.rolename(), role.Serialize().dump()); +void Auth::SaveRole(const Role &role) { + if (!storage_.Put(kRolePrefix + role.rolename(), role.Serialize().dump())) { + throw AuthException("Couldn't save role '{}'!", role.rolename()); + } } std::experimental::optional<Role> Auth::AddRole(const std::string &rolename) { auto existing_role = GetRole(rolename); if (existing_role) return std::experimental::nullopt; + auto existing_user = GetUser(rolename); + if (existing_user) return std::experimental::nullopt; auto new_role = Role(rolename); - if (!SaveRole(new_role)) return std::experimental::nullopt; + SaveRole(new_role); 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); + std::vector<std::string> keys; + for (auto it = storage_.begin(kLinkPrefix); it != storage_.end(kLinkPrefix); + ++it) { + if (it->second == rolename) { + keys.push_back(it->first); } } - for (const auto &link : links) { - storage_.Delete(link); + keys.push_back(kRolePrefix + rolename); + if (!storage_.DeleteMultiple(keys)) { + throw AuthException("Couldn't remove role '{}'!", rolename); } - return storage_.Delete(kRolePrefix + rolename); + return true; +} + +std::vector<auth::Role> Auth::AllRoles() { + std::vector<auth::Role> ret; + for (auto it = storage_.begin(kRolePrefix); it != storage_.end(kRolePrefix); + ++it) { + auto rolename = it->first.substr(kRolePrefix.size()); + auto role = GetRole(rolename); + if (role) { + ret.push_back(*role); + } else { + throw AuthException("Couldn't load role '{}'!", rolename); + } + } + return ret; +} + +std::vector<auth::User> Auth::AllUsersForRole(const std::string &rolename) { + std::vector<auth::User> ret; + for (auto it = storage_.begin(kLinkPrefix); it != storage_.end(kLinkPrefix); + ++it) { + auto username = it->first.substr(kLinkPrefix.size()); + if (it->second == rolename) { + auto user = GetUser(username); + if (user) { + ret.push_back(*user); + } else { + throw AuthException("Couldn't load user '{}'!", username); + } + } + } + return ret; } std::mutex &Auth::WithLock() { return lock_; } diff --git a/src/auth/auth.hpp b/src/auth/auth.hpp index 9590cfdd4..55060f66a 100644 --- a/src/auth/auth.hpp +++ b/src/auth/auth.hpp @@ -2,7 +2,9 @@ #include <experimental/optional> #include <mutex> +#include <vector> +#include "auth/exceptions.hpp" #include "auth/models.hpp" #include "storage/kvstore.hpp" @@ -18,27 +20,125 @@ class Auth final { public: Auth(const std::string &storage_directory); + /** + * Authenticates a user using his username and password. + * + * @param username + * @param password + * + * @return a user when the username and password match, nullopt otherwise + */ std::experimental::optional<User> Authenticate(const std::string &username, const std::string &password); + /** + * Gets a user from the storage. + * + * @param username + * + * @return a user when the user exists, nullopt otherwise + */ std::experimental::optional<User> GetUser(const std::string &username); - bool SaveUser(const User &user); + /** + * Saves a user object to the storage. + * + * @param user + */ + void SaveUser(const User &user); - std::experimental::optional<User> AddUser(const std::string &username); + /** + * Creates a user if the user doesn't exist. + * + * @param username + * @param password + * + * @return a user when the user is created, nullopt if the user exists + */ + std::experimental::optional<User> AddUser( + const std::string &username, + const std::experimental::optional<std::string> &password = + std::experimental::nullopt); + /** + * Removes a user from the storage. + * + * @param username + * + * @return `true` if the user existed and was removed, `false` if the user + * doesn't exist + */ bool RemoveUser(const std::string &username); + /** + * Gets all users from the storage. + * + * @return a list of users + */ + std::vector<User> AllUsers(); + + /** + * Returns whether there are users in the storage. + * + * @return `true` if the storage contains any users, `false` otherwise + */ bool HasUsers(); + /** + * Gets a role from the storage. + * + * @param rolename + * + * @return a role when the role exists, nullopt otherwise + */ std::experimental::optional<Role> GetRole(const std::string &rolename); - bool SaveRole(const Role &role); + /** + * Saves a role object to the storage. + * + * @param role + */ + void SaveRole(const Role &role); + /** + * Creates a role if the role doesn't exist. + * + * @param rolename + * + * @return a role when the role is created, nullopt if the role exists + */ std::experimental::optional<Role> AddRole(const std::string &rolename); + /** + * Removes a role from the storage. + * + * @param rolename + * + * @return `true` if the role existed and was removed, `false` if the role + * doesn't exist + */ bool RemoveRole(const std::string &rolename); + /** + * Gets all roles from the storage. + * + * @return a list of roles + */ + std::vector<Role> AllRoles(); + + /** + * Gets all users for a role from the storage. + * + * @param rolename + * + * @return a list of roles + */ + std::vector<User> AllUsersForRole(const std::string &rolename); + + /** + * Returns a reference to the lock that should be used for all operations that + * require more than one interaction with this class. + */ std::mutex &WithLock(); private: diff --git a/src/auth/crypto.cpp b/src/auth/crypto.cpp index 7dabe09b7..1362ec2f4 100644 --- a/src/auth/crypto.cpp +++ b/src/auth/crypto.cpp @@ -2,7 +2,7 @@ #include <libbcrypt/bcrypt.h> -#include "utils/exceptions.hpp" +#include "auth/exceptions.hpp" namespace auth { @@ -14,11 +14,11 @@ const std::string EncryptPassword(const std::string &password) { // 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!"); + throw AuthException("Couldn't generate hashing salt!"); } if (bcrypt_hashpw(password.c_str(), salt, hash) != 0) { - throw utils::BasicException("Couldn't hash password!"); + throw AuthException("Couldn't hash password!"); } return std::string(hash); @@ -27,7 +27,7 @@ const std::string EncryptPassword(const std::string &password) { 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!"); + throw AuthException("Couldn't check password!"); } return ret == 0; } diff --git a/src/auth/exceptions.hpp b/src/auth/exceptions.hpp new file mode 100644 index 000000000..964764233 --- /dev/null +++ b/src/auth/exceptions.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "utils/exceptions.hpp" + +namespace auth { + +/** + * This exception class is thrown for all exceptions that can occur when dealing + * with the Auth library. + */ +class AuthException : public utils::BasicException { + public: + using utils::BasicException::BasicException; +}; +} // namespace auth diff --git a/src/auth/models.cpp b/src/auth/models.cpp index 6a4555918..20c3c1140 100644 --- a/src/auth/models.cpp +++ b/src/auth/models.cpp @@ -1,11 +1,45 @@ #include "auth/models.hpp" +#include <regex> + +#include <gflags/gflags.h> + #include "auth/crypto.hpp" +#include "auth/exceptions.hpp" #include "utils/cast.hpp" -#include "utils/exceptions.hpp" + +DEFINE_bool(auth_password_permit_null, true, + "Set to false to disable null passwords."); + +DEFINE_string(auth_password_strength_regex, ".+", + "The regular expression that should be used to match the entire " + "entered password to ensure its strength."); namespace auth { +std::string PermissionToString(Permission permission) { + switch (permission) { + case Permission::MATCH: + return "MATCH"; + case Permission::CREATE: + return "CREATE"; + case Permission::MERGE: + return "MERGE"; + case Permission::DELETE: + return "DELETE"; + case Permission::SET: + return "SET"; + case Permission::REMOVE: + return "REMOVE"; + case Permission::INDEX: + return "INDEX"; + case Permission::AUTH: + return "AUTH"; + case Permission::STREAM: + return "STREAM"; + } +} + Permissions::Permissions(uint64_t grants, uint64_t denies) { // The deny bitmask has higher priority than the grant bitmask. denies_ = denies; @@ -16,11 +50,11 @@ Permissions::Permissions(uint64_t grants, uint64_t 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; + return PermissionLevel::DENY; } else if (grants_ & utils::UnderlyingCast(permission)) { - return PermissionLevel::Grant; + return PermissionLevel::GRANT; } - return PermissionLevel::Neutral; + return PermissionLevel::NEUTRAL; } void Permissions::Grant(Permission permission) { @@ -44,6 +78,26 @@ void Permissions::Deny(Permission permission) { grants_ &= ~utils::UnderlyingCast(permission); } +std::vector<Permission> Permissions::GetGrants() const { + std::vector<Permission> ret; + for (const auto &permission : kPermissionsAll) { + if (Has(permission) == PermissionLevel::GRANT) { + ret.push_back(permission); + } + } + return ret; +} + +std::vector<Permission> Permissions::GetDenies() const { + std::vector<Permission> ret; + for (const auto &permission : kPermissionsAll) { + if (Has(permission) == PermissionLevel::DENY) { + ret.push_back(permission); + } + } + return ret; +} + nlohmann::json Permissions::Serialize() const { nlohmann::json data = nlohmann::json::object(); data["grants"] = grants_; @@ -53,11 +107,11 @@ nlohmann::json Permissions::Serialize() const { Permissions Permissions::Deserialize(const nlohmann::json &data) { if (!data.is_object()) { - throw utils::BasicException("Couldn't load permissions data!"); + throw AuthException("Couldn't load permissions data!"); } if (!data["grants"].is_number_unsigned() || !data["denies"].is_number_unsigned()) { - throw utils::BasicException("Couldn't load permissions data!"); + throw AuthException("Couldn't load permissions data!"); } return {data["grants"], data["denies"]}; } @@ -91,10 +145,10 @@ nlohmann::json Role::Serialize() const { Role Role::Deserialize(const nlohmann::json &data) { if (!data.is_object()) { - throw utils::BasicException("Couldn't load role data!"); + throw AuthException("Couldn't load role data!"); } if (!data["rolename"].is_string() || !data["permissions"].is_object()) { - throw utils::BasicException("Couldn't load role data!"); + throw AuthException("Couldn't load role data!"); } auto permissions = Permissions::Deserialize(data["permissions"]); return {data["rolename"], permissions}; @@ -114,15 +168,33 @@ User::User(const std::string &username, const std::string &password_hash, permissions_(permissions) {} bool User::CheckPassword(const std::string &password) { + if (password_hash_ == "") return true; return VerifyPassword(password, password_hash_); } -void User::UpdatePassword(const std::string &password) { - password_hash_ = EncryptPassword(password); +void User::UpdatePassword( + const std::experimental::optional<std::string> &password) { + if (password) { + std::regex re(FLAGS_auth_password_strength_regex); + if (!std::regex_match(*password, re)) { + throw AuthException( + "The user password doesn't conform to the required strength! Regex: " + "{}", + FLAGS_auth_password_strength_regex); + } + password_hash_ = EncryptPassword(*password); + } else { + if (!FLAGS_auth_password_permit_null) { + throw AuthException("Null passwords aren't permitted!"); + } + password_hash_ = ""; + } } void User::SetRole(const Role &role) { role_.emplace(role); } +void User::ClearRole() { role_ = std::experimental::nullopt; } + const Permissions User::GetPermissions() const { if (role_) { return Permissions(permissions_.grants() | role_->permissions().grants(), @@ -148,11 +220,11 @@ nlohmann::json User::Serialize() const { User User::Deserialize(const nlohmann::json &data) { if (!data.is_object()) { - throw utils::BasicException("Couldn't load user data!"); + throw AuthException("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!"); + throw AuthException("Couldn't load user data!"); } auto permissions = Permissions::Deserialize(data["permissions"]); return {data["username"], data["password_hash"], permissions}; diff --git a/src/auth/models.hpp b/src/auth/models.hpp index fc80021dd..8c9cd25c1 100644 --- a/src/auth/models.hpp +++ b/src/auth/models.hpp @@ -7,25 +7,36 @@ namespace auth { -// TODO (mferencevic): Add permissions for admin actions. +// These permissions must have values that are applicable for usage in a +// bitmask. enum class Permission : uint64_t { - Read = 0x00000001, - - Create = 0x00000002, - - Update = 0x00000004, - - Delete = 0x00000008, + MATCH = 0x00000001, + CREATE = 0x00000002, + MERGE = 0x00000004, + DELETE = 0x00000008, + SET = 0x00000010, + REMOVE = 0x00000020, + INDEX = 0x00000040, + AUTH = 0x00010000, + STREAM = 0x00020000, }; +// Constant list of all available permissions. +const std::vector<Permission> kPermissionsAll = { + Permission::MATCH, Permission::CREATE, Permission::MERGE, + Permission::DELETE, Permission::SET, Permission::REMOVE, + Permission::INDEX, Permission::AUTH, Permission::STREAM}; + +// Function that converts a permission to its string representation. +std::string PermissionToString(Permission permission); + +// Class that indicates what permission level the user/role has. enum class PermissionLevel { - Grant, - Neutral, - Deny, + GRANT, + NEUTRAL, + DENY, }; -// TODO (mferencevic): Add string conversions to/from permissions. - class Permissions final { public: Permissions(uint64_t grants = 0, uint64_t denies = 0); @@ -38,6 +49,10 @@ class Permissions final { void Deny(Permission permission); + std::vector<Permission> GetGrants() const; + + std::vector<Permission> GetDenies() const; + nlohmann::json Serialize() const; static Permissions Deserialize(const nlohmann::json &data); @@ -77,7 +92,6 @@ class Role final { bool operator==(const Role &first, const Role &second); -// TODO (mferencevic): Implement password strength enforcement. // TODO (mferencevic): Implement password expiry. class User final { public: @@ -88,10 +102,13 @@ class User final { bool CheckPassword(const std::string &password); - void UpdatePassword(const std::string &password); + void UpdatePassword(const std::experimental::optional<std::string> &password = + std::experimental::nullopt); void SetRole(const Role &role); + void ClearRole(); + const Permissions GetPermissions() const; const std::string &username() const; diff --git a/src/query/frontend/ast/ast.capnp b/src/query/frontend/ast/ast.capnp index 1c48259f1..2368fc9fb 100644 --- a/src/query/frontend/ast/ast.capnp +++ b/src/query/frontend/ast/ast.capnp @@ -421,8 +421,10 @@ struct AuthQuery { match @2; merge @3; set @4; - auth @5; - stream @6; + remove @5; + index @6; + auth @7; + stream @8; } action @0 :Action; user @1 :Text; diff --git a/src/query/frontend/ast/ast.cpp b/src/query/frontend/ast/ast.cpp index ce604be2d..f417bb9b4 100644 --- a/src/query/frontend/ast/ast.cpp +++ b/src/query/frontend/ast/ast.cpp @@ -2146,6 +2146,12 @@ void AuthQuery::Save(capnp::AuthQuery::Builder *builder, case Privilege::SET: privileges_builder.set(i, capnp::AuthQuery::Privilege::SET); break; + case Privilege::REMOVE: + privileges_builder.set(i, capnp::AuthQuery::Privilege::REMOVE); + break; + case Privilege::INDEX: + privileges_builder.set(i, capnp::AuthQuery::Privilege::INDEX); + break; case Privilege::AUTH: privileges_builder.set(i, capnp::AuthQuery::Privilege::AUTH); break; @@ -2232,6 +2238,12 @@ void AuthQuery::Load(const capnp::Tree::Reader &base_reader, case capnp::AuthQuery::Privilege::SET: privileges_.push_back(Privilege::SET); break; + case capnp::AuthQuery::Privilege::REMOVE: + privileges_.push_back(Privilege::REMOVE); + break; + case capnp::AuthQuery::Privilege::INDEX: + privileges_.push_back(Privilege::INDEX); + break; case capnp::AuthQuery::Privilege::AUTH: privileges_.push_back(Privilege::AUTH); break; diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp index e829def1e..96da6b840 100644 --- a/src/query/frontend/ast/ast.hpp +++ b/src/query/frontend/ast/ast.hpp @@ -2361,7 +2361,19 @@ class AuthQuery : public Clause { SHOW_USERS_FOR_ROLE }; - enum class Privilege { CREATE, DELETE, MATCH, MERGE, SET, AUTH, STREAM }; + // When adding new privileges, please add them to the `kPrivilegesAll` + // constant. + enum class Privilege { + CREATE, + DELETE, + MATCH, + MERGE, + SET, + REMOVE, + INDEX, + AUTH, + STREAM + }; Action action_; std::string user_; @@ -2405,6 +2417,14 @@ class AuthQuery : public Clause { std::vector<int> *loaded_uids) override; }; +// Constant that holds all available privileges. +const std::vector<AuthQuery::Privilege> kPrivilegesAll = { + AuthQuery::Privilege::CREATE, AuthQuery::Privilege::DELETE, + AuthQuery::Privilege::MATCH, AuthQuery::Privilege::MERGE, + AuthQuery::Privilege::SET, AuthQuery::Privilege::REMOVE, + AuthQuery::Privilege::INDEX, AuthQuery::Privilege::AUTH, + AuthQuery::Privilege::STREAM}; + class CreateStream : public Clause { friend class AstStorage; diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index 86f3a1e57..62f6dd957 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -386,8 +386,13 @@ antlrcpp::Any CypherMainVisitor::visitGrantPrivilege( AuthQuery *auth = storage_.Create<AuthQuery>(); auth->action_ = AuthQuery::Action::GRANT_PRIVILEGE; auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>(); - for (auto *privilege : ctx->privilegeList()->privilege()) { - auth->privileges_.push_back(privilege->accept(this)); + if (ctx->privilegeList()) { + for (auto *privilege : ctx->privilegeList()->privilege()) { + auth->privileges_.push_back(privilege->accept(this)); + } + } else { + /* grant all privileges */ + auth->privileges_ = kPrivilegesAll; } return auth; } @@ -400,8 +405,13 @@ antlrcpp::Any CypherMainVisitor::visitDenyPrivilege( AuthQuery *auth = storage_.Create<AuthQuery>(); auth->action_ = AuthQuery::Action::DENY_PRIVILEGE; auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>(); - for (auto *privilege : ctx->privilegeList()->privilege()) { - auth->privileges_.push_back(privilege->accept(this)); + if (ctx->privilegeList()) { + for (auto *privilege : ctx->privilegeList()->privilege()) { + auth->privileges_.push_back(privilege->accept(this)); + } + } else { + /* deny all privileges */ + auth->privileges_ = kPrivilegesAll; } return auth; } @@ -420,11 +430,7 @@ antlrcpp::Any CypherMainVisitor::visitRevokePrivilege( } } else { /* revoke all privileges */ - auth->privileges_ = { - AuthQuery::Privilege::CREATE, AuthQuery::Privilege::DELETE, - AuthQuery::Privilege::MATCH, AuthQuery::Privilege::MERGE, - AuthQuery::Privilege::SET, AuthQuery::Privilege::AUTH, - AuthQuery::Privilege::STREAM}; + auth->privileges_ = kPrivilegesAll; } return auth; } @@ -439,6 +445,8 @@ antlrcpp::Any CypherMainVisitor::visitPrivilege( if (ctx->MATCH()) return AuthQuery::Privilege::MATCH; if (ctx->MERGE()) return AuthQuery::Privilege::MERGE; if (ctx->SET()) return AuthQuery::Privilege::SET; + if (ctx->REMOVE()) return AuthQuery::Privilege::REMOVE; + if (ctx->INDEX()) return AuthQuery::Privilege::INDEX; if (ctx->AUTH()) return AuthQuery::Privilege::AUTH; if (ctx->STREAM()) return AuthQuery::Privilege::STREAM; LOG(FATAL) << "Should not get here - unknown privilege!"; diff --git a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 index 56c533074..9c8f2dc7c 100644 --- a/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 +++ b/src/query/frontend/opencypher/grammar/MemgraphCypher.g4 @@ -88,14 +88,14 @@ grantRole : GRANT ROLE role=userOrRoleName TO user=userOrRoleName ; revokeRole : REVOKE ROLE role=userOrRoleName FROM user=userOrRoleName ; -grantPrivilege : GRANT privilegeList TO userOrRole=userOrRoleName ; +grantPrivilege : GRANT ( ALL PRIVILEGES | privileges=privilegeList ) TO userOrRole=userOrRoleName ; -denyPrivilege : DENY privilegeList TO userOrRole=userOrRoleName ; +denyPrivilege : DENY ( ALL PRIVILEGES | privileges=privilegeList ) TO userOrRole=userOrRoleName ; revokePrivilege : REVOKE ( ALL PRIVILEGES | privileges=privilegeList ) FROM userOrRole=userOrRoleName ; privilege : CREATE | DELETE | MATCH | MERGE | SET - | AUTH | STREAM ; + | REMOVE | INDEX | AUTH | STREAM ; privilegeList : privilege ( ',' privilege )* ; diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index ba26ca500..6f0a00a2d 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -3893,22 +3893,108 @@ std::unique_ptr<Cursor> PullRemoteOrderBy::MakeCursor( AuthHandler::AuthHandler(AuthQuery::Action action, std::string user, std::string role, std::string user_or_role, Expression *password, - std::vector<AuthQuery::Privilege> privileges) + std::vector<AuthQuery::Privilege> privileges, + Symbol user_symbol, Symbol role_symbol, + Symbol grants_symbol) : action_(action), user_(user), role_(role), user_or_role_(user_or_role), password_(password), - privileges_(privileges) {} + privileges_(privileges), + user_symbol_(user_symbol), + role_symbol_(role_symbol), + grants_symbol_(grants_symbol) {} bool AuthHandler::Accept(HierarchicalLogicalOperatorVisitor &visitor) { return visitor.Visit(*this); } +std::vector<Symbol> AuthHandler::OutputSymbols(const SymbolTable &) const { + switch (action_) { + case AuthQuery::Action::SHOW_USERS: + case AuthQuery::Action::SHOW_USERS_FOR_ROLE: + return {user_symbol_}; + + case AuthQuery::Action::SHOW_ROLES: + case AuthQuery::Action::SHOW_ROLE_FOR_USER: + return {role_symbol_}; + + case AuthQuery::Action::SHOW_GRANTS: + return {grants_symbol_}; + + case AuthQuery::Action::CREATE_USER: + case AuthQuery::Action::DROP_USER: + case AuthQuery::Action::SET_PASSWORD: + case AuthQuery::Action::CREATE_ROLE: + case AuthQuery::Action::DROP_ROLE: + case AuthQuery::Action::GRANT_ROLE: + case AuthQuery::Action::REVOKE_ROLE: + case AuthQuery::Action::GRANT_PRIVILEGE: + case AuthQuery::Action::DENY_PRIVILEGE: + case AuthQuery::Action::REVOKE_PRIVILEGE: + return {}; + } +} + class AuthHandlerCursor : public Cursor { public: AuthHandlerCursor(const AuthHandler &self) : self_(self) {} + std::vector<auth::Permission> GetAuthPermissions() { + std::vector<auth::Permission> ret; + for (const auto &privilege : self_.privileges()) { + switch (privilege) { + case AuthQuery::Privilege::MATCH: + ret.push_back(auth::Permission::MATCH); + break; + case AuthQuery::Privilege::CREATE: + ret.push_back(auth::Permission::CREATE); + break; + case AuthQuery::Privilege::MERGE: + ret.push_back(auth::Permission::MERGE); + break; + case AuthQuery::Privilege::DELETE: + ret.push_back(auth::Permission::DELETE); + break; + case AuthQuery::Privilege::SET: + ret.push_back(auth::Permission::SET); + break; + case AuthQuery::Privilege::REMOVE: + ret.push_back(auth::Permission::REMOVE); + break; + case AuthQuery::Privilege::INDEX: + ret.push_back(auth::Permission::INDEX); + break; + case AuthQuery::Privilege::AUTH: + ret.push_back(auth::Permission::AUTH); + break; + case AuthQuery::Privilege::STREAM: + ret.push_back(auth::Permission::STREAM); + break; + } + } + return ret; + } + + std::vector<std::string> GetGrantsFromAuthPermissions( + auth::Permissions &permissions) { + std::vector<std::string> grants, denies, ret; + for (const auto &permission : permissions.GetGrants()) { + grants.push_back(auth::PermissionToString(permission)); + } + for (const auto &permission : permissions.GetDenies()) { + denies.push_back(auth::PermissionToString(permission)); + } + if (grants.size() > 0) { + ret.push_back(fmt::format("GRANT {}", utils::Join(grants, ", "))); + } + if (denies.size() > 0) { + ret.push_back(fmt::format("DENY {}", utils::Join(denies, ", "))); + } + return ret; + } + bool Pull(Frame &frame, Context &ctx) override { if (ctx.in_explicit_transaction_) { throw UserModificationInMulticommandTxException(); @@ -3916,37 +4002,32 @@ class AuthHandlerCursor : public Cursor { ExpressionEvaluator evaluator(frame, &ctx, GraphView::OLD); std::experimental::optional<std::string> password; - /* TODO(mferencevic): handle null passwords properly */ if (self_.password()) { auto password_tv = self_.password()->Accept(evaluator); - if (!password_tv.IsString()) { - throw QueryRuntimeException("Password must be a string, not '{}'!", - password_tv.type()); + if (!password_tv.IsString() && !password_tv.IsNull()) { + throw QueryRuntimeException( + "Password must be a string or null, not '{}'!", password_tv.type()); + } + if (password_tv.IsString()) { + password = password_tv.ValueString(); } - password = password_tv.ValueString(); } auto &auth = *ctx.auth_; - std::lock_guard<std::mutex> lock(auth.WithLock()); switch (self_.action()) { case AuthQuery::Action::CREATE_USER: { - if (!password) { - throw QueryRuntimeException( - "Password must be provided when creating a user!"); - } - auto user = auth.AddUser(self_.user()); + std::lock_guard<std::mutex> lock(auth.WithLock()); + auto user = auth.AddUser(self_.user(), password); if (!user) { - throw QueryRuntimeException("User '{}' already exists!", + throw QueryRuntimeException("User or role '{}' already exists!", self_.user()); } - user->UpdatePassword(*password); - if (!auth.SaveUser(*user)) { - throw QueryRuntimeException("Couldn't save user '{}'!", self_.user()); - } - break; + return false; } + case AuthQuery::Action::DROP_USER: { + std::lock_guard<std::mutex> lock(auth.WithLock()); auto user = auth.GetUser(self_.user()); if (!user) { throw QueryRuntimeException("User '{}' doesn't exist!", self_.user()); @@ -3955,38 +4036,216 @@ class AuthHandlerCursor : public Cursor { throw QueryRuntimeException("Couldn't remove user '{}'!", self_.user()); } - break; + return false; } + case AuthQuery::Action::SET_PASSWORD: { - if (!password) { - throw QueryRuntimeException("Password must be provided!"); - } + std::lock_guard<std::mutex> lock(auth.WithLock()); auto user = auth.GetUser(self_.user()); if (!user) { throw QueryRuntimeException("User '{}' doesn't exist!", self_.user()); } - user->UpdatePassword(*password); - if (!auth.SaveUser(*user)) { - throw QueryRuntimeException("Couldn't set password for user '{}'!", - self_.user()); - } - break; + user->UpdatePassword(password); + auth.SaveUser(*user); + return false; } - case AuthQuery::Action::CREATE_ROLE: - case AuthQuery::Action::DROP_ROLE: - case AuthQuery::Action::SHOW_ROLES: - case AuthQuery::Action::SHOW_USERS: - case AuthQuery::Action::GRANT_ROLE: - case AuthQuery::Action::REVOKE_ROLE: + + case AuthQuery::Action::CREATE_ROLE: { + std::lock_guard<std::mutex> lock(auth.WithLock()); + auto role = auth.AddRole(self_.role()); + if (!role) { + throw QueryRuntimeException("User or role '{}' already exists!", + self_.role()); + } + return false; + } + + case AuthQuery::Action::DROP_ROLE: { + std::lock_guard<std::mutex> lock(auth.WithLock()); + auto role = auth.GetRole(self_.role()); + if (!role) { + throw QueryRuntimeException("Role '{}' doesn't exist!", self_.role()); + } + if (!auth.RemoveRole(self_.role())) { + throw QueryRuntimeException("Couldn't remove role '{}'!", + self_.role()); + } + return false; + } + + case AuthQuery::Action::SHOW_USERS: { + if (!users_) { + std::lock_guard<std::mutex> lock(auth.WithLock()); + users_.emplace(auth.AllUsers()); + users_it_ = users_->begin(); + } + + if (users_it_ == users_->end()) return false; + + frame[self_.user_symbol()] = users_it_->username(); + users_it_++; + + return true; + } + + case AuthQuery::Action::SHOW_ROLES: { + if (!roles_) { + std::lock_guard<std::mutex> lock(auth.WithLock()); + roles_.emplace(auth.AllRoles()); + roles_it_ = roles_->begin(); + } + + if (roles_it_ == roles_->end()) return false; + + frame[self_.role_symbol()] = roles_it_->rolename(); + roles_it_++; + + return true; + } + + case AuthQuery::Action::GRANT_ROLE: { + std::lock_guard<std::mutex> lock(auth.WithLock()); + auto user = auth.GetUser(self_.user()); + if (!user) { + throw QueryRuntimeException("User '{}' doesn't exist!", self_.user()); + } + auto role = auth.GetRole(self_.role()); + if (!role) { + throw QueryRuntimeException("Role '{}' doesn't exist!", self_.role()); + } + if (user->role()) { + throw QueryRuntimeException( + "User '{}' is already a member of role '{}'!", self_.user(), + user->role()->rolename()); + } + user->SetRole(*role); + auth.SaveUser(*user); + return false; + } + + case AuthQuery::Action::REVOKE_ROLE: { + std::lock_guard<std::mutex> lock(auth.WithLock()); + auto user = auth.GetUser(self_.user()); + if (!user) { + throw QueryRuntimeException("User '{}' doesn't exist!", self_.user()); + } + auto role = auth.GetRole(self_.role()); + if (!role) { + throw QueryRuntimeException("Role '{}' doesn't exist!", self_.role()); + } + if (user->role() != role) { + throw QueryRuntimeException("User '{}' isn't a member of role '{}'!", + self_.user(), self_.role()); + } + user->ClearRole(); + auth.SaveUser(*user); + return false; + } + case AuthQuery::Action::GRANT_PRIVILEGE: case AuthQuery::Action::DENY_PRIVILEGE: - case AuthQuery::Action::REVOKE_PRIVILEGE: - case AuthQuery::Action::SHOW_GRANTS: - case AuthQuery::Action::SHOW_ROLE_FOR_USER: - case AuthQuery::Action::SHOW_USERS_FOR_ROLE: - throw utils::NotYetImplemented("user auth"); + case AuthQuery::Action::REVOKE_PRIVILEGE: { + std::lock_guard<std::mutex> lock(auth.WithLock()); + auto user = auth.GetUser(self_.user_or_role()); + auto role = auth.GetRole(self_.user_or_role()); + if (!user && !role) { + throw QueryRuntimeException("User or role '{}' doesn't exist!", + self_.user_or_role()); + } + auto permissions = GetAuthPermissions(); + if (user) { + for (const auto &permission : permissions) { + // TODO (mferencevic): should we first check that the privilege + // is granted/denied/revoked before unconditionally + // granting/denying/revoking it? + if (self_.action() == AuthQuery::Action::GRANT_PRIVILEGE) { + user->permissions().Grant(permission); + } else if (self_.action() == AuthQuery::Action::DENY_PRIVILEGE) { + user->permissions().Deny(permission); + } else { + user->permissions().Revoke(permission); + } + } + auth.SaveUser(*user); + } else { + for (const auto &permission : permissions) { + // TODO (mferencevic): should we first check that the privilege + // is granted/denied/revoked before unconditionally + // granting/denying/revoking it? + if (self_.action() == AuthQuery::Action::GRANT_PRIVILEGE) { + role->permissions().Grant(permission); + } else if (self_.action() == AuthQuery::Action::DENY_PRIVILEGE) { + role->permissions().Deny(permission); + } else { + role->permissions().Revoke(permission); + } + } + auth.SaveRole(*role); + } + return false; + } + + case AuthQuery::Action::SHOW_GRANTS: { + if (!grants_) { + std::lock_guard<std::mutex> lock(auth.WithLock()); + auto user = auth.GetUser(self_.user_or_role()); + auto role = auth.GetRole(self_.user_or_role()); + if (!user && !role) { + throw QueryRuntimeException("User or role '{}' doesn't exist!", + self_.user_or_role()); + } + if (user) { + grants_.emplace(GetGrantsFromAuthPermissions(user->permissions())); + } else { + grants_.emplace(GetGrantsFromAuthPermissions(role->permissions())); + } + grants_it_ = grants_->begin(); + } + + if (grants_it_ == grants_->end()) return false; + + frame[self_.grants_symbol()] = *grants_it_; + grants_it_++; + + return true; + } + + case AuthQuery::Action::SHOW_ROLE_FOR_USER: { + if (returned_role_for_user_) return false; + std::lock_guard<std::mutex> lock(auth.WithLock()); + auto user = auth.GetUser(self_.user()); + if (!user) { + throw QueryRuntimeException("User '{}' doesn't exist!", self_.user()); + } + if (user->role()) { + frame[self_.role_symbol()] = user->role()->rolename(); + } else { + frame[self_.role_symbol()] = TypedValue::Null; + } + returned_role_for_user_ = true; + return true; + } + + case AuthQuery::Action::SHOW_USERS_FOR_ROLE: { + if (!users_) { + std::lock_guard<std::mutex> lock(auth.WithLock()); + auto role = auth.GetRole(self_.role()); + if (!role) { + throw QueryRuntimeException("Role '{}' doesn't exist!", + self_.role()); + } + users_.emplace(auth.AllUsersForRole(self_.role())); + users_it_ = users_->begin(); + } + + if (users_it_ == users_->end()) return false; + + frame[self_.user_symbol()] = users_it_->username(); + users_it_++; + + return true; + } } - return false; } void Reset() override { @@ -3995,6 +4254,13 @@ class AuthHandlerCursor : public Cursor { private: const AuthHandler &self_; + std::experimental::optional<std::vector<auth::User>> users_; + std::vector<auth::User>::iterator users_it_; + std::experimental::optional<std::vector<auth::Role>> roles_; + std::vector<auth::Role>::iterator roles_it_; + std::experimental::optional<std::vector<std::string>> grants_; + std::vector<std::string>::iterator grants_it_; + bool returned_role_for_user_{false}; }; std::unique_ptr<Cursor> AuthHandler::MakeCursor( diff --git a/src/query/plan/operator.lcp b/src/query/plan/operator.lcp index 05e5a9f42..4f3e39cc9 100644 --- a/src/query/plan/operator.lcp +++ b/src/query/plan/operator.lcp @@ -2019,6 +2019,12 @@ and returns true, once.") case AuthQuery::Privilege::SET: ${builder}.set(i, query::capnp::AuthQuery::Privilege::SET); break; + case AuthQuery::Privilege::REMOVE: + ${builder}.set(i, query::capnp::AuthQuery::Privilege::REMOVE); + break; + case AuthQuery::Privilege::INDEX: + ${builder}.set(i, query::capnp::AuthQuery::Privilege::INDEX); + break; case AuthQuery::Privilege::AUTH: ${builder}.set(i, query::capnp::AuthQuery::Privilege::AUTH); break; @@ -2048,6 +2054,12 @@ and returns true, once.") case query::capnp::AuthQuery::Privilege::SET: ${member-name}.push_back(AuthQuery::Privilege::SET); break; + case query::capnp::AuthQuery::Privilege::REMOVE: + ${member-name}.push_back(AuthQuery::Privilege::REMOVE); + break; + case query::capnp::AuthQuery::Privilege::INDEX: + ${member-name}.push_back(AuthQuery::Privilege::INDEX); + break; case query::capnp::AuthQuery::Privilege::AUTH: ${member-name}.push_back(AuthQuery::Privilege::AUTH); break; @@ -2056,16 +2068,21 @@ and returns true, once.") break; } } - cpp<#))) + cpp<#)) + (user-symbol "Symbol" :reader t) + (role-symbol "Symbol" :reader t) + (grants-symbol "Symbol" :reader t)) (:public #>cpp AuthHandler(AuthQuery::Action action, std::string user, std::string role, std::string user_or_role, Expression * password, - std::vector<AuthQuery::Privilege> privileges); + std::vector<AuthQuery::Privilege> privileges, + Symbol user_symbol, Symbol role_symbol, Symbol grants_symbol); bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override; std::unique_ptr<Cursor> MakeCursor(database::GraphDbAccessor & db) const override; + std::vector<Symbol> OutputSymbols(const SymbolTable &) const override; virtual std::vector<Symbol> ModifiedSymbols(const SymbolTable &) const override { return {}; } diff --git a/src/query/plan/rule_based_planner.hpp b/src/query/plan/rule_based_planner.hpp index 50bf99883..821d04f65 100644 --- a/src/query/plan/rule_based_planner.hpp +++ b/src/query/plan/rule_based_planner.hpp @@ -185,10 +185,14 @@ class RuleBasedPlanner { } else if (auto *auth_query = dynamic_cast<query::AuthQuery *>(clause)) { DCHECK(!input_op) << "Unexpected operator before AuthQuery"; + auto &symbol_table = context.symbol_table; input_op = std::make_unique<plan::AuthHandler>( auth_query->action_, auth_query->user_, auth_query->role_, auth_query->user_or_role_, auth_query->password_, - auth_query->privileges_); + auth_query->privileges_, + symbol_table.CreateSymbol("user", false), + symbol_table.CreateSymbol("role", false), + symbol_table.CreateSymbol("grants", false)); } else if (auto *create_stream = dynamic_cast<query::CreateStream *>(clause)) { DCHECK(!input_op) << "Unexpected operator before CreateStream"; diff --git a/src/storage/kvstore.cpp b/src/storage/kvstore.cpp index 1260c08fc..10a2a4246 100644 --- a/src/storage/kvstore.cpp +++ b/src/storage/kvstore.cpp @@ -40,6 +40,15 @@ bool KVStore::Put(const std::string &key, const std::string &value) { return s.ok(); } +bool KVStore::PutMultiple(const std::map<std::string, std::string> &items) { + rocksdb::WriteBatch batch; + for (const auto &item : items) { + batch.Put(item.first, item.second); + } + auto s = pimpl_->db->Write(rocksdb::WriteOptions(), &batch); + return s.ok(); +} + std::experimental::optional<std::string> KVStore::Get( const std::string &key) const noexcept { std::string value; @@ -53,6 +62,15 @@ bool KVStore::Delete(const std::string &key) { return s.ok(); } +bool KVStore::DeleteMultiple(const std::vector<std::string> &keys) { + rocksdb::WriteBatch batch; + for (const auto &key : keys) { + batch.Delete(key); + } + auto s = pimpl_->db->Write(rocksdb::WriteOptions(), &batch); + return s.ok(); +} + bool KVStore::DeletePrefix(const std::string &prefix) { std::unique_ptr<rocksdb::Iterator> iter = std::unique_ptr<rocksdb::Iterator>( pimpl_->db->NewIterator(rocksdb::ReadOptions())); @@ -64,6 +82,20 @@ bool KVStore::DeletePrefix(const std::string &prefix) { return true; } +bool KVStore::PutAndDeleteMultiple( + const std::map<std::string, std::string> &items, + const std::vector<std::string> &keys) { + rocksdb::WriteBatch batch; + for (const auto &item : items) { + batch.Put(item.first, item.second); + } + for (const auto &key : keys) { + batch.Delete(key); + } + auto s = pimpl_->db->Write(rocksdb::WriteOptions(), &batch); + return s.ok(); +} + // iterator struct KVStore::iterator::impl { diff --git a/src/storage/kvstore.hpp b/src/storage/kvstore.hpp index 049a432e2..5be82b72b 100644 --- a/src/storage/kvstore.hpp +++ b/src/storage/kvstore.hpp @@ -2,8 +2,10 @@ #include <experimental/filesystem> #include <experimental/optional> +#include <map> #include <memory> #include <string> +#include <vector> #include "utils/exceptions.hpp" @@ -49,6 +51,16 @@ class KVStore final { */ bool Put(const std::string &key, const std::string &value); + /** + * Store values under the given keys. + * + * @param items + * + * @return true if the items have been successfully stored. + * In case of any error false is going to be returned. + */ + bool PutMultiple(const std::map<std::string, std::string> &items); + /** * Retrieve value for the given key. * @@ -71,6 +83,17 @@ class KVStore final { */ bool Delete(const std::string &key); + /** + * Deletes the keys and corresponding values from storage. + * + * @param keys + * + * @return True on success, false on error. The return value is + * true if the keys don't exist and underlying storage + * didn't encounter any error. + */ + bool DeleteMultiple(const std::vector<std::string> &keys); + /** * Delete all (key, value) pairs where key begins with a given prefix. * @@ -83,6 +106,18 @@ class KVStore final { */ bool DeletePrefix(const std::string &prefix = ""); + /** + * Store values under the given keys and delete the keys. + * + * @param items + * @param keys + * + * @return true if the items have been successfully stored and deleted. + * In case of any error false is going to be returned. + */ + bool PutAndDeleteMultiple(const std::map<std::string, std::string> &items, + const std::vector<std::string> &keys); + /** * Returns total number of stored (key, value) pairs. The function takes an * optional prefix parameter used for filtering keys that start with that diff --git a/src/storage/kvstore_dummy.cpp b/src/storage/kvstore_dummy.cpp index e3fd5d6fa..c78c75582 100644 --- a/src/storage/kvstore_dummy.cpp +++ b/src/storage/kvstore_dummy.cpp @@ -16,6 +16,11 @@ bool KVStore::Put(const std::string &key, const std::string &value) { << "Unsupported operation (KVStore::Put) -- this is a dummy kvstore"; } +bool KVStore::PutMultiple(const std::map<std::string, std::string> &items) { + CHECK(false) << "Unsupported operation (KVStore::PutMultiple) -- this is a " + "dummy kvstore"; +} + std::experimental::optional<std::string> KVStore::Get( const std::string &key) const noexcept { CHECK(false) @@ -27,11 +32,24 @@ bool KVStore::Delete(const std::string &key) { << "Unsupported operation (KVStore::Delete) -- this is a dummy kvstore"; } +bool KVStore::DeleteMultiple(const std::vector<std::string> &keys) { + CHECK(false) << "Unsupported operation (KVStore::DeleteMultiple) -- this is " + "a dummy kvstore"; +} + bool KVStore::DeletePrefix(const std::string &prefix) { CHECK(false) << "Unsupported operation (KVStore::DeletePrefix) -- this is a " "dummy kvstore"; } +bool KVStore::PutAndDeleteMultiple( + const std::map<std::string, std::string> &items, + const std::vector<std::string> &keys) { + CHECK(false) + << "Unsupported operation (KVStore::PutAndDeleteMultiple) -- this is a " + "dummy kvstore"; +} + // iterator struct KVStore::iterator::impl {}; diff --git a/tests/unit/auth.cpp b/tests/unit/auth.cpp index 542c7512b..dd06c6363 100644 --- a/tests/unit/auth.cpp +++ b/tests/unit/auth.cpp @@ -12,9 +12,16 @@ using namespace auth; namespace fs = std::experimental::filesystem; +DECLARE_bool(auth_password_permit_null); +DECLARE_string(auth_password_strength_regex); + class AuthWithStorage : public ::testing::Test { protected: - virtual void SetUp() { utils::EnsureDir(test_folder_); } + virtual void SetUp() { + utils::EnsureDir(test_folder_); + FLAGS_auth_password_permit_null = true; + FLAGS_auth_password_strength_regex = ".+"; + } virtual void TearDown() { fs::remove_all(test_folder_); } @@ -64,15 +71,24 @@ TEST_F(AuthWithStorage, Authenticate) { ASSERT_NE(user, std::experimental::nullopt); ASSERT_TRUE(auth.HasUsers()); - ASSERT_THROW(auth.Authenticate("test", "123"), utils::BasicException); + ASSERT_TRUE(auth.Authenticate("test", "123")); user->UpdatePassword("123"); - ASSERT_TRUE(auth.SaveUser(*user)); + 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); + + user->UpdatePassword(); + auth.SaveUser(*user); + + ASSERT_NE(auth.Authenticate("test", "123"), std::experimental::nullopt); + ASSERT_NE(auth.Authenticate("test", "456"), std::experimental::nullopt); + + ASSERT_EQ(auth.Authenticate("nonexistant", "123"), + std::experimental::nullopt); } TEST_F(AuthWithStorage, UserRolePermissions) { @@ -84,27 +100,27 @@ TEST_F(AuthWithStorage, UserRolePermissions) { 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().Has(Permission::MATCH), + PermissionLevel::NEUTRAL); + ASSERT_EQ(user->permissions().Has(Permission::CREATE), + PermissionLevel::NEUTRAL); + ASSERT_EQ(user->permissions().Has(Permission::MERGE), + 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); + user->permissions().Grant(Permission::MATCH); // 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().Has(Permission::MATCH), PermissionLevel::GRANT); + ASSERT_EQ(user->permissions().Has(Permission::CREATE), + PermissionLevel::NEUTRAL); + ASSERT_EQ(user->permissions().Has(Permission::MERGE), + PermissionLevel::NEUTRAL); + ASSERT_EQ(user->permissions().Has(Permission::DELETE), + PermissionLevel::NEUTRAL); ASSERT_EQ(user->permissions(), user->GetPermissions()); // Create role. @@ -113,29 +129,29 @@ TEST_F(AuthWithStorage, UserRolePermissions) { ASSERT_NE(role, std::experimental::nullopt); // Assign permissions to role and role to user. - role->permissions().Grant(Permission::Delete); + 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); + ASSERT_EQ(permissions.Has(Permission::MATCH), PermissionLevel::GRANT); + ASSERT_EQ(permissions.Has(Permission::DELETE), PermissionLevel::GRANT); + ASSERT_EQ(permissions.Has(Permission::CREATE), PermissionLevel::NEUTRAL); + ASSERT_EQ(permissions.Has(Permission::MERGE), PermissionLevel::NEUTRAL); } // Add explicit deny to role. - role->permissions().Deny(Permission::Read); + role->permissions().Deny(Permission::MATCH); 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); + ASSERT_EQ(permissions.Has(Permission::MATCH), PermissionLevel::DENY); + ASSERT_EQ(permissions.Has(Permission::DELETE), PermissionLevel::GRANT); + ASSERT_EQ(permissions.Has(Permission::CREATE), PermissionLevel::NEUTRAL); + ASSERT_EQ(permissions.Has(Permission::MERGE), PermissionLevel::NEUTRAL); } } @@ -202,6 +218,126 @@ TEST_F(AuthWithStorage, RoleManipulations) { ASSERT_TRUE(role2); ASSERT_EQ(role2->rolename(), "role2"); } + + { + auto users = auth.AllUsers(); + std::sort(users.begin(), users.end(), [](const User &a, const User &b) { + return a.username() < b.username(); + }); + ASSERT_EQ(users.size(), 2); + ASSERT_EQ(users[0].username(), "user1"); + ASSERT_EQ(users[1].username(), "user2"); + } + + { + auto roles = auth.AllRoles(); + std::sort(roles.begin(), roles.end(), [](const Role &a, const Role &b) { + return a.rolename() < b.rolename(); + }); + ASSERT_EQ(roles.size(), 2); + ASSERT_EQ(roles[0].rolename(), "role1"); + ASSERT_EQ(roles[1].rolename(), "role2"); + } + + { + auto users = auth.AllUsersForRole("role2"); + ASSERT_EQ(users.size(), 1); + ASSERT_EQ(users[0].username(), "user2"); + } +} + +TEST_F(AuthWithStorage, UserRoleLinkUnlink) { + { + auto user = auth.AddUser("user"); + ASSERT_TRUE(user); + auto role = auth.AddRole("role"); + ASSERT_TRUE(role); + user->SetRole(*role); + auth.SaveUser(*user); + } + + { + auto user = auth.GetUser("user"); + ASSERT_TRUE(user); + auto role = user->role(); + ASSERT_TRUE(role); + ASSERT_EQ(role->rolename(), "role"); + } + + { + auto user = auth.GetUser("user"); + ASSERT_TRUE(user); + user->ClearRole(); + auth.SaveUser(*user); + } + + { + auto user = auth.GetUser("user"); + ASSERT_TRUE(user); + ASSERT_FALSE(user->role()); + } +} + +TEST_F(AuthWithStorage, UserPasswordCreation) { + { + auto user = auth.AddUser("test"); + ASSERT_TRUE(user); + ASSERT_TRUE(auth.Authenticate("test", "123")); + ASSERT_TRUE(auth.Authenticate("test", "456")); + ASSERT_TRUE(auth.RemoveUser(user->username())); + } + + { + auto user = auth.AddUser("test", "123"); + ASSERT_TRUE(user); + ASSERT_TRUE(auth.Authenticate("test", "123")); + ASSERT_FALSE(auth.Authenticate("test", "456")); + ASSERT_TRUE(auth.RemoveUser(user->username())); + } +} + +TEST_F(AuthWithStorage, PasswordStrength) { + const std::string kWeakRegex = ".+"; + // https://stackoverflow.com/questions/5142103/regex-to-validate-password-strength + const std::string kStrongRegex = + "^(?=.*[A-Z].*[A-Z])(?=.*[!@#$&*])(?=.*[0-9].*[0-9])(?=.*[a-z].*[a-z].*[" + "a-z]).{8,}$"; + + const std::string kWeakPassword = "weak"; + const std::string kAlmostStrongPassword = + "ThisPasswordMeetsAllButOneCriterion1234"; + const std::string kStrongPassword = "ThisIsAVeryStrongPassword123$"; + + auto user = auth.AddUser("user"); + ASSERT_TRUE(user); + + FLAGS_auth_password_permit_null = true; + FLAGS_auth_password_strength_regex = kWeakRegex; + ASSERT_NO_THROW(user->UpdatePassword()); + ASSERT_NO_THROW(user->UpdatePassword(kWeakPassword)); + ASSERT_NO_THROW(user->UpdatePassword(kAlmostStrongPassword)); + ASSERT_NO_THROW(user->UpdatePassword(kStrongPassword)); + + FLAGS_auth_password_permit_null = false; + FLAGS_auth_password_strength_regex = kWeakRegex; + ASSERT_THROW(user->UpdatePassword(), AuthException); + ASSERT_NO_THROW(user->UpdatePassword(kWeakPassword)); + ASSERT_NO_THROW(user->UpdatePassword(kAlmostStrongPassword)); + ASSERT_NO_THROW(user->UpdatePassword(kStrongPassword)); + + FLAGS_auth_password_permit_null = true; + FLAGS_auth_password_strength_regex = kStrongRegex; + ASSERT_NO_THROW(user->UpdatePassword()); + ASSERT_THROW(user->UpdatePassword(kWeakPassword), AuthException); + ASSERT_THROW(user->UpdatePassword(kAlmostStrongPassword), AuthException); + ASSERT_NO_THROW(user->UpdatePassword(kStrongPassword)); + + FLAGS_auth_password_permit_null = false; + FLAGS_auth_password_strength_regex = kStrongRegex; + ASSERT_THROW(user->UpdatePassword(), AuthException); + ASSERT_THROW(user->UpdatePassword(kWeakPassword), AuthException); + ASSERT_THROW(user->UpdatePassword(kAlmostStrongPassword), AuthException); + ASSERT_NO_THROW(user->UpdatePassword(kStrongPassword)); } TEST(AuthWithoutStorage, Permissions) { @@ -209,50 +345,50 @@ TEST(AuthWithoutStorage, 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)); + permissions.Grant(Permission::MATCH); + ASSERT_EQ(permissions.Has(Permission::MATCH), PermissionLevel::GRANT); + ASSERT_EQ(permissions.grants(), utils::UnderlyingCast(Permission::MATCH)); ASSERT_EQ(permissions.denies(), 0); - permissions.Revoke(Permission::Read); - ASSERT_EQ(permissions.Has(Permission::Read), PermissionLevel::Neutral); + permissions.Revoke(Permission::MATCH); + ASSERT_EQ(permissions.Has(Permission::MATCH), 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)); + permissions.Deny(Permission::MATCH); + ASSERT_EQ(permissions.Has(Permission::MATCH), PermissionLevel::DENY); + ASSERT_EQ(permissions.denies(), utils::UnderlyingCast(Permission::MATCH)); 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)); + permissions.Grant(Permission::MATCH); + ASSERT_EQ(permissions.Has(Permission::MATCH), PermissionLevel::GRANT); + ASSERT_EQ(permissions.grants(), utils::UnderlyingCast(Permission::MATCH)); 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.Deny(Permission::CREATE); + ASSERT_EQ(permissions.Has(Permission::MATCH), PermissionLevel::GRANT); + ASSERT_EQ(permissions.Has(Permission::CREATE), PermissionLevel::DENY); + ASSERT_EQ(permissions.Has(Permission::MERGE), PermissionLevel::NEUTRAL); + ASSERT_EQ(permissions.grants(), utils::UnderlyingCast(Permission::MATCH)); + 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); + permissions.Grant(Permission::DELETE); + ASSERT_EQ(permissions.Has(Permission::MATCH), PermissionLevel::GRANT); + ASSERT_EQ(permissions.Has(Permission::CREATE), PermissionLevel::DENY); + ASSERT_EQ(permissions.Has(Permission::MERGE), 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)); + utils::UnderlyingCast(Permission::MATCH) | + 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)); + permissions.Revoke(Permission::DELETE); + ASSERT_EQ(permissions.Has(Permission::MATCH), PermissionLevel::GRANT); + ASSERT_EQ(permissions.Has(Permission::CREATE), PermissionLevel::DENY); + ASSERT_EQ(permissions.Has(Permission::MERGE), PermissionLevel::NEUTRAL); + ASSERT_EQ(permissions.Has(Permission::DELETE), PermissionLevel::NEUTRAL); + ASSERT_EQ(permissions.grants(), utils::UnderlyingCast(Permission::MATCH)); + ASSERT_EQ(permissions.denies(), utils::UnderlyingCast(Permission::CREATE)); } TEST(AuthWithoutStorage, PermissionsMaskTest) { @@ -275,8 +411,8 @@ TEST(AuthWithoutStorage, PermissionsMaskTest) { TEST(AuthWithoutStorage, UserSerializeDeserialize) { auto user = User("test"); - user.permissions().Grant(Permission::Read); - user.permissions().Deny(Permission::Update); + user.permissions().Grant(Permission::MATCH); + user.permissions().Deny(Permission::MERGE); user.UpdatePassword("world"); auto data = user.Serialize(); @@ -287,8 +423,8 @@ TEST(AuthWithoutStorage, UserSerializeDeserialize) { TEST(AuthWithoutStorage, RoleSerializeDeserialize) { auto role = Role("test"); - role.permissions().Grant(Permission::Read); - role.permissions().Deny(Permission::Update); + role.permissions().Grant(Permission::MATCH); + role.permissions().Deny(Permission::MERGE); auto data = role.Serialize(); @@ -297,25 +433,32 @@ TEST(AuthWithoutStorage, RoleSerializeDeserialize) { } TEST_F(AuthWithStorage, UserWithRoleSerializeDeserialize) { - auto role = auth.AddRole("test"); + auto role = auth.AddRole("role"); ASSERT_TRUE(role); - role->permissions().Grant(Permission::Read); - role->permissions().Deny(Permission::Update); + role->permissions().Grant(Permission::MATCH); + role->permissions().Deny(Permission::MERGE); auth.SaveRole(*role); - auto user = auth.AddUser("test"); + auto user = auth.AddUser("user"); ASSERT_TRUE(user); - user->permissions().Grant(Permission::Read); - user->permissions().Deny(Permission::Update); + user->permissions().Grant(Permission::MATCH); + user->permissions().Deny(Permission::MERGE); user->UpdatePassword("world"); user->SetRole(*role); auth.SaveUser(*user); - auto new_user = auth.GetUser("test"); + auto new_user = auth.GetUser("user"); ASSERT_TRUE(new_user); ASSERT_EQ(*user, *new_user); } +TEST_F(AuthWithStorage, UserRoleUniqueName) { + ASSERT_TRUE(auth.AddUser("user")); + ASSERT_TRUE(auth.AddRole("role")); + ASSERT_FALSE(auth.AddRole("user")); + ASSERT_FALSE(auth.AddUser("role")); +} + TEST(AuthWithoutStorage, Crypto) { auto hash = EncryptPassword("hello"); ASSERT_TRUE(VerifyPassword("hello", hash)); diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index 01061d084..e3ca7d99e 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -2037,7 +2037,8 @@ TYPED_TEST(CypherMainVisitorTest, RevokePrivilege) { "", "", "user", {}, {AuthQuery::Privilege::CREATE, AuthQuery::Privilege::DELETE, AuthQuery::Privilege::MATCH, AuthQuery::Privilege::MERGE, - AuthQuery::Privilege::SET, AuthQuery::Privilege::AUTH, + AuthQuery::Privilege::SET, AuthQuery::Privilege::REMOVE, + AuthQuery::Privilege::INDEX, AuthQuery::Privilege::AUTH, AuthQuery::Privilege::STREAM}); } diff --git a/tests/unit/kvstore.cpp b/tests/unit/kvstore.cpp index e8afef4bd..4b232187f 100644 --- a/tests/unit/kvstore.cpp +++ b/tests/unit/kvstore.cpp @@ -26,6 +26,13 @@ TEST_F(KVStore, PutGet) { ASSERT_EQ(kvstore.Get("key").value(), "value"); } +TEST_F(KVStore, PutMultipleGet) { + storage::KVStore kvstore(test_folder_ / "PutMultipleGet"); + ASSERT_TRUE(kvstore.PutMultiple({{"key1", "value1"}, {"key2", "value2"}})); + ASSERT_EQ(kvstore.Get("key1").value(), "value1"); + ASSERT_EQ(kvstore.Get("key2").value(), "value2"); +} + TEST_F(KVStore, PutGetDeleteGet) { storage::KVStore kvstore(test_folder_ / "PutGetDeleteGet"); ASSERT_TRUE(kvstore.Put("key", "value")); @@ -34,6 +41,28 @@ TEST_F(KVStore, PutGetDeleteGet) { ASSERT_FALSE(static_cast<bool>(kvstore.Get("key"))); } +TEST_F(KVStore, PutMultipleGetDeleteMultipleGet) { + storage::KVStore kvstore(test_folder_ / "PutMultipleGetDeleteMultipleGet"); + ASSERT_TRUE(kvstore.PutMultiple({{"key1", "value1"}, {"key2", "value2"}})); + ASSERT_EQ(kvstore.Get("key1").value(), "value1"); + ASSERT_EQ(kvstore.Get("key2").value(), "value2"); + ASSERT_TRUE(kvstore.DeleteMultiple({"key1", "key2", "key3"})); + ASSERT_FALSE(static_cast<bool>(kvstore.Get("key1"))); + ASSERT_FALSE(static_cast<bool>(kvstore.Get("key2"))); + ASSERT_FALSE(static_cast<bool>(kvstore.Get("key3"))); +} + +TEST_F(KVStore, PutMultipleGetPutAndDeleteMultipleGet) { + storage::KVStore kvstore(test_folder_ / "PutMultipleGetPutAndDeleteMultipleGet"); + ASSERT_TRUE(kvstore.PutMultiple({{"key1", "value1"}, {"key2", "value2"}})); + ASSERT_EQ(kvstore.Get("key1").value(), "value1"); + ASSERT_EQ(kvstore.Get("key2").value(), "value2"); + ASSERT_TRUE(kvstore.PutAndDeleteMultiple({{"key3", "value3"}}, {"key1", "key2"})); + ASSERT_FALSE(static_cast<bool>(kvstore.Get("key1"))); + ASSERT_FALSE(static_cast<bool>(kvstore.Get("key2"))); + ASSERT_EQ(kvstore.Get("key3").value(), "value3"); +} + TEST_F(KVStore, Durability) { { storage::KVStore kvstore(test_folder_ / "Durability");