// Copyright 2024 Memgraph Ltd. // // Licensed as a Memgraph Enterprise file under the Memgraph Enterprise // License (the "License"); by using this file, you agree to be bound by the terms of the License, and you may not use // this file except in compliance with the License. You may obtain a copy of the License at https://memgraph.com/legal. // // #include "auth/auth.hpp" #include #include #include #include #include "auth/crypto.hpp" #include "auth/exceptions.hpp" #include "auth/rpc.hpp" #include "license/license.hpp" #include "system/transaction.hpp" #include "utils/flag_validation.hpp" #include "utils/message.hpp" #include "utils/settings.hpp" #include "utils/string.hpp" DEFINE_VALIDATED_string(auth_module_executable, "", "Absolute path to the auth module executable that should be used.", { if (value.empty()) return true; // Check the file status, following symlinks. auto status = std::filesystem::status(value); if (!std::filesystem::is_regular_file(status)) { std::cerr << "The auth module path doesn't exist or isn't a file!" << std::endl; return false; } return true; }); DEFINE_VALIDATED_int32(auth_module_timeout_ms, 10000, "Timeout (in milliseconds) used when waiting for a " "response from the auth module.", FLAG_IN_RANGE(100, 1800000)); // DEPRECATED FLAGS // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables, misc-unused-parameters) DEFINE_VALIDATED_HIDDEN_bool(auth_module_create_missing_user, true, "Set to false to disable creation of missing users.", { spdlog::warn( "auth_module_create_missing_user flag is deprecated. It not possible to create " "users through the module anymore."); return true; }); // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables, misc-unused-parameters) DEFINE_VALIDATED_HIDDEN_bool(auth_module_create_missing_role, true, "Set to false to disable creation of missing roles.", { spdlog::warn( "auth_module_create_missing_role flag is deprecated. It not possible to create " "roles through the module anymore."); return true; }); // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables, misc-unused-parameters) DEFINE_VALIDATED_HIDDEN_bool( auth_module_manage_roles, true, "Set to false to disable management of roles through the auth module.", { spdlog::warn( "auth_module_manage_roles flag is deprecated. It not possible to create roles through the module anymore."); return true; }); namespace memgraph::auth { const Auth::Epoch Auth::kStartEpoch = 1; namespace { #ifdef MG_ENTERPRISE /** * REPLICATION SYSTEM ACTION IMPLEMENTATIONS */ struct UpdateAuthData : memgraph::system::ISystemAction { explicit UpdateAuthData(User user) : user_{std::move(user)}, role_{std::nullopt} {} explicit UpdateAuthData(Role role) : user_{std::nullopt}, role_{std::move(role)} {} void DoDurability() override { /* Done during Auth execution */ } bool DoReplication(replication::ReplicationClient &client, const utils::UUID &main_uuid, replication::ReplicationEpoch const &epoch, memgraph::system::Transaction const &txn) const override { auto check_response = [](const replication::UpdateAuthDataRes &response) { return response.success; }; if (user_) { return client.SteamAndFinalizeDelta( check_response, main_uuid, std::string{epoch.id()}, txn.last_committed_system_timestamp(), txn.timestamp(), *user_); } if (role_) { return client.SteamAndFinalizeDelta( check_response, main_uuid, std::string{epoch.id()}, txn.last_committed_system_timestamp(), txn.timestamp(), *role_); } // Should never get here MG_ASSERT(false, "Trying to update auth data that is not a user nor a role"); return {}; } void PostReplication(replication::RoleMainData &mainData) const override {} private: std::optional user_; std::optional role_; }; struct DropAuthData : memgraph::system::ISystemAction { enum class AuthDataType { USER, ROLE }; explicit DropAuthData(AuthDataType type, std::string_view name) : type_{type}, name_{name} {} void DoDurability() override { /* Done during Auth execution */ } bool DoReplication(replication::ReplicationClient &client, const utils::UUID &main_uuid, replication::ReplicationEpoch const &epoch, memgraph::system::Transaction const &txn) const override { auto check_response = [](const replication::DropAuthDataRes &response) { return response.success; }; memgraph::replication::DropAuthDataReq::DataType type{}; switch (type_) { case AuthDataType::USER: type = memgraph::replication::DropAuthDataReq::DataType::USER; break; case AuthDataType::ROLE: type = memgraph::replication::DropAuthDataReq::DataType::ROLE; break; } return client.SteamAndFinalizeDelta( check_response, main_uuid, std::string{epoch.id()}, txn.last_committed_system_timestamp(), txn.timestamp(), type, name_); } void PostReplication(replication::RoleMainData &mainData) const override {} private: AuthDataType type_; std::string name_; }; #endif /** * CONSTANTS */ const std::string kUserPrefix = "user:"; const std::string kRolePrefix = "role:"; const std::string kLinkPrefix = "link:"; const std::string kVersion = "version"; static constexpr auto kVersionV1 = "V1"; } // namespace /** * All data stored in the `Auth` storage is stored in an underlying * `kvstore::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:", value="" * Role: key="role:", value="" * * 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:", value="" */ namespace { void MigrateVersions(kvstore::KVStore &store) { static constexpr auto kPasswordHashV0V1 = "password_hash"; auto version_str = store.Get(kVersion); if (!version_str) { using namespace std::string_literals; // pre versioning, add version to the store auto puts = std::map{{kVersion, kVersionV1}}; // also add hash kind into durability auto it = store.begin(kUserPrefix); auto const e = store.end(kUserPrefix); if (it != e) { const auto hash_algo = CurrentHashAlgorithm(); spdlog::info("Updating auth durability, assuming previously stored as {}", AsString(hash_algo)); for (; it != e; ++it) { auto const &[key, value] = *it; try { auto user_data = nlohmann::json::parse(value); auto password_hash = user_data[kPasswordHashV0V1]; if (!password_hash.is_string()) { throw AuthException("Couldn't load user data!"); } // upgrade the password_hash to include the hash algortihm if (password_hash.empty()) { user_data[kPasswordHashV0V1] = nullptr; } else { user_data[kPasswordHashV0V1] = HashedPassword{hash_algo, password_hash}; } puts.emplace(key, user_data.dump()); } catch (const nlohmann::json::parse_error &e) { throw AuthException("Couldn't load user data!"); } } } // Perform migration to V1 store.PutMultiple(puts); version_str = kVersionV1; } } auto ParseJson(std::string_view str) { nlohmann::json data; try { data = nlohmann::json::parse(str); } catch (const nlohmann::json::parse_error &e) { throw AuthException("Couldn't load auth data!"); } return data; } }; // namespace Auth::Auth(std::string storage_directory, Config config) : storage_(std::move(storage_directory)), module_(FLAGS_auth_module_executable), config_{std::move(config)} { MigrateVersions(storage_); } std::optional Auth::Authenticate(const std::string &username, const std::string &password) { if (module_.IsUsed()) { /* * MODULE AUTH STORAGE */ const auto license_check_result = license::global_license_checker.IsEnterpriseValid(utils::global_settings); if (license_check_result.HasError()) { spdlog::warn(license::LicenseCheckErrorToString(license_check_result.GetError(), "authentication modules")); return std::nullopt; } nlohmann::json params = nlohmann::json::object(); params["username"] = username; params["password"] = password; auto ret = module_.Call(params, FLAGS_auth_module_timeout_ms); // Verify response integrity. if (!ret.is_object() || ret.find("authenticated") == ret.end() || ret.find("role") == ret.end()) { return std::nullopt; } const auto &ret_authenticated = ret.at("authenticated"); const auto &ret_role = ret.at("role"); if (!ret_authenticated.is_boolean() || !ret_role.is_string()) { return std::nullopt; } auto is_authenticated = ret_authenticated.get(); const auto &rolename = ret_role.get(); // Check if role is present auto role = GetRole(rolename); if (!role) { spdlog::warn(utils::MessageWithLink("Couldn't authenticate user '{}' because the role '{}' doesn't exist.", username, rolename, "https://memgr.ph/auth")); return std::nullopt; } // Authenticate the user. if (!is_authenticated) return std::nullopt; return RoleWUsername{username, std::move(*role)}; } /* * LOCAL AUTH STORAGE */ auto user = GetUser(username); if (!user) { spdlog::warn(utils::MessageWithLink("Couldn't authenticate user '{}' because the user doesn't exist.", username, "https://memgr.ph/auth")); return std::nullopt; } if (!user->CheckPassword(password)) { spdlog::warn(utils::MessageWithLink("Couldn't authenticate user '{}' because the password is not correct.", username, "https://memgr.ph/auth")); return std::nullopt; } if (user->UpgradeHash(password)) { SaveUser(*user); } return user; } void Auth::LinkUser(User &user) const { auto link = storage_.Get(kLinkPrefix + user.username()); if (link) { auto role = GetRole(*link); if (role) { user.SetRole(*role); } } } std::optional Auth::GetUser(const std::string &username_orig) const { if (module_.IsUsed()) return std::nullopt; // User's are not supported when using module auto username = utils::ToLowerCase(username_orig); auto existing_user = storage_.Get(kUserPrefix + username); if (!existing_user) return std::nullopt; auto user = User::Deserialize(ParseJson(*existing_user)); LinkUser(user); return user; } void Auth::SaveUser(const User &user, system::Transaction *system_tx) { DisableIfModuleUsed(); bool success = false; if (const auto *role = user.role(); role != nullptr) { success = storage_.PutMultiple( {{kUserPrefix + user.username(), user.Serialize().dump()}, {kLinkPrefix + user.username(), role->rolename()}}); } else { success = storage_.PutAndDeleteMultiple({{kUserPrefix + user.username(), user.Serialize().dump()}}, {kLinkPrefix + user.username()}); } if (!success) { throw AuthException("Couldn't save user '{}'!", user.username()); } // Durability updated -> new epoch UpdateEpoch(); // All changes to the user end up calling this function, so no need to add a delta anywhere else if (system_tx) { #ifdef MG_ENTERPRISE system_tx->AddAction(user); #endif } } void Auth::UpdatePassword(auth::User &user, const std::optional &password) { DisableIfModuleUsed(); // Check if null if (!password) { if (!config_.password_permit_null) { throw AuthException("Null passwords aren't permitted!"); } } else { // Check if compliant with our filter if (config_.custom_password_regex) { if (const auto license_check_result = license::global_license_checker.IsEnterpriseValid(utils::global_settings); license_check_result.HasError()) { throw AuthException( "Custom password regex is a Memgraph Enterprise feature. Please set the config " "(\"--auth-password-strength-regex\") to its default value (\"{}\") or remove the flag.\n{}", glue::kDefaultPasswordRegex, license::LicenseCheckErrorToString(license_check_result.GetError(), "password regex")); } } if (!std::regex_match(*password, config_.password_regex)) { throw AuthException( "The user password doesn't conform to the required strength! Regex: " "\"{}\"", config_.password_regex_str); } } // All checks passed; update user.UpdatePassword(password); } std::optional Auth::AddUser(const std::string &username, const std::optional &password, system::Transaction *system_tx) { DisableIfModuleUsed(); if (!NameRegexMatch(username)) { throw AuthException("Invalid user name."); } auto existing_user = GetUser(username); if (existing_user) return std::nullopt; auto existing_role = GetRole(username); if (existing_role) return std::nullopt; auto new_user = User(username); UpdatePassword(new_user, password); SaveUser(new_user, system_tx); return new_user; } bool Auth::RemoveUser(const std::string &username_orig, system::Transaction *system_tx) { DisableIfModuleUsed(); auto username = utils::ToLowerCase(username_orig); if (!storage_.Get(kUserPrefix + username)) return false; std::vector keys({kLinkPrefix + username, kUserPrefix + username}); if (!storage_.DeleteMultiple(keys)) { throw AuthException("Couldn't remove user '{}'!", username); } // Durability updated -> new epoch UpdateEpoch(); // Handling drop user delta if (system_tx) { #ifdef MG_ENTERPRISE system_tx->AddAction(DropAuthData::AuthDataType::USER, username); #endif } return true; } std::vector Auth::AllUsers() const { std::vector ret; for (auto it = storage_.begin(kUserPrefix); it != storage_.end(kUserPrefix); ++it) { auto username = it->first.substr(kUserPrefix.size()); if (username != utils::ToLowerCase(username)) continue; try { User user = auth::User::Deserialize(ParseJson(it->second)); // Will throw on failure LinkUser(user); ret.emplace_back(std::move(user)); } catch (AuthException &) { continue; } } return ret; } std::vector Auth::AllUsernames() const { std::vector ret; for (auto it = storage_.begin(kUserPrefix); it != storage_.end(kUserPrefix); ++it) { auto username = it->first.substr(kUserPrefix.size()); if (username != utils::ToLowerCase(username)) continue; try { // Check if serialized correctly memgraph::auth::User::Deserialize(ParseJson(it->second)); // Will throw on failure ret.emplace_back(std::move(username)); } catch (AuthException &) { continue; } } return ret; } bool Auth::HasUsers() const { return storage_.begin(kUserPrefix) != storage_.end(kUserPrefix); } bool Auth::AccessControlled() const { return HasUsers() || module_.IsUsed(); } std::optional Auth::GetRole(const std::string &rolename_orig) const { auto rolename = utils::ToLowerCase(rolename_orig); auto existing_role = storage_.Get(kRolePrefix + rolename); if (!existing_role) return std::nullopt; return Role::Deserialize(ParseJson(*existing_role)); } void Auth::SaveRole(const Role &role, system::Transaction *system_tx) { if (!storage_.Put(kRolePrefix + role.rolename(), role.Serialize().dump())) { throw AuthException("Couldn't save role '{}'!", role.rolename()); } // Durability updated -> new epoch UpdateEpoch(); // All changes to the role end up calling this function, so no need to add a delta anywhere else if (system_tx) { #ifdef MG_ENTERPRISE system_tx->AddAction(role); #endif } } std::optional Auth::AddRole(const std::string &rolename, system::Transaction *system_tx) { if (!NameRegexMatch(rolename)) { throw AuthException("Invalid role name."); } if (auto existing_role = GetRole(rolename)) return std::nullopt; if (auto existing_user = GetUser(rolename)) return std::nullopt; auto new_role = Role(rolename); SaveRole(new_role, system_tx); return new_role; } bool Auth::RemoveRole(const std::string &rolename_orig, system::Transaction *system_tx) { auto rolename = utils::ToLowerCase(rolename_orig); if (!storage_.Get(kRolePrefix + rolename)) return false; std::vector keys; for (auto it = storage_.begin(kLinkPrefix); it != storage_.end(kLinkPrefix); ++it) { if (utils::ToLowerCase(it->second) == rolename) { keys.push_back(it->first); } } keys.push_back(kRolePrefix + rolename); if (!storage_.DeleteMultiple(keys)) { throw AuthException("Couldn't remove role '{}'!", rolename); } // Durability updated -> new epoch UpdateEpoch(); // Handling drop role delta if (system_tx) { #ifdef MG_ENTERPRISE system_tx->AddAction(DropAuthData::AuthDataType::ROLE, rolename); #endif } return true; } std::vector Auth::AllRoles() const { std::vector ret; for (auto it = storage_.begin(kRolePrefix); it != storage_.end(kRolePrefix); ++it) { auto rolename = it->first.substr(kRolePrefix.size()); if (rolename != utils::ToLowerCase(rolename)) continue; Role role = memgraph::auth::Role::Deserialize(ParseJson(it->second)); // Will throw on failure ret.emplace_back(std::move(role)); } return ret; } std::vector Auth::AllRolenames() const { std::vector ret; for (auto it = storage_.begin(kRolePrefix); it != storage_.end(kRolePrefix); ++it) { auto rolename = it->first.substr(kRolePrefix.size()); if (rolename != utils::ToLowerCase(rolename)) continue; try { // Check that the data is serialized correctly memgraph::auth::Role::Deserialize(ParseJson(it->second)); ret.emplace_back(std::move(rolename)); } catch (AuthException &) { continue; } } return ret; } std::vector Auth::AllUsersForRole(const std::string &rolename_orig) const { DisableIfModuleUsed(); const auto rolename = utils::ToLowerCase(rolename_orig); std::vector ret; for (auto it = storage_.begin(kLinkPrefix); it != storage_.end(kLinkPrefix); ++it) { auto username = it->first.substr(kLinkPrefix.size()); if (username != utils::ToLowerCase(username)) continue; if (it->second != utils::ToLowerCase(it->second)) continue; if (it->second == rolename) { if (auto user = GetUser(username)) { ret.push_back(std::move(*user)); } else { throw AuthException("Couldn't load user '{}'!", username); } } } return ret; } #ifdef MG_ENTERPRISE Auth::Result Auth::GrantDatabase(const std::string &db, const std::string &name, system::Transaction *system_tx) { using enum Auth::Result; if (module_.IsUsed()) { if (auto role = GetRole(name)) { GrantDatabase(db, *role, system_tx); return SUCCESS; } return NO_ROLE; } if (auto user = GetUser(name)) { GrantDatabase(db, *user, system_tx); return SUCCESS; } if (auto role = GetRole(name)) { GrantDatabase(db, *role, system_tx); return SUCCESS; } return NO_USER_ROLE; } void Auth::GrantDatabase(const std::string &db, User &user, system::Transaction *system_tx) { if (db == kAllDatabases) { user.db_access().GrantAll(); } else { user.db_access().Grant(db); } SaveUser(user, system_tx); } void Auth::GrantDatabase(const std::string &db, Role &role, system::Transaction *system_tx) { if (db == kAllDatabases) { role.db_access().GrantAll(); } else { role.db_access().Grant(db); } SaveRole(role, system_tx); } Auth::Result Auth::DenyDatabase(const std::string &db, const std::string &name, system::Transaction *system_tx) { using enum Auth::Result; if (module_.IsUsed()) { if (auto role = GetRole(name)) { DenyDatabase(db, *role, system_tx); return SUCCESS; } return NO_ROLE; } if (auto user = GetUser(name)) { DenyDatabase(db, *user, system_tx); return SUCCESS; } if (auto role = GetRole(name)) { DenyDatabase(db, *role, system_tx); return SUCCESS; } return NO_USER_ROLE; } void Auth::DenyDatabase(const std::string &db, User &user, system::Transaction *system_tx) { if (db == kAllDatabases) { user.db_access().DenyAll(); } else { user.db_access().Deny(db); } SaveUser(user, system_tx); } void Auth::DenyDatabase(const std::string &db, Role &role, system::Transaction *system_tx) { if (db == kAllDatabases) { role.db_access().DenyAll(); } else { role.db_access().Deny(db); } SaveRole(role, system_tx); } Auth::Result Auth::RevokeDatabase(const std::string &db, const std::string &name, system::Transaction *system_tx) { using enum Auth::Result; if (module_.IsUsed()) { if (auto role = GetRole(name)) { RevokeDatabase(db, *role, system_tx); return SUCCESS; } return NO_ROLE; } if (auto user = GetUser(name)) { RevokeDatabase(db, *user, system_tx); return SUCCESS; } if (auto role = GetRole(name)) { RevokeDatabase(db, *role, system_tx); return SUCCESS; } return NO_USER_ROLE; } void Auth::RevokeDatabase(const std::string &db, User &user, system::Transaction *system_tx) { if (db == kAllDatabases) { user.db_access().RevokeAll(); } else { user.db_access().Revoke(db); } SaveUser(user, system_tx); } void Auth::RevokeDatabase(const std::string &db, Role &role, system::Transaction *system_tx) { if (db == kAllDatabases) { role.db_access().RevokeAll(); } else { role.db_access().Revoke(db); } SaveRole(role, system_tx); } void Auth::DeleteDatabase(const std::string &db, system::Transaction *system_tx) { for (auto it = storage_.begin(kUserPrefix); it != storage_.end(kUserPrefix); ++it) { auto username = it->first.substr(kUserPrefix.size()); try { User user = auth::User::Deserialize(ParseJson(it->second)); LinkUser(user); user.db_access().Revoke(db); SaveUser(user, system_tx); } catch (AuthException &) { continue; } } for (auto it = storage_.begin(kRolePrefix); it != storage_.end(kRolePrefix); ++it) { auto rolename = it->first.substr(kRolePrefix.size()); try { auto role = memgraph::auth::Role::Deserialize(ParseJson(it->second)); role.db_access().Revoke(db); SaveRole(role, system_tx); } catch (AuthException &) { continue; } } } Auth::Result Auth::SetMainDatabase(std::string_view db, const std::string &name, system::Transaction *system_tx) { using enum Auth::Result; if (module_.IsUsed()) { if (auto role = GetRole(name)) { SetMainDatabase(db, *role, system_tx); return SUCCESS; } return NO_ROLE; } if (auto user = GetUser(name)) { SetMainDatabase(db, *user, system_tx); return SUCCESS; } if (auto role = GetRole(name)) { SetMainDatabase(db, *role, system_tx); return SUCCESS; } return NO_USER_ROLE; } void Auth::SetMainDatabase(std::string_view db, User &user, system::Transaction *system_tx) { if (!user.db_access().SetMain(db)) { throw AuthException("Couldn't set default database '{}' for '{}'!", db, user.username()); } SaveUser(user, system_tx); } void Auth::SetMainDatabase(std::string_view db, Role &role, system::Transaction *system_tx) { if (!role.db_access().SetMain(db)) { throw AuthException("Couldn't set default database '{}' for '{}'!", db, role.rolename()); } SaveRole(role, system_tx); } #endif bool Auth::NameRegexMatch(const std::string &user_or_role) const { if (config_.custom_name_regex) { if (const auto license_check_result = memgraph::license::global_license_checker.IsEnterpriseValid(memgraph::utils::global_settings); license_check_result.HasError()) { throw memgraph::auth::AuthException( "Custom user/role regex is a Memgraph Enterprise feature. Please set the config " "(\"--auth-user-or-role-name-regex\") to its default value (\"{}\") or remove the flag.\n{}", glue::kDefaultUserRoleRegex, memgraph::license::LicenseCheckErrorToString(license_check_result.GetError(), "user/role regex")); } } return std::regex_match(user_or_role, config_.name_regex); } } // namespace memgraph::auth