diff --git a/src/atomdb/BUILD b/src/atomdb/BUILD index 95015e91..d3ddcb24 100644 --- a/src/atomdb/BUILD +++ b/src/atomdb/BUILD @@ -13,6 +13,7 @@ cc_library( ":atomdbutils", ":protected_atomdb", "//atomdb/adapterdb:adapterdb_lib", + "//atomdb/auth:auth_lib", "//atomdb/inmemorydb:inmemorydb_lib", "//atomdb/morkdb:morkdb_lib", "//atomdb/redis_mongodb:redis_mongodb_lib", @@ -89,6 +90,7 @@ cc_library( deps = [ ":atomdb", ":atomdb_api_types", + "//atomdb/auth:auth_lib", "//commons:commons_lib", "//commons/atoms:atoms_lib", ], diff --git a/src/atomdb/ProtectedAtomDB.cc b/src/atomdb/ProtectedAtomDB.cc index 4bbe1ad5..64596611 100644 --- a/src/atomdb/ProtectedAtomDB.cc +++ b/src/atomdb/ProtectedAtomDB.cc @@ -15,6 +15,9 @@ ProtectedAtomDB::ProtectedAtomDB(shared_ptr backend) : backend(backend) if (this->backend == nullptr) { RAISE_ERROR("ProtectedAtomDB requires a non-null backend AtomDB"); } + + // TODO: initialize ManifestAuthorizer + LOG_INFO("ProtectedAtomDB initialized"); } diff --git a/src/atomdb/ProtectedAtomDB.h b/src/atomdb/ProtectedAtomDB.h index 67f4489e..e21036eb 100644 --- a/src/atomdb/ProtectedAtomDB.h +++ b/src/atomdb/ProtectedAtomDB.h @@ -6,6 +6,7 @@ #include #include "AtomDB.h" +#include "ManifestAuthorizer.h" using namespace std; using namespace atoms; @@ -163,6 +164,7 @@ class ProtectedAtomDB : public AtomDB { private: shared_ptr backend; + shared_ptr auth; [[noreturn]] static void raise_public_key_required(const string& method_name); }; diff --git a/src/atomdb/auth/AuthorizationManager.cc b/src/atomdb/auth/AuthorizationManager.cc new file mode 100644 index 00000000..24bd5bdf --- /dev/null +++ b/src/atomdb/auth/AuthorizationManager.cc @@ -0,0 +1,37 @@ + +#include "AuthorizationManager.h" + +using namespace std; +using namespace atomdb; + +// -------------------------------------------------------------------------------- +// Constructor + +AuthorizationManager::AuthorizationManager(shared_ptr persistence) + : persistence(persistence) { + if (!this->persistence) { + RAISE_ERROR("Authorization persistence is required"); + } +} + +// -------------------------------------------------------------------------------- +// Public methods + +vector atomdb::AuthorizationManager::list( + const string& public_key) { + return this->persistence->list(public_key); +} + +void AuthorizationManager::authorize(const string& public_key, + const atomdb_api_types::AccessPermissionEntry& entry) { + this->persistence->save(public_key, entry); +} + +void AuthorizationManager::revoke(const string& public_key, + const atomdb_api_types::AccessPermissionEntry& entry) { + this->persistence->remove(public_key, entry); +} + +void AuthorizationManager::revoke_all(const string& public_key) { + this->persistence->remove_all(public_key); +} diff --git a/src/atomdb/auth/AuthorizationManager.h b/src/atomdb/auth/AuthorizationManager.h new file mode 100644 index 00000000..2f5f6309 --- /dev/null +++ b/src/atomdb/auth/AuthorizationManager.h @@ -0,0 +1,51 @@ + +#pragma once + +#include +#include + +#include "AtomDBAPITypes.h" +#include "AuthorizationPersistence.h" + +using namespace std; +using namespace atoms; + +namespace atomdb { + +/** + * @brief Manages authorization permissions through persistent storage. + */ +class AuthorizationManager { + public: + /** + * @param persistence Storage used to manage authorization permissions. + */ + AuthorizationManager(shared_ptr persistence); + + ~AuthorizationManager() = default; + + /** + * @brief Lists all permissions granted to public_key. + */ + vector list(const string& public_key); + + /** + * @brief Grants an authorization entry to public_key. + */ + void authorize(const string& public_key, const atomdb_api_types::AccessPermissionEntry& entry); + + /** + * @brief Revokes an authorization entry from public_key. + */ + void revoke(const string& public_key, const atomdb_api_types::AccessPermissionEntry& entry); + + /** + * @brief Revokes all authorization entries from public_key. + */ + void revoke_all(const string& public_key); + + private: + shared_ptr persistence; +}; + +} // namespace atomdb \ No newline at end of file diff --git a/src/atomdb/auth/AuthorizationManifest.cc b/src/atomdb/auth/AuthorizationManifest.cc new file mode 100644 index 00000000..0cdf87eb --- /dev/null +++ b/src/atomdb/auth/AuthorizationManifest.cc @@ -0,0 +1,77 @@ +#include "AuthorizationManifest.h" + +#include + +#define LOG_LEVEL INFO_LEVEL +#include "Logger.h" +#include "Utils.h" + +using namespace std; +using namespace atomdb; + +// -------------------------------------------------------------------------------- +// Public methods + +void AuthorizationManifest::set(const atomdb_api_types::AccessPermissionDocument& document) { + this->documents.insert_or_assign(document.access_key, document); +} + +void AuthorizationManifest::add(const string& public_key, + const atomdb_api_types::AccessPermissionEntry& entry) { + auto document = this->get_document(public_key); + + if (document == nullptr) { + this->documents.emplace(public_key, + atomdb_api_types::AccessPermissionDocument(public_key, false, {entry})); + return; + } + + vector& entries = document->entries; + + for (auto& existing : entries) { + if (existing.schema.handle() == entry.schema.handle()) { + existing = entry; + return; + } + } + + entries.push_back(entry); +} + +void AuthorizationManifest::remove(const string& public_key, + const atomdb_api_types::AccessPermissionEntry& entry) { + auto document = this->get_document(public_key); + + if (document == nullptr) return; + + vector& entries = document->entries; + + for (auto it = entries.begin(); it != entries.end(); ++it) { + if (it->schema.handle() == entry.schema.handle()) { + entries.erase(it); + return; + } + } +} + +void AuthorizationManifest::remove_all(const string& public_key) { this->documents.erase(public_key); } + +bool AuthorizationManifest::is_registered(const string& public_key) const { + return this->documents.find(public_key) != this->documents.end(); +} + +bool AuthorizationManifest::full_access(const string& public_key) { + auto document = this->get_document(public_key); + if (document == nullptr) return false; + return document->full_access; +} + +atomdb_api_types::AccessPermissionDocument* AuthorizationManifest::get_document( + const string& public_key) { + auto it = this->documents.find(public_key); + + if (it == this->documents.end()) { + return nullptr; + } + return &it->second; +} diff --git a/src/atomdb/auth/AuthorizationManifest.h b/src/atomdb/auth/AuthorizationManifest.h new file mode 100644 index 00000000..6030e746 --- /dev/null +++ b/src/atomdb/auth/AuthorizationManifest.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include + +#include "AtomDBAPITypes.h" + +using namespace std; + +namespace atomdb { + +/** + * @brief In-memory representation of the authorization state. + * + * Stores the authorization documents used by the authorization checks. + * The manifest is independent of the underlying persistence mechanism. + */ +class AuthorizationManifest { + public: + AuthorizationManifest() = default; + ~AuthorizationManifest() = default; + + /** + * @brief Replaces or inserts the authorization document. + */ + void set(const atomdb_api_types::AccessPermissionDocument& document); + + /** + * @brief Adds an authorization entry for public_key. + * + * Creates the authorization document if public_key is not registered. + */ + void add(const string& public_key, const atomdb_api_types::AccessPermissionEntry& entry); + + /** + * @brief Removes an authorization entry from public_key. + * + * Does nothing if public_key or the specified entry is not present. + */ + void remove(const string& public_key, const atomdb_api_types::AccessPermissionEntry& entry); + + /** + * @brief Removes all authorization entries for public_key. + * + * Does nothing if public_key is not registered. + */ + void remove_all(const string& public_key); + + /** + * @brief Returns whether public_key has an authorization document. + */ + bool is_registered(const string& public_key) const; + + /** + * @brief Returns whether public_key has full access. + */ + bool full_access(const string& public_key); + + /** + * @brief Returns a document from public_key. + */ + atomdb_api_types::AccessPermissionDocument* get_document(const string& public_key); + + private: + map documents; +}; + +} // namespace atomdb diff --git a/src/atomdb/auth/AuthorizationPersistence.h b/src/atomdb/auth/AuthorizationPersistence.h new file mode 100644 index 00000000..69e525e5 --- /dev/null +++ b/src/atomdb/auth/AuthorizationPersistence.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "AtomDBAPITypes.h" + +using namespace std; + +namespace atomdb { + +/** + * @brief Persistence interface for authorization data. + */ +class AuthorizationPersistence { + public: + virtual ~AuthorizationPersistence() = default; + + /** + * @brief Lists all authorization entries for public_key. + */ + virtual vector list(const string& public_key) = 0; + + /** + * @brief Persists an authorization entry for public_key. + */ + virtual void save(const string& public_key, + const atomdb_api_types::AccessPermissionEntry& entry) = 0; + + /** + * @brief Removes an authorization entry from public_key. + */ + virtual void remove(const string& public_key, + const atomdb_api_types::AccessPermissionEntry& entry) = 0; + + /** + * @brief Removes all authorization entries for public_key. + */ + virtual void remove_all(const string& public_key) = 0; +}; +} // namespace atomdb diff --git a/src/atomdb/auth/BUILD b/src/atomdb/auth/BUILD new file mode 100644 index 00000000..bae7380e --- /dev/null +++ b/src/atomdb/auth/BUILD @@ -0,0 +1,78 @@ +load("@rules_cc//cc:cc_library.bzl", "cc_library") + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "auth_lib", + includes = ["."], + deps = [ + ":authorization_manager", + ":authorization_manifest", + ":manifest_authorizer", + ":mongo_authorization_persistence", + ], +) + +cc_library( + name = "authorization_persistence", + hdrs = ["AuthorizationPersistence.h"], + includes = ["."], + deps = [ + "//atomdb:atomdb_api_types", + ], +) + +cc_library( + name = "mongo_authorization_persistence", + srcs = ["MongoAuthorizationPersistence.cc"], + hdrs = ["MongoAuthorizationPersistence.h"], + includes = ["."], + deps = [ + ":authorization_persistence", + "//atomdb:atomdb_api_types", + "//commons:commons_lib", + "//commons/atoms:atoms_lib", + "//hasher:hasher_lib", + "@nlohmann_json//:json", + ], +) + +cc_library( + name = "authorization_manager", + srcs = ["AuthorizationManager.cc"], + hdrs = ["AuthorizationManager.h"], + includes = ["."], + deps = [ + ":mongo_authorization_persistence", + "//atomdb", + "//atomdb:atomdb_api_types", + "//commons:commons_lib", + "//commons/atoms:atoms_lib", + ], +) + +cc_library( + name = "manifest_authorizer", + srcs = ["ManifestAuthorizer.cc"], + hdrs = ["ManifestAuthorizer.h"], + includes = ["."], + deps = [ + ":authorization_manifest", + "//atomdb", + "//atomdb:atomdb_api_types", + "//commons:commons_lib", + "//commons/atoms:atoms_lib", + ], +) + +cc_library( + name = "authorization_manifest", + srcs = ["AuthorizationManifest.cc"], + hdrs = ["AuthorizationManifest.h"], + includes = ["."], + deps = [ + "//atomdb:atomdb_api_types", + "//commons:commons_lib", + "//commons/atoms:atoms_lib", + ], +) diff --git a/src/atomdb/auth/ManifestAuthorizer.cc b/src/atomdb/auth/ManifestAuthorizer.cc new file mode 100644 index 00000000..91aebf05 --- /dev/null +++ b/src/atomdb/auth/ManifestAuthorizer.cc @@ -0,0 +1,97 @@ +#include "ManifestAuthorizer.h" + +#include "Assignment.h" +#include "Atom.h" +#include "AtomDB.h" +#include "Link.h" +#include "Utils.h" + +using namespace atomdb; +using namespace atoms; +using namespace commons; + +// -------------------------------------------------------------------------------- +// Constructor + +ManifestAuthorizer::ManifestAuthorizer(shared_ptr manifest) : manifest(manifest) { + if (this->manifest == nullptr) { + RAISE_ERROR("ManifestAuthorizer requires a non-null AuthorizationManifest"); + } +} + +// -------------------------------------------------------------------------------- +// Public methods + +bool ManifestAuthorizer::is_authorized(const Atom& atom, + const string& public_key, + AuthorizationOperation operation, + HandleDecoder& decoder) { + if (!this->manifest->is_registered(public_key)) { + return false; + } + if (this->manifest->full_access(public_key)) { + return true; + } + + auto document = this->manifest->get_document(public_key); + for (const auto& entry : document->entries) { + if (this->allows(entry, operation) && this->matches_schema(entry.schema, atom, decoder)) { + return true; + } + } + return false; +} + +bool ManifestAuthorizer::is_authorized(const string& handle, + const string& public_key, + AuthorizationOperation operation, + HandleDecoder& decoder) { + if (!this->manifest->is_registered(public_key)) { + return false; + } + if (this->manifest->full_access(public_key)) { + return true; + } + + auto document = this->manifest->get_document(public_key); + for (const auto& entry : document->entries) { + if (allows(entry, operation) && this->matches_schema(entry.schema, handle, decoder)) { + return true; + } + } + return false; +} + +// -------------------------------------------------------------------------------- +// Private methods + +bool ManifestAuthorizer::allows(const atomdb_api_types::AccessPermissionEntry& entry, + AuthorizationOperation operation) { + switch (operation) { + case AuthorizationOperation::READ: + return entry.read; + case AuthorizationOperation::WRITE: + return entry.write; + } + return false; +} + +bool ManifestAuthorizer::matches_schema(const LinkSchema& schema, + const Atom& atom, + HandleDecoder& decoder) const { + Assignment assignment; + LinkSchema local_schema(schema); + if (Atom::is_link(atom)) { + auto& link = const_cast(static_cast(atom)); + return local_schema.match(link, assignment, decoder); + } + return local_schema.match(atom.handle(), assignment, decoder); +} + +bool ManifestAuthorizer::matches_schema(const LinkSchema& schema, + const string& handle, + HandleDecoder& decoder) const { + Assignment assignment; + LinkSchema local_schema(schema); + return local_schema.match(handle, assignment, decoder); +} \ No newline at end of file diff --git a/src/atomdb/auth/ManifestAuthorizer.h b/src/atomdb/auth/ManifestAuthorizer.h new file mode 100644 index 00000000..6c1fae44 --- /dev/null +++ b/src/atomdb/auth/ManifestAuthorizer.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include "Atom.h" +#include "AtomDBAPITypes.h" +#include "AuthorizationManifest.h" +#include "HandleDecoder.h" + +using namespace std; +using namespace atoms; + +namespace atomdb { + +enum class AuthorizationOperation { READ, WRITE }; + +/** + * @brief Evaluates authorization requests using an in-memory AuthorizationManifest. + */ +class ManifestAuthorizer { + public: + explicit ManifestAuthorizer(shared_ptr manifest); + ~ManifestAuthorizer() = default; + + /** + * @brief Checks whether public_key is authorized to perform an operation on an atom. + */ + bool is_authorized(const Atom& atom, + const string& public_key, + AuthorizationOperation operation, + HandleDecoder& decoder); + + /** + * @brief Checks whether public_key is authorized to perform an operation on a handle. + */ + bool is_authorized(const string& handle, + const string& public_key, + AuthorizationOperation operation, + HandleDecoder& decoder); + + private: + shared_ptr manifest; + + bool matches_schema(const LinkSchema& schema, const Atom& atom, HandleDecoder& decoder) const; + bool matches_schema(const LinkSchema& schema, const string& handle, HandleDecoder& decoder) const; + + static bool allows(const atomdb_api_types::AccessPermissionEntry& entry, + AuthorizationOperation operation); +}; + +} // namespace atomdb diff --git a/src/atomdb/auth/MongoAuthorizationPersistence.cc b/src/atomdb/auth/MongoAuthorizationPersistence.cc new file mode 100644 index 00000000..3c691c9c --- /dev/null +++ b/src/atomdb/auth/MongoAuthorizationPersistence.cc @@ -0,0 +1,195 @@ +#include "MongoAuthorizationPersistence.h" + +#include "Hasher.h" +#include "JsonConfig.h" +#include "MongoInitializer.h" +#include "Utils.h" +#include "expression_hasher.h" +#define LOG_LEVEL INFO_LEVEL +#include "Logger.h" +#include "nlohmann/json.hpp" + +using namespace atomdb; +using namespace commons; + +// -------------------------------------------------------------------------------- +// Constructors + +MongoAuthorizationPersistence::MongoAuthorizationPersistence(const string& endpoint, + const string& username, + const string& password, + const string& database_name, + const string& collection_name) { + if (endpoint.empty() || endpoint == ":" || username.empty() || password.empty()) { + RAISE_ERROR("Invalid MongoDB configuration: need non-empty username, username, and password."); + } + + string url = "mongodb://" + username + ":" + password + "@" + endpoint; + + MongoInitializer::initialize(); + + try { + auto uri = mongocxx::uri{url}; + this->mongodb_pool = new mongocxx::pool(uri); + auto conn = this->mongodb_pool->acquire(); + auto mongodb = (*conn)[database_name]; + const auto ping_cmd = + bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp("ping", 1)); + mongodb.run_command(ping_cmd.view()); + LOG_DEBUG("MongoAuthorizationPersistence connected to MongoDB at " + << endpoint << " (db=" << database_name << ")"); + } catch (const exception& e) { + RAISE_ERROR(e.what()); + } +} + +MongoAuthorizationPersistence::~MongoAuthorizationPersistence() { delete this->mongodb_pool; } + +// -------------------------------------------------------------------------------- +// Public methods + +vector MongoAuthorizationPersistence::list( + const string& public_key) { + auto conn = this->mongodb_pool->acquire(); + auto collection = (*conn)[this->database_name][this->collection_name]; + auto document = this->get_document(collection, public_key); + if (!document) return {}; + return document->entries; +} + +void MongoAuthorizationPersistence::save(const string& public_key, + const atomdb_api_types::AccessPermissionEntry& entry) { + auto conn = this->mongodb_pool->acquire(); + auto collection = (*conn)[this->database_name][this->collection_name]; + + auto access_document = this->get_document(collection, public_key); + + string id; + string public_key_; + bool full_access = false; + auto schemas = bsoncxx::builder::basic::array{}; + + if (access_document) { + id = Hasher::plain_string_hash(access_document->access_key); + public_key_ = access_document->access_key; + full_access = access_document->full_access; + + bool entry_exists = false; + + for (const auto& document_entry : access_document->entries) { + if (document_entry.schema.handle() == entry.schema.handle()) { + schemas.append(this->entry_to_document(entry)); + entry_exists = true; + } else { + schemas.append(this->entry_to_document(document_entry)); + } + } + + if (!entry_exists) { + schemas.append(this->entry_to_document(entry)); + } + } else { + id = Hasher::plain_string_hash(public_key); + public_key_ = public_key; + schemas.append(this->entry_to_document(entry)); + } + + auto new_access_document = + bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp("_id", id), + bsoncxx::builder::basic::kvp("public_key", public_key_), + bsoncxx::builder::basic::kvp("full_access", full_access), + bsoncxx::builder::basic::kvp("allowed_schemas", schemas)); + + auto filter = bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp("_id", id)); + + mongocxx::options::replace opts; + opts.upsert(true); + + auto reply = collection.replace_one(filter.view(), new_access_document.view(), opts); + + if (!reply) { + RAISE_ERROR("Failed to update authorization entry in MongoDB"); + } +} + +void MongoAuthorizationPersistence::remove(const string& public_key, + const atomdb_api_types::AccessPermissionEntry& entry) { + auto conn = this->mongodb_pool->acquire(); + auto collection = (*conn)[this->database_name][this->collection_name]; + + auto access_document = this->get_document(collection, public_key); + + if (!access_document) return; + + auto schemas = bsoncxx::builder::basic::array{}; + for (const auto& document_entry : access_document->entries) { + if (document_entry.schema.handle() == entry.schema.handle()) { + continue; // Skip the entry to remove + } else { + schemas.append(this->entry_to_document(document_entry)); + } + } + + string id = Hasher::plain_string_hash(access_document->access_key); + + auto new_access_document = bsoncxx::builder::basic::make_document( + bsoncxx::builder::basic::kvp("_id", id), + bsoncxx::builder::basic::kvp("public_key", access_document->access_key), + bsoncxx::builder::basic::kvp("full_access", access_document->full_access), + bsoncxx::builder::basic::kvp("allowed_schemas", schemas)); + + auto filter = bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp("_id", id)); + + auto reply = collection.replace_one(filter.view(), new_access_document.view()); + + if (!reply) { + RAISE_ERROR("Failed to update authorization entry in MongoDB"); + } +} + +void MongoAuthorizationPersistence::remove_all(const string& public_key) { + auto conn = this->mongodb_pool->acquire(); + auto collection = (*conn)[this->database_name][this->collection_name]; + auto reply = collection.delete_one(bsoncxx::builder::basic::make_document( + bsoncxx::builder::basic::kvp("_id", Hasher::plain_string_hash(public_key)))); + if (!reply) { + RAISE_ERROR("Failed to remove authorization document from MongoDB"); + } +} + +// -------------------------------------------------------------------------------- +// Private methods + +bsoncxx::document::value MongoAuthorizationPersistence::entry_to_document( + const atomdb_api_types::AccessPermissionEntry& entry) { + auto tokens_array = bsoncxx::builder::basic::array{}; + auto local_schema = entry.schema; + for (const auto& token : local_schema.tokenize()) { + tokens_array.append(token); + } + return bsoncxx::builder::basic::make_document(bsoncxx::builder::basic::kvp("tokens", tokens_array), + bsoncxx::builder::basic::kvp("read", entry.read), + bsoncxx::builder::basic::kvp("write", entry.write)); +} + +shared_ptr MongoAuthorizationPersistence::get_document( + mongocxx::collection& collection, const string& public_key) { + auto reply = collection.find_one(bsoncxx::v_noabi::builder::basic::make_document( + bsoncxx::v_noabi::builder::basic::kvp("_id", Hasher::plain_string_hash(public_key)))); + + if (!reply) return nullptr; + + auto document_json = nlohmann::json::parse(bsoncxx::to_json(reply.value().view())); + + vector entries; + for (const auto& item : document_json["allowed_schemas"]) { + vector tokens; + for (const auto& token : item["tokens"]) { + tokens.push_back(token.get()); + } + entries.emplace_back(tokens, item["read"].get(), item["write"].get()); + } + + return make_shared( + public_key, document_json["full_access"].get(), entries); +} diff --git a/src/atomdb/auth/MongoAuthorizationPersistence.h b/src/atomdb/auth/MongoAuthorizationPersistence.h new file mode 100644 index 00000000..9c96f76a --- /dev/null +++ b/src/atomdb/auth/MongoAuthorizationPersistence.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AuthorizationPersistence.h" + +using namespace std; + +namespace atomdb { + +/** + * @brief MongoDB implementation of the authorization persistence interface. + */ +class MongoAuthorizationPersistence : public AuthorizationPersistence { + public: + /** + * @param endpoint MongoDB server endpoint in the format ip:port. + * @param username MongoDB username. + * @param password MongoDB password. + * @param database_name MongoDB database name. + * @param collection_name MongoDB collection used to store authorization data. + */ + MongoAuthorizationPersistence(const string& endpoint, + const string& username, + const string& password, + const string& database_name, + const string& collection_name); + ~MongoAuthorizationPersistence(); + + /** + * @brief Lists all authorization entries for public_key. + */ + vector list(const string& public_key) override; + + /** + * @brief Persists an authorization entry for public_key. + */ + void save(const string& public_key, const atomdb_api_types::AccessPermissionEntry& entry) override; + + /** + * @brief Removes an authorization entry from public_key. + */ + void remove(const string& public_key, const atomdb_api_types::AccessPermissionEntry& entry) override; + + /** + * @brief Removes all authorization entries for public_key. + */ + void remove_all(const string& public_key) override; + + private: + mongocxx::pool* mongodb_pool; + string database_name; + string collection_name; + + bsoncxx::document::value entry_to_document(const atomdb_api_types::AccessPermissionEntry& entry); + shared_ptr get_document(mongocxx::collection& collection, + const string& public_key); +}; + +} // namespace atomdb diff --git a/src/tests/cpp/BUILD b/src/tests/cpp/BUILD index e026ea3e..b2d0f0be 100644 --- a/src/tests/cpp/BUILD +++ b/src/tests/cpp/BUILD @@ -867,6 +867,23 @@ cc_test( ], ) +cc_test( + name = "authorization_test", + size = "small", + srcs = ["authorization_test.cc"], + copts = [ + "-Iexternal/gtest/googletest/include", + "-Iexternal/gtest/googletest", + ], + linkstatic = 1, + deps = [ + "//atomdb/auth:auth_lib", + "//atomdb/inmemorydb:inmemorydb_lib", + "//commons/atoms:atoms_lib", + "@com_github_google_googletest//:gtest_main", + ], +) + cc_test( name = "atomdb_factory_test", size = "medium", diff --git a/src/tests/cpp/authorization_test.cc b/src/tests/cpp/authorization_test.cc new file mode 100644 index 00000000..4370b9b6 --- /dev/null +++ b/src/tests/cpp/authorization_test.cc @@ -0,0 +1,182 @@ +#include + +#include +#include +#include +#include + +#include "AuthorizationManager.h" +#include "AuthorizationManifest.h" +#include "AuthorizationPersistence.h" +#include "InMemoryDB.h" +#include "Link.h" +#include "LinkSchema.h" +#include "ManifestAuthorizer.h" +#include "Node.h" + +using namespace atomdb; +using namespace atomdb_api_types; +using namespace atoms; +using namespace std; + +namespace { + +vector inheritance_mammal_tokens() { + return {"LINK_TEMPLATE", + "Expression", + "3", + "NODE", + "Symbol", + "Inheritance", + "VARIABLE", + "x", + "NODE", + "Symbol", + "\"mammal\""}; +} + +AccessPermissionEntry read_only_inheritance_entry() { + return AccessPermissionEntry(inheritance_mammal_tokens(), true, false); +} + +class DummyPersistence : public AuthorizationPersistence { + public: + map> documents; + + vector list(const string& public_key) override { + return {}; + } + + void save(const string& public_key, const atomdb_api_types::AccessPermissionEntry& entry) override { + auto& handles = documents[public_key]; + handles.push_back(entry.schema.handle()); + } + + void remove(const string& public_key, + const atomdb_api_types::AccessPermissionEntry& entry) override { + auto it = documents.find(public_key); + if (it == documents.end()) { + return; + } + + auto& handles = it->second; + + auto handle = entry.schema.handle(); + + handles.erase(std::remove(handles.begin(), handles.end(), handle), handles.end()); + } + + void remove_all(const string& public_key) override { documents.erase(public_key); } +}; + +shared_ptr db_with_inheritance_link(string* link_handle) { + auto db = make_shared("auth_test_"); + auto human = new Node("Symbol", "\"human\""); + auto mammal = new Node("Symbol", "\"mammal\""); + auto inheritance = new Node("Symbol", "Inheritance"); + string human_handle = db->add_node(human); + string mammal_handle = db->add_node(mammal); + string inheritance_handle = db->add_node(inheritance); + auto link = new Link("Expression", {inheritance_handle, human_handle, mammal_handle}); + *link_handle = db->add_link(link); + delete human; + delete mammal; + delete inheritance; + delete link; + return db; +} + +} // namespace + +TEST(AuthorizationManifestTest, ManagesAuthorizationEntriesAndAccess) { + AuthorizationManifest manifest; + string public_key = "pk1"; + + EXPECT_FALSE(manifest.is_registered(public_key)); + EXPECT_FALSE(manifest.full_access(public_key)); + + EXPECT_EQ(manifest.get_document(public_key), nullptr); + + AccessPermissionEntry entry = read_only_inheritance_entry(); + manifest.add(public_key, entry); + + ASSERT_TRUE(manifest.is_registered(public_key)); + + auto doc = manifest.get_document(public_key); + ASSERT_NE(doc, nullptr); + + auto entries = doc->entries; + ASSERT_EQ(entries.size(), 1u); + EXPECT_TRUE(entries[0].read); + EXPECT_FALSE(entries[0].write); + EXPECT_EQ(entries[0].schema.handle(), entry.schema.handle()); + + manifest.remove(public_key, entry); + EXPECT_TRUE(manifest.is_registered(public_key)); + + EXPECT_TRUE(manifest.get_document(public_key)->entries.empty()); + + manifest.set(AccessPermissionDocument(public_key, true, {})); + EXPECT_TRUE(manifest.full_access(public_key)); + + manifest.remove_all(public_key); + EXPECT_FALSE(manifest.is_registered(public_key)); +} + +TEST(AuthorizationManifestTest, AddReplacesEntryWithSameSchemaHandle) { + AuthorizationManifest manifest; + string public_key = "pk1"; + manifest.add(public_key, AccessPermissionEntry(inheritance_mammal_tokens(), true, false)); + manifest.add(public_key, AccessPermissionEntry(inheritance_mammal_tokens(), false, true)); + + auto entries = manifest.get_document(public_key)->entries; + + ASSERT_EQ(entries.size(), 1u); + EXPECT_FALSE(entries[0].read); + EXPECT_TRUE(entries[0].write); +} + +TEST(ManifestAuthorizerTest, IsAuthorized) { + string link_handle; + auto db = db_with_inheritance_link(&link_handle); + + auto manifest = make_shared(); + auto authorizer = make_shared(manifest); + + auto link = db->get_link(link_handle); + ASSERT_NE(link, nullptr); + EXPECT_FALSE(authorizer->is_authorized(*link, "unknown", AuthorizationOperation::READ, *db)); + EXPECT_FALSE(authorizer->is_authorized(link_handle, "unknown", AuthorizationOperation::READ, *db)); + + AccessPermissionEntry entry = read_only_inheritance_entry(); + manifest->add("pk", entry); + EXPECT_TRUE(authorizer->is_authorized(*link, "pk", AuthorizationOperation::READ, *db)); + EXPECT_TRUE(authorizer->is_authorized(link_handle, "pk", AuthorizationOperation::READ, *db)); + + EXPECT_FALSE(authorizer->is_authorized(*link, "pk", AuthorizationOperation::WRITE, *db)); + EXPECT_FALSE(authorizer->is_authorized(link_handle, "pk", AuthorizationOperation::WRITE, *db)); +} + +TEST(AuthorizationManagerTest, AuthorizeThenReadAndWriteFlags) { + string link_handle; + auto db = db_with_inheritance_link(&link_handle); + + auto persistence = make_shared(); + AuthorizationManager manager(persistence); + AccessPermissionEntry entry = read_only_inheritance_entry(); + + manager.authorize("pk", entry); + manager.authorize("pk2", entry); + + EXPECT_EQ(persistence->documents.size(), 2); + EXPECT_EQ(persistence->documents["pk"], vector{entry.schema.handle()}); + EXPECT_EQ(persistence->documents["pk2"], vector{entry.schema.handle()}); + + manager.revoke("pk", entry); + EXPECT_EQ(persistence->documents.size(), 2); + EXPECT_EQ(persistence->documents["pk"], vector{}); + EXPECT_EQ(persistence->documents["pk2"], vector{entry.schema.handle()}); + + manager.revoke_all("pk"); + EXPECT_EQ(persistence->documents.size(), 1); +} \ No newline at end of file