diff --git a/core/database/CMakeLists.txt b/core/database/CMakeLists.txt index a8ff45f7c..384ba68d9 100644 --- a/core/database/CMakeLists.txt +++ b/core/database/CMakeLists.txt @@ -15,11 +15,13 @@ if( ENABLE_FOXX_TESTS ) add_test(NAME foxx_teardown COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_teardown.sh") add_test(NAME foxx_authz COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_authz") add_test(NAME foxx_record COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_record") + add_test(NAME foxx_repo COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_repo") add_test(NAME foxx_path COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_path") add_test(NAME foxx_db_fixtures COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_fixture_setup.sh") add_test(NAME foxx_version COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_version") add_test(NAME foxx_support COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_support") - add_test(NAME foxx_user_router COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "user_router") + add_test(NAME foxx_user_router COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_user_router") + add_test(NAME foxx_authz_router COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_authz_router") set_tests_properties(foxx_setup PROPERTIES FIXTURES_SETUP Foxx) set_tests_properties(foxx_teardown PROPERTIES FIXTURES_CLEANUP Foxx) @@ -27,7 +29,9 @@ if( ENABLE_FOXX_TESTS ) set_tests_properties(foxx_version PROPERTIES FIXTURES_REQUIRED Foxx) set_tests_properties(foxx_support PROPERTIES FIXTURES_REQUIRED Foxx) set_tests_properties(foxx_authz PROPERTIES FIXTURES_REQUIRED Foxx) + set_tests_properties(foxx_authz_router PROPERTIES FIXTURES_REQUIRED Foxx) set_tests_properties(foxx_record PROPERTIES FIXTURES_REQUIRED Foxx) + set_tests_properties(foxx_repo PROPERTIES FIXTURES_REQUIRED Foxx) set_tests_properties(foxx_path PROPERTIES FIXTURES_REQUIRED Foxx) set_tests_properties(foxx_user_router PROPERTIES FIXTURES_REQUIRED "Foxx;FoxxDBFixtures") endif() diff --git a/core/database/foxx/api/authz.js b/core/database/foxx/api/authz.js index 33e773622..94950d541 100644 --- a/core/database/foxx/api/authz.js +++ b/core/database/foxx/api/authz.js @@ -2,7 +2,10 @@ const g_db = require("@arangodb").db; const path = require("path"); +const Record = require("./record"); +const pathModule = require("./posix_path"); const g_lib = require("./support"); +const { Repo, PathType } = require("./repo"); module.exports = (function () { let obj = {}; @@ -44,5 +47,145 @@ module.exports = (function () { return false; }; + obj.readRecord = function (client, path) { + const permission = g_lib.PERM_RD_DATA; + const path_components = pathModule.splitPOSIXPath(path); + const data_key = path_components.at(-1); + let record = new Record(data_key); + if (!record.exists()) { + // Return not found error for non-existent records + console.log("AUTHZ act: read client: " + client._id + " path " + path + " NOT_FOUND"); + throw [g_lib.ERR_NOT_FOUND, "Record not found: " + path]; + } + + // Special case - allow unknown client to read a publicly accessible record + // if record exists and if it is a public record + if (!client) { + if (!g_lib.hasPublicRead(record.id())) { + console.log("AUTHZ act: read" + " unknown client " + " path " + path + " FAILED"); + throw [ + g_lib.ERR_PERM_DENIED, + "Unknown client does not have read permissions on " + path, + ]; + } + } else if (!obj.isRecordActionAuthorized(client, data_key, permission)) { + console.log("AUTHZ act: read" + " client: " + client._id + " path " + path + " FAILED"); + throw [ + g_lib.ERR_PERM_DENIED, + "Client " + client._id + " does not have read permissions on " + path, + ]; + } + + if (!record.isPathConsistent(path)) { + console.log("AUTHZ act: read client: " + client._id + " path " + path + " FAILED"); + throw [record.error(), record.errorMessage()]; + } + }; + + obj.none = function (client, path) { + const permission = g_lib.PERM_NONE; + }; + + obj.denied = function (client, path) { + throw g_lib.ERR_PERM_DENIED; + }; + + obj.createRecord = function (client, path) { + const permission = g_lib.PERM_WR_DATA; + const path_components = pathModule.splitPOSIXPath(path); + const data_key = path_components.at(-1); + + if (!client) { + console.log( + "AUTHZ act: create" + " client: " + client._id + " path " + path + " FAILED", + ); + throw [ + g_lib.ERR_PERM_DENIED, + "Unknown client does not have create permissions on " + path, + ]; + } else if (!obj.isRecordActionAuthorized(client, data_key, permission)) { + console.log( + "AUTHZ act: create" + " client: " + client._id + " path " + path + " FAILED", + ); + throw [ + g_lib.ERR_PERM_DENIED, + "Client " + client._id + " does not have create permissions on " + path, + ]; + } + + let record = new Record(data_key); + // This does not mean the record exsts in the repo it checks if an entry + // exists in the database. + if (!record.exists()) { + // If the record does not exist then the path would not be consistent. + console.log("AUTHZ act: create client: " + client._id + " path " + path + " FAILED"); + throw [g_lib.ERR_PERM_DENIED, "Invalid record specified: " + path]; + } + + // This will tell us if the proposed path is consistent with what we expect + // GridFTP will fail if the posix file path does not exist. + if (!record.isPathConsistent(path)) { + console.log("AUTHZ act: create client: " + client._id + " path " + path + " FAILED"); + throw [record.error(), record.errorMessage()]; + } + }; + + obj.authz_strategy = { + read: { + [PathType.USER_PATH]: obj.none, + [PathType.USER_RECORD_PATH]: obj.readRecord, + [PathType.PROJECT_PATH]: obj.none, + [PathType.PROJECT_RECORD_PATH]: obj.readRecord, + [PathType.REPO_BASE_PATH]: obj.none, + [PathType.REPO_ROOT_PATH]: obj.none, + [PathType.REPO_PATH]: obj.none, + }, + write: { + [PathType.USER_PATH]: obj.none, + [PathType.USER_RECORD_PATH]: obj.none, + [PathType.PROJECT_PATH]: obj.none, + [PathType.PROJECT_RECORD_PATH]: obj.none, + [PathType.REPO_BASE_PATH]: obj.none, + [PathType.REPO_ROOT_PATH]: obj.none, + [PathType.REPO_PATH]: obj.none, + }, + create: { + [PathType.USER_PATH]: obj.none, + [PathType.USER_RECORD_PATH]: obj.createRecord, + [PathType.PROJECT_PATH]: obj.none, + [PathType.PROJECT_RECORD_PATH]: obj.createRecord, + [PathType.REPO_BASE_PATH]: obj.none, + [PathType.REPO_ROOT_PATH]: obj.none, + [PathType.REPO_PATH]: obj.none, + }, + delete: { + [PathType.USER_PATH]: obj.denied, + [PathType.USER_RECORD_PATH]: obj.denied, + [PathType.PROJECT_PATH]: obj.denied, + [PathType.PROJECT_RECORD_PATH]: obj.denied, + [PathType.REPO_BASE_PATH]: obj.denied, + [PathType.REPO_ROOT_PATH]: obj.denied, + [PathType.REPO_PATH]: obj.denied, + }, + chdir: { + [PathType.USER_PATH]: obj.none, + [PathType.USER_RECORD_PATH]: obj.none, + [PathType.PROJECT_PATH]: obj.none, + [PathType.PROJECT_RECORD_PATH]: obj.none, + [PathType.REPO_BASE_PATH]: obj.none, + [PathType.REPO_ROOT_PATH]: obj.none, + [PathType.REPO_PATH]: obj.none, + }, + lookup: { + [PathType.USER_PATH]: obj.none, + [PathType.USER_RECORD_PATH]: obj.none, + [PathType.PROJECT_PATH]: obj.none, + [PathType.PROJECT_RECORD_PATH]: obj.none, + [PathType.REPO_BASE_PATH]: obj.none, + [PathType.REPO_ROOT_PATH]: obj.none, + [PathType.REPO_PATH]: obj.none, + }, + }; + return obj; })(); diff --git a/core/database/foxx/api/authz_router.js b/core/database/foxx/api/authz_router.js index cf68e834a..ff160e764 100644 --- a/core/database/foxx/api/authz_router.js +++ b/core/database/foxx/api/authz_router.js @@ -5,9 +5,8 @@ const router = createRouter(); const joi = require("joi"); const g_db = require("@arangodb").db; const g_lib = require("./support"); -const pathModule = require("./posix_path"); // Replace with the actual file name -const Record = require("./record"); // Replace with the actual file name -const authzModule = require("./authz"); // Replace with the actual file name +const authzModule = require("./authz"); +const { Repo, PathType } = require("./repo"); module.exports = router; @@ -26,109 +25,36 @@ router ); // Client will contain the following information - // { - // "_key" : "bob", - // "_id" : "u/bob", - // "name" : "bob junior ", - // "name_first" : "bob", - // "name_last" : "jones", - // "is_admin" : true, - // "max_coll" : 50, - // "max_proj" : 10, - // "max_sav_qry" : 20, - // : - // "email" : "bobjones@gmail.com" - // } - const client = g_lib.getUserFromClientID_noexcept(req.queryParams.client); - - // Will split a posix path into an array - // E.g. - // req.queryParams.file = "/usr/local/bin" - // const path_components = pathModule.splitPOSIXPath(req.queryParams.file); // - // Path components will be - // ["usr", "local", "bin"] - const path_components = pathModule.splitPOSIXPath(req.queryParams.file); - const data_key = path_components.at(-1); - let record = new Record(data_key); - - // Special case - allow unknown client to read a publicly accessible record - // if record exists and if it is a public record + // "_key" : "bob", + // "_id" : "u/bob", + // "name" : "bob junior ", + // "name_first" : "bob", + // "name_last" : "jones", + // "is_admin" : true, + // "max_coll" : 50, + // "max_proj" : 10, + // "max_sav_qry" : 20, + // : + // "email" : "bobjones@gmail.com" + const client = g_lib.getUserFromClientID_noexcept(req.queryParams.client); if (!client) { - if (record.exists()) { - if (req.queryParams.act != "read" || !g_lib.hasPublicRead(record.id())) { - console.log( - "AUTHZ act: " + - req.queryParams.act + - " client: " + - client._id + - " path " + - req.queryParams.file + - " FAILED", - ); - throw g_lib.ERR_PERM_DENIED; - } - } - } else { - // Actions: read, write, create, delete, chdir, lookup - let req_perm = 0; - switch (req.queryParams.act) { - case "read": - req_perm = g_lib.PERM_RD_DATA; - break; - case "write": - break; - case "create": - req_perm = g_lib.PERM_WR_DATA; - break; - case "delete": - throw g_lib.ERR_PERM_DENIED; - case "chdir": - break; - case "lookup": - // For TESTING, allow these actions - return; - default: - throw [ - g_lib.ERR_INVALID_PARAM, - "Invalid gridFTP action: ", - req.queryParams.act, - ]; - } - - // This will tell us if the action on the record is authorized - // we still do not know if the path is correct. - if (record.exists()) { - if (!authzModule.isRecordActionAuthorized(client, data_key, req_perm)) { - console.log( - "AUTHZ act: " + - req.queryParams.act + - " client: " + - client._id + - " path " + - req.queryParams.file + - " FAILED", - ); - throw g_lib.ERR_PERM_DENIED; - } - } + console.log( + "AUTHZ act: " + + req.queryParams.act + + " client: " + + +req.queryParams.client + + " path " + + req.queryParams.file + + " FAILED", + ); + throw [g_lib.ERR_PERM_DENIED, "Unknown client: " + req.queryParams.client]; } + let repo = new Repo(req.queryParams.repo); + let path_type = repo.pathType(req.queryParams.file); - if (record.exists()) { - if (!record.isPathConsistent(req.queryParams.file)) { - console.log( - "AUTHZ act: " + - req.queryParams.act + - " client: " + - client._id + - " path " + - req.queryParams.file + - " FAILED", - ); - throw [record.error(), record.errorMessage()]; - } - } else { - // If the record does not exist then the path would noe be consistent. + // If the provided path is not within the repo throw an error + if (path_type === PathType.UNKNOWN) { console.log( "AUTHZ act: " + req.queryParams.act + @@ -138,7 +64,22 @@ router req.queryParams.file + " FAILED", ); - throw [g_lib.ERR_PERM_DENIED, "Invalid record specified: " + req.queryParams.file]; + throw [ + g_lib.ERR_PERM_DENIED, + "Unknown path, or path is not consistent with supported repository folder hierarchy: " + + req.queryParams.file, + ]; + } + + // Determine permissions associated with path provided + // Actions: read, write, create, delete, chdir, lookup + if (Object.keys(authzModule.authz_strategy).includes(req.queryParams.act)) { + authzModule.authz_strategy[req.queryParams.act][path_type]( + client, + req.queryParams.file, + ); + } else { + throw [g_lib.ERR_INVALID_PARAM, "Invalid gridFTP action: ", req.queryParams.act]; } console.log( "AUTHZ act: " + diff --git a/core/database/foxx/api/posix_path.js b/core/database/foxx/api/posix_path.js index d56a76932..f171b1d8b 100644 --- a/core/database/foxx/api/posix_path.js +++ b/core/database/foxx/api/posix_path.js @@ -26,8 +26,15 @@ module.exports = (function () { * ['usr', 'local', 'bin', 'node'] */ obj.splitPOSIXPath = function (a_posix_path) { - if (!a_posix_path || typeof a_posix_path !== "string") { + if (!a_posix_path) { throw new Error("Invalid POSIX path"); + } else if (typeof a_posix_path !== "string") { + throw new Error( + "Invalid POSIX path type: ", + typeof a_posix_path, + " path content: ", + a_posix_path, + ); } // Split the path into components // components: ['', 'usr', 'local', 'bin', 'node'] diff --git a/core/database/foxx/api/record.js b/core/database/foxx/api/record.js index b49603d40..36c330672 100644 --- a/core/database/foxx/api/record.js +++ b/core/database/foxx/api/record.js @@ -20,6 +20,8 @@ class Record { #loc = null; // Allocation object, determines what allocation data item is associated with #alloc = null; + // Determines what repo the data item is associated with + #repo = null; // The data key #key = null; // The data id simply the key prepended with 'd/' @@ -63,11 +65,25 @@ class Record { /** * Generates the full path to the record as it should appear in the repository. * + * @param {object} loc - The location object which specifies the owner of the record. * @param {string} basePath - The base path where the record is stored. - * @returns {string} - The full path to the record. + * + * @returns {string} - the path to the record or null if error */ - _pathToRecord(basePath) { - return basePath.endsWith("/") ? basePath + this.#key : basePath + "/" + this.#key; + _pathToRecord(loc, basePath) { + const path = basePath.endsWith("/") ? basePath : basePath + "/"; + if (loc.uid.charAt(0) == "u") { + return path + "user/" + loc.uid.substr(2) + "/" + this.#key; + } else if (loc.uid.charAt(0) == "p") { + return path + "project/" + loc.uid.substr(2) + "/" + this.#key; + } else { + this.#error = g_lib.ERR_INTERNAL_FAULT; + this.#err_msg = "Provided path does not fit within supported directory "; + this.#err_msg += "structure for repository, no user or project folder has"; + this.#err_msg += " been determined for the record."; + console.log(e); + return null; + } } /** @@ -78,6 +94,9 @@ class Record { * @returns {boolean} - true if paths are equal false otherwise **/ _comparePaths(storedPath, inputPath) { + if (storedPath === null) { + return false; + } if (storedPath !== inputPath) { this.#error = g_lib.ERR_PERM_DENIED; this.#err_msg = @@ -109,7 +128,7 @@ class Record { /** * Will return error code of last run method. * - * @returns {integer} - error code + * @returns {number} - error code **/ error() { return this.#error; @@ -167,13 +186,8 @@ class Record { return false; } - // If path is missing the starting "/" add it back in - if (!a_path.startsWith("/") && this.#alloc.path.startsWith("/")) { - a_path = "/" + a_path; - } - // If there is a new repo we need to check the path there and use that - if (this.#loc.new_repo) { + if (this.#loc.hasOwnProperty("new_repo") && this.#loc.new_repo) { // Below we get the allocation associated with data item by // 1. Checking if the data item is in flight, is in the process // of being moved to a new location or new owner and using that @@ -185,8 +199,8 @@ class Record { _to: this.#loc.new_repo, }); - // If no allocation is found for the item thrown an error - // if the paths do not align also thrown an error. + // If no allocation is found for the item throw an error + // if the paths do not align also throw an error. if (!new_alloc) { this.#error = g_lib.ERR_PERM_DENIED; this.#err_msg = @@ -194,13 +208,35 @@ class Record { return false; } - let stored_path = this._pathToRecord(new_alloc.path); + this.#repo = g_db._document(this.#loc.new_repo); + + if (!this.#repo) { + this.#error = g_lib.ERR_INTERNAL_FAULT; + this.#err_msg = + "Unable to find repo that record is meant to be allocated too, '" + + this.#loc.new_repo + + "' record '" + + this.#data_id; + return false; + } + + // If path is missing the starting "/" add it back in + if (!a_path.startsWith("/") && this.#repo.path.startsWith("/")) { + a_path = "/" + a_path; + } + + let stored_path = this._pathToRecord(this.#loc, this.#repo.path); if (!this._comparePaths(stored_path, a_path)) { return false; } } else { - let stored_path = this._pathToRecord(this.#alloc.path); + this.#repo = g_db._document(this.#loc._to); + + if (!a_path.startsWith("/") && this.#repo.path.startsWith("/")) { + a_path = "/" + a_path; + } + let stored_path = this._pathToRecord(this.#loc, this.#repo.path); // If there is no new repo check that the paths align if (!this._comparePaths(stored_path, a_path)) { diff --git a/core/database/foxx/api/repo.js b/core/database/foxx/api/repo.js new file mode 100644 index 000000000..74f47b8a6 --- /dev/null +++ b/core/database/foxx/api/repo.js @@ -0,0 +1,189 @@ +"use strict"; + +const g_db = require("@arangodb").db; +const g_lib = require("./support"); +const { errors } = require("@arangodb"); +const pathModule = require("./posix_path"); + +/** + * All DataFed repositories have the following path structure on a POSIX file system + * + * E.g. + * /mnt/science/datafed/project/foo/904u42 + * /mnt/science/datafed/user/bob/352632 + * + * In these cases + * + * PROJECT_PATH = /mnt/science/datafed/project/foo + * USER_PATH = /mnt/science/datafed/user/bob + * + * USER_RECORD_PATH = /mnt/science/datafed/user/bob/352632 + * and + * PROJECT_RECORD_PATH = /mnt/science/datafed/project/foo/904u42 + * + * REPO_BASE_PATH = /mnt/science + * REPO_ROOT_PATH = /mnt/science/datafed + * REPO_PATH = /mnt/science/datafed/project + * REPO_PATH = /mnt/science/datafed/user + **/ +const PathType = { + USER_PATH: "USER_PATH", + USER_RECORD_PATH: "USER_RECORD_PATH", + PROJECT_PATH: "PROJECT_PATH", + PROJECT_RECORD_PATH: "PROJECT_RECORD_PATH", + REPO_BASE_PATH: "REPO_BASE_PATH", + REPO_ROOT_PATH: "REPO_ROOT_PATH", + REPO_PATH: "REPO_PATH", + UNKNOWN: "UNKNOWN", +}; + +class Repo { + // ERROR code + #error = null; + // Error message should be a string if defined + #err_msg = null; + // Boolean value that determines if the repo exists in the database + #exists = false; + // The repo id simply the key prepended with 'repo/' + #repo_id = null; + #repo_key = null; + + /** + * Constructs a Repo object and checks if the key exists in the database. + * + * @param {string} a_key or id - The unique identifier for the repo, of repo key. + * e.g. can be either + * repo/repo_name + * or + * repo_name + */ + constructor(a_key) { + // Define the collection + const collection = g_db._collection("repo"); + + // This function is designed to check if the provided key exists in the + // database as a record. Searches are only made in the 'd' collection + // + // Will return true if it does and false if it does not. + if (a_key && a_key !== "repo/") { + if (a_key.startsWith("repo/")) { + this.#repo_id = a_key; + this.#repo_key = a_key.slice("repo/".length); + } else { + this.#repo_id = "repo/" + a_key; + this.#repo_key = a_key; + } + + // Check if the repo document exists + try { + if (collection.exists(this.#repo_key)) { + this.#exists = true; + } else { + this.#exists = false; + this.#error = g_lib.ERR_NOT_FOUND; + this.#err_msg = "Invalid repo: (" + a_key + "). No record found."; + } + } catch (e) { + this.#exists = false; + this.#error = g_lib.ERR_INTERNAL_FAULT; + this.#err_msg = "Unknown error encountered."; + console.log(e); + } + } + } + + /** + * Checks if the repo exists in the database. + * + * @returns {boolean} True if the repo exists, otherwise false. + */ + exists() { + return this.#exists; + } + + key() { + return this.#repo_key; + } + + id() { + return this.#repo_id; + } + + /** + * Will return error code of last run method. + * @returns {number} - If no error code, will return null + **/ + error() { + return this.#error; + } + + /** + * Retrieves the error code of the last run method. + * + * @returns {string|null} Error code or null if no error. + */ + errorMessage() { + return this.#err_msg; + } + + /** + * Detect what kind of POSIX path has been provided + * + * @param {string} a_path - the POSIX path that is supposed to exist on the repo + * @returns {string} - posix path type + **/ + pathType(a_path) { + // Ensure the repo exists + if (!this.#exists) { + throw [g_lib.ERR_PERM_DENIED, "Repo does not exist " + this.#repo_id]; + } + + let repo = g_db._document(this.#repo_id); + if (!repo.path) { + throw [g_lib.ERR_INTERNAL_FAULT, "Repo document is missing path: " + this.#repo_id]; + } + + // Get and sanitize the repo root path + let repo_root_path = repo.path.replace(/\/$/, ""); + let sanitized_path = a_path.replace(/\/$/, ""); + + // Check if the sanitized path is exactly the repo root path + if (sanitized_path === repo_root_path) { + return PathType.REPO_ROOT_PATH; + } + + // Check if the sanitized path is a valid base path + if ( + sanitized_path.length < repo_root_path.length && + repo_root_path.startsWith(sanitized_path + "/") + ) { + return PathType.REPO_BASE_PATH; + } + + // Ensure the sanitized path starts with the repo root path + if (!sanitized_path.startsWith(repo_root_path + "/")) { + return PathType.UNKNOWN; + } + + // Get the relative path and its components + const relative_path = sanitized_path.substr(repo_root_path.length); + const relative_path_components = pathModule.splitPOSIXPath(relative_path); + + // Map the first component to its corresponding PathType + const pathMapping = { + project: [PathType.REPO_PATH, PathType.PROJECT_PATH, PathType.PROJECT_RECORD_PATH], + user: [PathType.REPO_PATH, PathType.USER_PATH, PathType.USER_RECORD_PATH], + }; + + const firstComponent = relative_path_components[0]; + if (pathMapping[firstComponent]) { + return ( + pathMapping[firstComponent][relative_path_components.length - 1] || PathType.UNKNOWN + ); + } + + return PathType.UNKNOWN; + } +} + +module.exports = { Repo, PathType }; diff --git a/core/database/foxx/tests/authz.test.js b/core/database/foxx/tests/authz.test.js index a6a8bef35..d9976a2c9 100644 --- a/core/database/foxx/tests/authz.test.js +++ b/core/database/foxx/tests/authz.test.js @@ -1,8 +1,7 @@ "use strict"; - const chai = require("chai"); const expect = chai.expect; -const authzModule = require("../api/authz"); // Replace with the actual file name +const authzModule = require("../api/authz"); const g_db = require("@arangodb").db; const g_lib = require("../api/support"); const arangodb = require("@arangodb"); @@ -14,40 +13,349 @@ describe("Authz functions", () => { g_db.loc.truncate(); g_db.repo.truncate(); g_db.u.truncate(); + g_db.owner.truncate(); + g_db.p.truncate(); + g_db.admin.truncate(); }); - it("unit_authz: if admin should return true", () => { - let data_key = "big_data_obj"; - let data_id = "d/" + data_key; + describe("unit_authz: if a user is an admin 'bob' they should have authorization on not_bob's record", () => { + it("unit_authz: should return true", () => { + let data_key = "big_data_obj"; + let data_id = "d/" + data_key; + + g_db.d.save({ + _key: data_key, + _id: data_id, + creator: "u/george", + }); + + let owner_id = "u/not_bob"; + g_db.u.save({ + _key: "not_bob", + _id: owner_id, + is_admin: false, + }); + + let client = { + _key: "bob", + _id: "u/bob", + is_admin: true, + }; + + g_db.u.save(client); - g_db.d.save({ - _key: data_key, - _id: data_id, - creator: "george", + g_db.owner.save({ + _from: data_id, + _to: owner_id, + }); + + let req_perm = g_lib.PERM_CREATE; + + expect(authzModule.isRecordActionAuthorized(client, data_key, req_perm)).to.be.true; }); + }); + + describe("unit_authz: if a record is part of George's user allocation a non owning regular user 'bob' should not have access to the record", () => { + it("unit_authz: should thrown an error if bob tries to run a create request on the record.", () => { + let data_key = "data_key"; + let data_id = "d/" + data_key; - let owner_id = "u/not_bob"; - g_db.u.save({ - _key: "not_bob", - _id: owner_id, - is_admin: false, + g_db.d.save({ + _key: data_key, + _id: data_id, + creator: "u/george", + }); + + let client = { + _key: "bob", + _id: "u/bob", + is_admin: false, + }; + + g_db.u.save(client); + + let req_perm = g_lib.PERM_CREATE; + + expect(() => + authzModule.isRecordActionAuthorized(client, data_key, req_perm), + ).to.throw(); }); + }); + + describe("unit_authz: a user 'george' who is an owner of a record should have authorization on the record.", () => { + it("unit_authz: should return true.", () => { + let data_key = "data_key"; + let data_id = "d/" + data_key; + + g_db.d.save({ + _key: data_key, + _id: data_id, + creator: "u/george", + }); + + let client = { + _key: "george", + _id: "u/george", + is_admin: false, + }; - let client = { - _key: "bob", - _id: "u/bob", - is_admin: true, - }; + g_db.u.save(client); - g_db.u.save(client); + g_db.owner.save({ + _from: data_id, + _to: "u/george", + }); - g_db.owner.save({ - _from: data_id, - _to: owner_id, + let req_perm = g_lib.PERM_CREATE; + + expect(authzModule.isRecordActionAuthorized(client, data_key, req_perm)).to.be.true; + }); + }); + + describe("unit_authz: a user 'bob' who is an admin of a project should have authorization on a record in the project.", () => { + it("unit_authz: should return true", () => { + let data_key = "project_data_obj"; + let data_id = "d/" + data_key; + + g_db.d.save({ + _key: data_key, + _id: data_id, + creator: "u/george", + }); + + let project_id = "p/project_1"; + g_db.p.save({ + _key: "project_1", + _id: project_id, + name: "Project One", + }); + + let bob_id = "u/bob"; + + let bob = { + _key: "bob", + _id: bob_id, + is_admin: false, + }; + + let george = { + _key: "george", + _id: "u/george", + is_admin: false, + }; + + g_db.u.save(bob); + g_db.u.save(george); + + g_db.owner.save({ + _from: data_id, + _to: project_id, + }); + + g_db.admin.save({ + _from: project_id, + _to: bob_id, + }); + let req_perm = g_lib.PERM_CREATE; + + expect(authzModule.isRecordActionAuthorized(bob, data_key, req_perm)).to.be.true; }); + }); + + describe("unit_authz: non-owner 'bob' should be denied access to another user's 'george' record.", () => { + it("unit_authz: should return false", () => { + let data_key = "bananas"; + let data_id = "d/" + data_key; + + g_db.d.save( + { + _key: data_key, + _id: data_id, + creator: "u/george", + }, + { waitForSync: true }, + ); - let req_perm = g_lib.PERM_CREATE; + let bob = { + _key: "bob", + _id: "u/bob", + is_admin: false, + }; - expect(authzModule.isRecordActionAuthorized(client, data_key, req_perm)).to.be.true; + let george = { + _key: "george", + _id: "u/george", + is_admin: false, + }; + + g_db.u.save(bob, { waitForSync: true }); + g_db.u.save(george, { waitForSync: true }); + + g_db.owner.save( + { + _from: data_id, + _to: "u/george", + }, + { waitForSync: true }, + ); + let req_perm = g_lib.PERM_CREATE; + + expect(authzModule.isRecordActionAuthorized(bob, data_key, req_perm)).to.be.false; + }); + }); + + describe("unit_authz: 'Jack' is a creator of the an 'apples' document in the 'fruity' project, 'Mandy' is the admin of the 'condiments' project, 'Mandy' should not have access to the 'apples' document.", () => { + it("unit_authz: should return false.", () => { + let data_key = "apples"; + let data_id = "d/" + data_key; + + g_db.d.save( + { + _key: data_key, + _id: data_id, + creator: "u/jack", + }, + { waitForSync: true }, + ); + + let jack = { + _key: "jack", + _id: "u/jack", + is_admin: false, + }; + + g_db.u.save(jack, { waitForSync: true }); + + let fruity_project_id = "p/fruity"; + g_db.p.save( + { + _key: "fruity", + _id: fruity_project_id, + name: "Project Fruity", + }, + { waitForSync: true }, + ); + + let condiments_project_id = "p/condiments"; + g_db.p.save( + { + _key: "condiments", + _id: condiments_project_id, + name: "Project Condiments", + }, + { waitForSync: true }, + ); + + let mandy_admin_id = "u/mandy"; + let mandy = { + _key: "mandy", + _id: mandy_admin_id, + is_admin: false, + }; + g_db.u.save(mandy, { waitForSync: true }); + + let amy_admin_id = "u/amy"; + g_db.u.save( + { + _key: "amy", + _id: amy_admin_id, + is_admin: false, + }, + { waitForSync: true }, + ); + + g_db.owner.save( + { + _from: data_id, + _to: fruity_project_id, + }, + { waitForSync: true }, + ); + + g_db.admin.save( + { + _from: fruity_project_id, + _to: amy_admin_id, + }, + { waitForSync: true }, + ); + + g_db.admin.save( + { + _from: condiments_project_id, + _to: mandy_admin_id, + }, + { waitForSync: true }, + ); + let req_perm = g_lib.PERM_CREATE; + + // Non-project admin should not have permission + expect(authzModule.isRecordActionAuthorized(mandy, data_key, req_perm)).to.be.false; + }); + }); + + describe("unit_authz: 'Tim' is the creator of a record 'cherry', but the 'cherry' record has been moved to a different project 'red_fruit' that 'Tim' does not have access to.", () => { + it("unit_authz: should return false.", () => { + let data_key = "cherry"; + let data_id = "d/" + data_key; + + g_db.d.save( + { + _key: data_key, + _id: data_id, + creator: "tim", + }, + { waitForSync: true }, + ); + + let tim = { + _key: "tim", + _id: "u/tim", + is_admin: false, + }; + + // A project is the owner + let project_id = "p/red_fruit"; + g_db.p.save( + { + _key: "red_fruit", + _id: project_id, + name: "Project Red Fruit", + }, + { waitForSync: true }, + ); + + let bob_id = "u/bob"; + + let bob = { + _key: "bob", + _id: bob_id, + is_admin: false, + }; + + g_db.u.save(bob, { waitForSync: true }); + + g_db.owner.save( + { + _from: data_id, + _to: project_id, + }, + { waitForSync: true }, + ); + + g_db.admin.save( + { + _from: project_id, + _to: bob_id, + }, + { waitForSync: true }, + ); + + g_db.u.save(tim, { waitForSync: true }); + + let req_perm = g_lib.PERM_READ; + + expect(authzModule.isRecordActionAuthorized(tim, data_key, req_perm)).to.be.false; + }); }); }); diff --git a/core/database/foxx/tests/authz_router.test.js b/core/database/foxx/tests/authz_router.test.js new file mode 100644 index 000000000..53ec97d26 --- /dev/null +++ b/core/database/foxx/tests/authz_router.test.js @@ -0,0 +1,338 @@ +"use strict"; + +// Integration test of API +const { expect } = require("chai"); +const request = require("@arangodb/request"); +const { baseUrl } = module.context; +const g_db = require("@arangodb").db; +const g_lib = require("../api/support"); + +// Constants used throughout test file +// The base URL for the authz foxx route +const authz_base_url = `${baseUrl}/authz`; +// Current time used for updating documents in the database +const current_time = Math.floor(Date.now() / 1000); +// Test user information +const james_uuid = "XXXXYYYY-XXXX-YYYY-XXXX-YYYYXXXXYYYY"; +const james_uuid_id = "uuid/" + james_uuid; +const james_key = "jamesw"; +const james_id = "u/" + james_key; +const base_user_data = { + _key: james_key, + _id: james_id, + name_first: "james", + name_last: "whiticker", + is_admin: true, + max_coll: g_lib.DEF_MAX_COLL, + max_proj: g_lib.DEF_MAX_PROJ, + max_sav_qry: g_lib.DEF_MAX_SAV_QRY, + ct: current_time, + ut: current_time, +}; + +// Test record information +const valid_key = "1120"; +const record_id = "d/" + valid_key; + +// Test repo information +const repo_key = "datafed-at-hot-potato"; +const repo_id = "repo/" + repo_key; +const repo_path = "/mnt/repo_name"; +const file_path = repo_path + "/user/" + james_key + "/" + valid_key; + +// Test Project information +const project_key = "physics"; +const project_id = "p/" + project_key; +const project_file_path = repo_path + "/project/" + project_key + "/" + valid_key; + +/** + * This method sets up the database with the documents for a user with a data record + * + * Creates a user, creates a repository and adds an allocation that connects the user + * to the repository. A record is also created and is connected to the repository + * via a location connection. + **/ +function defaultWorkingSetup() { + g_db.uuid.save({ + _id: james_uuid_id, + }); + + g_db.ident.save({ + _from: james_id, + _to: james_uuid_id, + }); + + const repo_data = { + _key: repo_key, + path: repo_path, + }; + // Create nodes + //recordRepoAndUserSetup(valid_key, base_user_data, repo_data); + + g_db.d.save({ + _key: valid_key, + owner: james_id, + }); + g_db.repo.save(repo_data, { waitForSync: true }); + + g_db.u.save(base_user_data); + + // Create edges + g_db.loc.save({ + _from: record_id, + _to: repo_id, + uid: james_id, + new_repo: null, + }); + + g_db.alloc.save({ + _from: james_id, + _to: repo_id, + }); +} + +/** + * This method sets up the database with the documents for a project owned data record + * + * The following items are needed for this. + * A user + * A record + * A project + * A group + * A collection + * A owner + * A repo + * + * These different documents are connected through edges. + * record <-> item <-> collection + * record <-> owner <-> project + * record <-> loc <-> repo + * uuid <-> ident <-> user + * group <-> acl <-> collection + * user <-> member <-> group + * project <-> alloc <-> repo + * project <-> owner <-> group + **/ +function defaultWorkingSetupProject() { + // Create nodes + g_db.uuid.save({ + _id: james_uuid_id, + }); + + g_db.u.save(base_user_data); + + g_db.p.save({ + _key: project_key, + }); + + const root = g_db.c.save( + { + _key: "p_" + project_key + "_root", + is_root: true, + owner: project_id, + acls: 2, + }, + { + returnNew: true, + }, + ); + + const mem_grp = g_db.g.save( + { + uid: "p/" + project_key, + gid: "members", + }, + { + returnNew: true, + }, + ); + + g_db.d.save({ + _key: valid_key, + owner: project_id, + }); + + const repo_data = { + _key: repo_key, + path: repo_path, + }; + g_db.repo.save(repo_data, { waitForSync: true }); + + // Create edges + g_db.item.save({ + _from: root._id, + _to: record_id, + }); + + g_db.acl.save({ + _from: root._id, + _to: mem_grp._id, + grant: g_lib.PERM_MEMBER, + inhgrant: g_lib.PERM_MEMBER, + }); + + g_db.loc.save({ + _from: record_id, + _to: repo_id, + uid: project_id, + new_repo: null, + }); + + g_db.alloc.save({ + _from: project_id, + _to: repo_id, + }); + + g_db.member.save({ + _from: mem_grp._id, + _to: james_id, + }); + + g_db.ident.save({ + _from: james_id, + _to: james_uuid_id, + }); + + g_db.owner.save({ + _from: record_id, + _to: project_id, + }); +} + +// NOTE: describe block strings are compared against test specification during test call, not file name +describe("unit_authz_router: the Foxx microservice authz_router", () => { + beforeEach(() => { + g_db.u.truncate(); + g_db.ident.truncate(); + g_db.uuid.truncate(); + g_db.acl.truncate(); + g_db.item.truncate(); + g_db.c.truncate(); + g_db.g.truncate(); + g_db.p.truncate(); + g_db.owner.truncate(); + g_db.member.truncate(); + g_db.d.truncate(); + g_db.alloc.truncate(); + g_db.loc.truncate(); + g_db.repo.truncate(); + }); + + it("unit_authz_router: gridftp create action with user record and valid file path.", () => { + defaultWorkingSetup(); + const request_string = + `${authz_base_url}/gridftp?client=` + + james_uuid + + `&repo=` + + encodeURIComponent(repo_id) + + `&file=` + + encodeURIComponent(file_path) + + `&act=create`; + + // act + const response = request.get(request_string); + + // assert + expect(response.status).to.equal(204); + }); + + it("unit_authz_router: gridftp create action with user record and invalid file path.", () => { + defaultWorkingSetup(); + + // This is invalid because bobby does not exist as a user and the record "valid_key" they are not the owner of or have a membership too. + const bad_file_path = repo_path + "/user/bobby/" + valid_key; + const request_string = + `${authz_base_url}/gridftp?client=` + + james_uuid + + `&repo=` + + encodeURIComponent(repo_id) + + `&file=` + + encodeURIComponent(bad_file_path) + + `&act=create`; + + // act + const response = request.get(request_string); + + // assert + expect(response.status).to.equal(400); + }); + + it("unit_authz_router: gridftp create action with invalid repo and valid file path.", () => { + defaultWorkingSetup(); + const bad_repo_id = "repo/not_exist"; + const request_string = + `${authz_base_url}/gridftp?client=` + + james_uuid + + `&repo=` + + encodeURIComponent(bad_repo_id) + + `&file=` + + encodeURIComponent(file_path) + + `&act=create`; + + // act + const response = request.get(request_string); + + // assert + expect(response.status).to.equal(400); + }); + + it("unit_authz_router: gridftp create action with invalid client and valid file path.", () => { + // Here we are creating a valid user but they simply do not have access to the provided file + // path. + const bad_user_data = { + _key: "george", + _id: "u/george", + name_first: "george", + name_last: "Brown", + is_admin: false, + }; + const george_uuid = "ZZZZYYYY-ZZZZ-ZZZZ-ZZZZ-ZZZZTTTTZZZZ"; + const george_uuid_id = "uuid/" + george_uuid; + g_db.u.save(bad_user_data); + + g_db.uuid.save({ + _id: george_uuid_id, + }); + + g_db.ident.save({ + _from: "u/george", + _to: george_uuid_id, + }); + + defaultWorkingSetup(); + const bad_repo_id = "repo/not_exist"; + const request_string = + `${authz_base_url}/gridftp?client=` + + george_uuid + + `&repo=` + + encodeURIComponent(repo_id) + + `&file=` + + encodeURIComponent(file_path) + + `&act=create`; + + // act + const response = request.get(request_string); + + // assert + expect(response.status).to.equal(400); + }); + + it("unit_authz_router: gridftp create action with valid repo and valid file path in project.", () => { + defaultWorkingSetupProject(); + const bad_repo_id = "repo/not_exist"; + const request_string = + `${authz_base_url}/gridftp?client=` + + james_uuid + + `&repo=` + + encodeURIComponent(repo_id) + + `&file=` + + encodeURIComponent(project_file_path) + + `&act=create`; + + // act + const response = request.get(request_string); + + // assert + expect(response.status).to.equal(204); + }); +}); diff --git a/core/database/foxx/tests/record.test.js b/core/database/foxx/tests/record.test.js index a272b259d..ef88ee9c8 100644 --- a/core/database/foxx/tests/record.test.js +++ b/core/database/foxx/tests/record.test.js @@ -7,19 +7,21 @@ const g_db = require("@arangodb").db; const g_lib = require("../api/support"); const arangodb = require("@arangodb"); -function recordRepoAndUserSetup(record_key, user_id, repo_id) { +function recordRepoAndUserSetup(record_key, user_id, repo_data) { const record_id = "d/" + record_key; - g_db.d.save({ - _key: record_key, - _id: record_id, - }); - g_db.repo.save({ - _id: repo_id, - }); + if (!g_db._exists(record_id)) { + g_db.d.save({ + _key: record_key, + _id: record_id, + }); + } + g_db.repo.save(repo_data); - g_db.u.save({ - _id: user_id, - }); + if (!g_db._exists(user_id)) { + g_db.u.save({ + _id: user_id, + }); + } } describe("Record Class", () => { @@ -43,8 +45,11 @@ describe("Record Class", () => { const key_id = "d/" + valid_key; const owner_id = "u/bob"; const repo_id = "repo/datafed-at-com"; + const repo_data = { + _key: "datafed-at-com", + }; // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + recordRepoAndUserSetup(valid_key, owner_id, repo_data); // Create edges g_db.loc.save({ @@ -69,7 +74,10 @@ describe("Record Class", () => { const owner_id = "u/jim"; const repo_id = "repo/datafed-at-org"; // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + const repo_data = { + _key: "datafed-at-org", + }; + recordRepoAndUserSetup(valid_key, owner_id, repo_data); const record = new Record(valid_key); expect(record.isManaged()).to.be.false; @@ -86,7 +94,10 @@ describe("Record Class", () => { const owner_id = "u/tom"; const repo_id = "repo/datafed-banana-com"; // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + const repo_data = { + _key: "datafed-banana-com", + }; + recordRepoAndUserSetup(valid_key, owner_id, repo_data); // Create edges g_db.loc.save({ @@ -108,7 +119,10 @@ describe("Record Class", () => { const owner_id = "u/drake"; const repo_id = "repo/datafed-best-com"; // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + const repo_data = { + _key: "datafed-best-com", + }; + recordRepoAndUserSetup(valid_key, owner_id, repo_data); // Create edges g_db.loc.save({ @@ -133,8 +147,13 @@ describe("Record Class", () => { const key_id = "d/" + valid_key; const owner_id = "u/carl"; const repo_id = "repo/datafed-at-super"; + const repo_data = { + _id: repo_id, + _key: "datafed-at-super", + path: "/correct/file/path", + }; // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + recordRepoAndUserSetup(valid_key, owner_id, repo_data); // Create edges g_db.loc.save({ @@ -142,6 +161,10 @@ describe("Record Class", () => { _to: repo_id, uid: owner_id, }); + g_db.alloc.save({ + _from: owner_id, + _to: repo_id, + }); const record = new Record(valid_key); expect(record.isPathConsistent("file/path/" + valid_key)).to.be.false; @@ -153,7 +176,11 @@ describe("Record Class", () => { const owner_id = "u/red"; const repo_id = "repo/datafed-fine-com"; // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + const repo_data = { + _key: "datafed-fine-com", + path: "/correct/file/path", + }; + recordRepoAndUserSetup(valid_key, owner_id, repo_data); // Create edges g_db.loc.save({ @@ -164,7 +191,6 @@ describe("Record Class", () => { g_db.alloc.save({ _from: owner_id, _to: repo_id, - path: "/correct/file/path", }); const record = new Record(valid_key); @@ -177,10 +203,15 @@ describe("Record Class", () => { it("unit_record: isPathConsistent should return true paths are consistent.", () => { const valid_key = "1126"; const key_id = "d/" + valid_key; - const owner_id = "u/karen"; + const owner_name = "karen"; + const owner_id = "u/" + owner_name; const repo_id = "repo/datafed-cool-com"; // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + const repo_data = { + _key: "datafed-cool-com", + path: "/correct/file/path", + }; + recordRepoAndUserSetup(valid_key, owner_id, repo_data); // Create edges g_db.loc.save({ @@ -195,12 +226,13 @@ describe("Record Class", () => { }); const record = new Record(valid_key); - expect(record.isPathConsistent("/correct/file/path/" + valid_key)).to.be.true; + expect(record.isPathConsistent("/correct/file/path/user/" + owner_name + "/" + valid_key)) + .to.be.true; expect(record.error()).to.be.null; expect(record.errorMessage()).to.be.null; }); - it("unit_record: isPathConsistent should return true paths are inconsistent, but new path in new alloc is valid.", () => { + it("unit_record: isPathConsistent should return true, paths are inconsistent, but new path in new repo is valid.", () => { const valid_key = "1127"; const key_id = "d/" + valid_key; const owner_id = "u/john"; @@ -208,7 +240,17 @@ describe("Record Class", () => { const new_repo_id = "repo/watermelon-at-org"; // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + + const repo_data = { + _key: "orange-at-org", + path: "/old/file/path", + }; + const repo_data_new = { + _key: "watermelon-at-org", + path: "/correct/file/path", + }; + recordRepoAndUserSetup(valid_key, owner_id, repo_data); + recordRepoAndUserSetup(valid_key, owner_id, repo_data_new); // Create edges g_db.loc.save({ @@ -220,28 +262,35 @@ describe("Record Class", () => { g_db.alloc.save({ _from: owner_id, _to: repo_id, - path: "/old/file/path", }); g_db.alloc.save({ _from: owner_id, _to: new_repo_id, - path: "/correct/file/path", }); const record = new Record(valid_key); - expect(record.isPathConsistent("/correct/file/path/" + valid_key)).to.be.true; + expect(record.isPathConsistent("/correct/file/path/user/john/" + valid_key)).to.be.true; expect(record.error()).to.be.null; expect(record.errorMessage()).to.be.null; }); - it("unit_record: isPathConsistent should return false paths are inconsistent in new and old alloc.", () => { + it("unit_record: isPathConsistent should return false paths are inconsistent in new and old repo.", () => { const valid_key = "1128"; const key_id = "d/" + valid_key; const owner_id = "u/sherry"; const repo_id = "repo/passionfruit"; const new_repo_id = "repo/hamburger"; // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + const repo_data = { + _key: "passionfruit", + path: "/old/file/path", + }; + const repo_data_new = { + _key: "hamburger", + path: "/new/file/path", + }; + recordRepoAndUserSetup(valid_key, owner_id, repo_data); + recordRepoAndUserSetup(valid_key, owner_id, repo_data_new); // Create edges g_db.loc.save({ @@ -253,16 +302,15 @@ describe("Record Class", () => { g_db.alloc.save({ _from: owner_id, _to: repo_id, - path: "/old/file/path", }); g_db.alloc.save({ _from: owner_id, _to: new_repo_id, - path: "/incorrect/file/path", }); const record = new Record(valid_key); - expect(record.isPathConsistent("/correct/file/path/" + valid_key)).to.be.false; + expect(record.isPathConsistent("/incorrect/file/path/user/sherry/" + valid_key)).to.be + .false; expect(record.error()).to.equal(g_lib.ERR_PERM_DENIED); const pattern = /^Record path is not consistent/; expect(record.errorMessage()).to.match(pattern); diff --git a/core/database/foxx/tests/repo.test.js b/core/database/foxx/tests/repo.test.js new file mode 100644 index 000000000..c136e874d --- /dev/null +++ b/core/database/foxx/tests/repo.test.js @@ -0,0 +1,224 @@ +"use strict"; + +const chai = require("chai"); +const { expect } = chai; +const { Repo, PathType } = require("../api/repo"); +const g_db = require("@arangodb").db; +const g_lib = require("../api/support"); +const arangodb = require("@arangodb"); + +describe("Testing Repo class", () => { + beforeEach(() => { + g_db.d.truncate(); + g_db.alloc.truncate(); + g_db.loc.truncate(); + g_db.repo.truncate(); + }); + + it("unit_repo: should throw an error if the repo does not exist", () => { + const repo = new Repo("invalidKey"); + expect(repo.exists()).to.be.false; + expect(repo.key()).to.equal("invalidKey"); + expect(repo.error()).to.equal(g_lib.ERR_NOT_FOUND); + }); + + it("unit_repo: should return REPO_ROOT_PATH for exact match with repo root", () => { + const path = "/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType(path)).to.equal(PathType.REPO_ROOT_PATH); + }); + + it("unit_repo: should return UNKNOWN for invalid path not matching repo root", () => { + const path = "/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/invalid_path")).to.equal(PathType.UNKNOWN); + }); + + it("unit_repo: should return PROJECT_PATH for valid project paths", () => { + const path = "/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/repo_root/project/bam")).to.equal(PathType.PROJECT_PATH); + }); + + it("unit_repo: should return USER_PATH for valid user paths", () => { + const path = "/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/repo_root/user/george")).to.equal(PathType.USER_PATH); + }); + + it("unit_repo: should return UNKNOWN for a path that does not start with repo root", () => { + const path = "/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/randome_string/user/george/id")).to.equal(PathType.UNKNOWN); + }); + + it("unit_repo: should trim trailing slashes from repo root path and input path", () => { + const path = "/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/repo_root/user/")).to.equal(PathType.REPO_PATH); + }); + + it("unit_repo: should handle an empty relative path correctly", () => { + const path = "/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/repo_root/")).to.equal(PathType.REPO_ROOT_PATH); + }); + + it("unit_repo: should handle an unknown path that begins with project", () => { + const path = "/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/random_string/project_bam")).to.equal(PathType.UNKNOWN); + }); + + it("unit_repo: should handle an repo base path", () => { + const path = "/mnt/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/mnt")).to.equal(PathType.REPO_BASE_PATH); + }); + + it("unit_repo: should handle an repo base path with ending /", () => { + const path = "/mnt/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/mnt/")).to.equal(PathType.REPO_BASE_PATH); + }); + + it("unit_repo: should handle an repo base path containing only /", () => { + const path = "/mnt/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/")).to.equal(PathType.REPO_BASE_PATH); + }); + + it("unit_repo: should handle an repo base path and repo root path are the same and only containing only /", () => { + const path = "/"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/")).to.equal(PathType.REPO_ROOT_PATH); + }); + + it("unit_repo: should handle an repo base path containing only part of base.", () => { + const path = "/mnt/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/m")).to.equal(PathType.UNKNOWN); + }); + + it("unit_repo: should handle an repo root path containing only part of root.", () => { + const path = "/mnt/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/mnt/re")).to.equal(PathType.UNKNOWN); + }); + + it("unit_repo: should handle an repo path containing only part of project.", () => { + const path = "/mnt/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/mnt/repo_root/pro")).to.equal(PathType.UNKNOWN); + }); + + it("unit_repo: should handle an repo path containing only part of user.", () => { + const path = "/mnt/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/mnt/repo_root/us")).to.equal(PathType.UNKNOWN); + }); + + it("unit_repo: should handle a project record path.", () => { + const path = "/mnt/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/mnt/repo_root/project/bam/4243")).to.equal( + PathType.PROJECT_RECORD_PATH, + ); + }); + + it("unit_repo: should handle a user record path.", () => { + const path = "/mnt/repo_root"; + g_db.repo.save({ + _id: "repo/foo", + _key: "foo", + path: path, + }); + const repo = new Repo("foo"); + expect(repo.pathType("/mnt/repo_root/user/jane/4243")).to.equal(PathType.USER_RECORD_PATH); + }); +}); diff --git a/core/database/foxx/tests/user_router.test.js b/core/database/foxx/tests/user_router.test.js index b509c22ee..db8acdb03 100644 --- a/core/database/foxx/tests/user_router.test.js +++ b/core/database/foxx/tests/user_router.test.js @@ -13,7 +13,7 @@ const { db } = require("@arangodb"); const usr_base_url = `${baseUrl}/usr`; // NOTE: describe block strings are compared against test specification during test call, not file name -describe("user_router: the Foxx microservice user_router token/set endpoint", () => { +describe("unit_user_router: the Foxx microservice user_router token/set endpoint", () => { const test_param_indexes = [0, 1, 2, 3, 4]; const test_params = test_param_indexes.map((param_index) => { return { diff --git a/core/database/tests/test_foxx.sh b/core/database/tests/test_foxx.sh index b818ce9cf..35d52b72d 100755 --- a/core/database/tests/test_foxx.sh +++ b/core/database/tests/test_foxx.sh @@ -128,7 +128,6 @@ if ! command -v foxx > /dev/null 2>&1; then fi PATH_TO_PASSWD_FILE=${SOURCE}/database_temp.password -echo "$local_DATAFED_DATABASE_PASSWORD" > "${PATH_TO_PASSWD_FILE}" if [ "$TEST_TO_RUN" == "all" ] then # WARNING Foxx and arangosh arguments differ --server is used for Foxx not --server.endpoint @@ -138,6 +137,7 @@ then --database "${local_DATABASE_NAME}" \ "/api/${local_FOXX_MAJOR_API_VERSION}" --reporter spec else + echo "Test: $TEST_TO_RUN" # WARNING Foxx and arangosh arguments differ --server is used for Foxx not --server.endpoint "${FOXX_PREFIX}foxx" test -u "${local_DATABASE_USER}" \ --server "tcp://${DATAFED_DATABASE_HOST}:8529" \