From 6f5a0fea1aee3f1f918317a50f45287c5df1a2f9 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 11 Dec 2024 16:33:27 -0500 Subject: [PATCH 01/38] Add posix path js helper file --- core/database/foxx/api/posix_path.js | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 core/database/foxx/api/posix_path.js diff --git a/core/database/foxx/api/posix_path.js b/core/database/foxx/api/posix_path.js new file mode 100644 index 000000000..2a0fbf7bc --- /dev/null +++ b/core/database/foxx/api/posix_path.js @@ -0,0 +1,31 @@ +'use strict'; + +const path = require('path'); + +module.exports = (function() { + var obj = {} + + /** + * \brief will split a path string into components + * + * Example POSIX path + * const posixPath = '/usr/local/bin/node'; + * + * output: ['usr', 'local', 'bin', 'node'] + * + **/ + obj.splitPOSIXPath = function(a_posix_path) { + + // Split the path into components + // components: ['', 'usr', 'local', 'bin', 'node'] + // The empty '' is for root + const components = posixPath.split(path.posix.sep); + + // components: ['usr', 'local', 'bin', 'node'] + const cleanComponents = components.filter(component => component !== ''); + + + return components; + } + +}); From f920a7154667ea1cb8d83ed41ce7b7c8373acda3 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 11 Dec 2024 16:39:11 -0500 Subject: [PATCH 02/38] Add unit tests for poxix path separator --- core/database/foxx/tests/path.test.js | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 core/database/foxx/tests/path.test.js diff --git a/core/database/foxx/tests/path.test.js b/core/database/foxx/tests/path.test.js new file mode 100644 index 000000000..eb8f8e55d --- /dev/null +++ b/core/database/foxx/tests/path.test.js @@ -0,0 +1,37 @@ +"use strict" + +const chai = require('chai'); +const expect = chai.expect; +const pathModule = require('./pathModule'); // Replace with the actual file name + +describe('splitPOSIXPath', function () { + it('should split a simple POSIX path into components', function () { + const result = pathModule.splitPOSIXPath('/usr/local/bin/node'); + expect(result).to.deep.equal(['usr', 'local', 'bin', 'node']); + }); + + it('should handle root path and return an empty array', function () { + const result = pathModule.splitPOSIXPath('/'); + expect(result).to.deep.equal([]); + }); + + it('should handle paths with trailing slashes correctly', function () { + const result = pathModule.splitPOSIXPath('/usr/local/bin/'); + expect(result).to.deep.equal(['usr', 'local', 'bin']); + }); + + it('should handle empty paths and throw an error', function () { + expect(() => pathModule.splitPOSIXPath('')).to.throw('Invalid POSIX path'); + }); + + it('should handle null or undefined paths and throw an error', function () { + expect(() => pathModule.splitPOSIXPath(null)).to.throw('Invalid POSIX path'); + expect(() => pathModule.splitPOSIXPath(undefined)).to.throw('Invalid POSIX path'); + }); + + it('should handle non-string inputs and throw an error', function () { + expect(() => pathModule.splitPOSIXPath(123)).to.throw('Invalid POSIX path'); + expect(() => pathModule.splitPOSIXPath({})).to.throw('Invalid POSIX path'); + expect(() => pathModule.splitPOSIXPath([])).to.throw('Invalid POSIX path'); + }); +}); From 2c1f2cd4e9fe1049c83a1d3ea9edbf9ca72b112d Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Fri, 13 Dec 2024 14:30:16 -0500 Subject: [PATCH 03/38] First commit refactoring authz route --- core/database/foxx/api/authz.js | 26 ++++ core/database/foxx/api/authz_router.js | 127 +++++++++----------- core/database/foxx/api/posix_path.js | 15 +-- core/database/foxx/api/record.js | 151 ++++++++++++++++++++++++ core/database/foxx/tests/path.test.js | 2 +- core/database/foxx/tests/record.test.js | 45 +++++++ 6 files changed, 284 insertions(+), 82 deletions(-) create mode 100644 core/database/foxx/api/authz.js create mode 100644 core/database/foxx/api/record.js create mode 100644 core/database/foxx/tests/record.test.js diff --git a/core/database/foxx/api/authz.js b/core/database/foxx/api/authz.js new file mode 100644 index 000000000..789f1f222 --- /dev/null +++ b/core/database/foxx/api/authz.js @@ -0,0 +1,26 @@ +'use strict'; + +const g_db = require('@arangodb').db; +const path = require('path'); +const g_lib = require('./support'); + +module.exports = (function() { + var obj = {} + + + obj.isRecordActionAuthorized = function(a_client, a_data_key, a_perm) { + const data_id = "d/" + a_data_key; + // If the user is not an admin of the object we will need + // to check if the user has the write authorization + if (g_lib.hasAdminPermObject(a_client, data_id)) { + return true; + } + var data = g_db.d.document(data_id); + // Grab the data item + if (g_lib.hasPermissions(a_client, data, a_perm)) { + return true; + } + return false; + }; + return obj; +})(); diff --git a/core/database/foxx/api/authz_router.js b/core/database/foxx/api/authz_router.js index 3988703e5..1575b4d92 100644 --- a/core/database/foxx/api/authz_router.js +++ b/core/database/foxx/api/authz_router.js @@ -5,6 +5,9 @@ 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 dataModule = require('./data'); // Replace with the actual file name +const authzModule = require('./authz'); // Replace with the actual file name module.exports = router; @@ -13,121 +16,97 @@ router.get('/gridftp', function(req, res) { try { console.log("/gridftp start authz client", req.queryParams.client, "repo", req.queryParams.repo, "file", req.queryParams.file, "act", req.queryParams.act); + // 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); - var idx = req.queryParams.file.lastIndexOf("/"); - var data_key = req.queryParams.file.substr(idx + 1); - var data_id = "d/" + data_key; + const path_components = pathModule.splitPOSIXPath(req.queryParams.file); + const data_key = path_components.at(-1); + var record = Record(data_key); + // 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"] // Special case - allow unknown client to read a publicly accessible record + // if record exists and if it is a public record if (!client) { - if (req.queryParams.act != "read" || !g_lib.hasPublicRead(data_id)) { - console.log("Permission to read denied!"); - throw g_lib.ERR_PERM_DENIED; + if( record.exists() ) { + if (req.queryParams.act != "read" || !g_lib.hasPublicRead(data_id)) { + console.log("AUTHZ act: " + req.queryParams.act + " client: " + client._id + " path " + req.queryParams.file + " FAILED"); + throw g_lib.ERR_PERM_DENIED; + } } - console.log("allow anon read of public record"); } else { - console.log("client:", client); // Actions: read, write, create, delete, chdir, lookup var req_perm = 0; switch (req.queryParams.act) { case "read": - console.log("Client: ", client, " read permissions?"); req_perm = g_lib.PERM_RD_DATA; break; case "write": - console.log("Client: ", client, " write permissions?"); break; case "create": - console.log("Client: ", client, " create permissions?"); req_perm = g_lib.PERM_WR_DATA; break; case "delete": - console.log("Client: ", client, " delete permissions?"); throw g_lib.ERR_PERM_DENIED; case "chdir": - console.log("Client: ", client, " chdir permissions?"); break; case "lookup": - console.log("Client: ", client, " lookup permissions?"); // For TESTING, allow these actions return; default: throw [g_lib.ERR_INVALID_PARAM, "Invalid gridFTP action: ", req.queryParams.act]; } - console.log("client: ", client, " data_id: ", data_id); - if (!g_lib.hasAdminPermObject(client, data_id)) { - var data = g_db.d.document(data_id); - if (!g_lib.hasPermissions(client, data, req_perm)) { - console.log("Client: ", client, " does not have permission!"); - throw g_lib.ERR_PERM_DENIED; - } - } - } - - // Verify repo and path are correct for record - // Note: only managed records have an allocations and this gridftp auth call is only made for managed records - //var path = req.queryParams.file.substr( req.queryParams.file.indexOf("/",8)); - var path = req.queryParams.file; - console.log("data_id is, ", data_id); - var loc = g_db.loc.firstExample({ - _from: data_id - }); - console.log("Loc is:") - console.log(loc) - if (!loc) { - console.log("Permission denied data is not managed by DataFed. This can happen if you try to do a transfer directly from Globus.") - throw g_lib.ERR_PERM_DENIED; - } - var alloc = g_db.alloc.firstExample({ - _from: loc.uid, - _to: loc._to - }); - console.log("path:", path, " alloc path:", alloc.path + data_key, " loc: ", loc); - if (!alloc) { - throw g_lib.ERR_PERM_DENIED; - } - // If path is missing the starting "/" add it back in - if (!path.startsWith("/") && alloc.path.startsWith("/") ) { - path = "/" + path; + // 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("path:", path, " alloc path:", alloc.path + data_key, " loc: ", loc); - if (alloc.path + data_key != path) { - // This may be due to an alloc/owner change - // Allow If new path matches - console.log("authz loc info:", loc); - - if (!loc.new_repo) { - console.log("Throw a permission denied error"); - throw g_lib.ERR_PERM_DENIED; - } - - console.log("Creating alloc"); - alloc = g_db.alloc.firstExample({ - _from: loc.new_owner ? loc.new_owner : loc.uid, - _to: loc.new_repo - }); - - - console.log("alloc is "); - console.log(alloc); - if (!alloc || (alloc.path + data_key != path)) { - throw [obj.ERR_PERM_DENIED, "Permission denied, DataFed registered path is '" + alloc.path + data_key + "' Globus path is '" + path + "'"] - } + 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. + console.log("AUTHZ act: " + req.queryParams.act + " client: " + client._id + " path " + req.queryParams.file + " FAILED"); + throw [g_lib.ERR_PERM_DENIED, "Invalid record specified: " + req.queryParams.file]; } + console.log("AUTHZ act: " + req.queryParams.act + " client: " + client._id + " path " + req.queryParams.file + " SUCCESS"); } catch (e) { g_lib.handleException(e, res); } }) .queryParam('client', joi.string().required(), "Client ID") - .queryParam('repo', joi.string().required(), "Originating repo ID") + .queryParam('repo', joi.string().required(), "Originating repo ID, where the DataFed managed GridFTP server is running.") .queryParam('file', joi.string().required(), "Data file name") - .queryParam('act', joi.string().required(), "Action") + .queryParam('act', joi.string().required(), "GridFTP action: 'lookup', 'chdir', 'read', 'write', 'create', 'delete'") .summary('Checks authorization') .description('Checks authorization'); diff --git a/core/database/foxx/api/posix_path.js b/core/database/foxx/api/posix_path.js index 2a0fbf7bc..761f47a03 100644 --- a/core/database/foxx/api/posix_path.js +++ b/core/database/foxx/api/posix_path.js @@ -16,16 +16,17 @@ module.exports = (function() { **/ obj.splitPOSIXPath = function(a_posix_path) { - // Split the path into components + if (!a_posix_path || typeof a_posix_path !== 'string') { + throw new Error('Invalid POSIX path'); + } + // Split the path into components // components: ['', 'usr', 'local', 'bin', 'node'] // The empty '' is for root - const components = posixPath.split(path.posix.sep); + const components = a_posix_path.split(path.posix.sep); // components: ['usr', 'local', 'bin', 'node'] - const cleanComponents = components.filter(component => component !== ''); - - - return components; + return components.filter(component => component !== ''); } -}); + return obj; +})(); diff --git a/core/database/foxx/api/record.js b/core/database/foxx/api/record.js new file mode 100644 index 000000000..0e0eb8d84 --- /dev/null +++ b/core/database/foxx/api/record.js @@ -0,0 +1,151 @@ +'use strict'; + +const g_db = require('@arangodb').db; +const g_lib = require('./support'); + +/** + * @class Record + * @brief Represents a record in the database and provides methods to manage it. + */ +class Record { + + /** + * @brief Constructs a Record object and checks if the key exists in the database. + * @param {string} a_key - The unique identifier for the record. + */ + constructor(a_key) { + // Define the collection + const collection = db._collection('d'); + // 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. + + this.exists = false; + if (a_key) { + // Check if the document exists + this.exists = collection.exists(key); + this.key = a_key; + } + this.loc = null; + this.alloc = null; + this.error = null; + this.err_msg = null; + } + + /** + * @brief Checks if the record exists in the database. + * @return {boolean} True if the record exists, otherwise false. + */ + exists() { + return this.exists; + } + + + /** + * @brief Will return error code of last run method. + * + * If no error code, will return null + **/ + error() { + return this.error; + } + + /** + * @brief Retrieves the error code of the last run method. + * @return {string|null} Error code or null if no error. + */ + errorMessage() { + return this.err_msg; + } + + /** + * @brief Checks if the record is managed by DataFed. + * @return {boolean} True if managed, otherwise false. + */ + isManaged() { + //{ + // _from: data._id, + // _to: repo_alloc._to, + // uid: owner_id + //}; + this.loc = g_db.loc.firstExample({ + _from: data_id + }); + + if (!this.loc) { + this.error = g_lib.ERR_PERM_DENIED; + this.err_msg = "Permission denied data is not managed by DataFed. This can happen if you try to do a transfer directly from Globus."; + return false; + } + this.alloc = g_db.alloc.firstExample({ + _from: loc.uid, + _to: loc._to + }); + + // If alloc is null then will return false if not null will return true. + return !!this.alloc; + } + + /** + * @brief Validates if the provided record path is consistent with the database. + * @param {string} a_path - The path to validate. + * @return {boolean} True if consistent, otherwise false. + */ + isPathConsistent(a_path) { + + // This function will populate the this.loc member and the this.alloc + // member + if ( !this.isManaged() ) { + 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 (this.alloc.path + this.key !== a_path) { + + // This may be due to an alloc/owner change allow if a new path matches + // If it is not a new repo throw an error because we have + // already determine that it is not part of the old repository + // allocation. The path should only be different if it is a new + // repo. + if (!this.loc.new_repo) { + this.error = g_lib.ERR_PERM_DENIED; + this.err_msg = "Record path is not consistent with repo expected path is: " + this.alloc.path + this.key + " attempted path is " + a_path; + return false; + } + + // 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 + // oweners id. + // 2. Using the loc.uid parameter if not inflight to get the owner + // id. + var new_alloc = g_db.alloc.firstExample({ + _from: this.loc.new_owner ? this.loc.new_owner : this.loc.uid, + _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 (!new_alloc) { + this.error = g_lib.ERR_PERM_DENIED; + this.err_msg = "Permission denied, '" + this.key + "' is not part of an allocation '" + return false; + } + + if (new_alloc.path + this.key !== a_path) { + this.error = g_lib.ERR_PERM_DENIED; + this.err_msg = "Permission denied, DataFed registered path is '" + new_alloc.path + this.key + "' Globus path is '" + a_path + "'" + return false; + } + } + return true; + + } +}; + +module.exports = Record diff --git a/core/database/foxx/tests/path.test.js b/core/database/foxx/tests/path.test.js index eb8f8e55d..6d3695060 100644 --- a/core/database/foxx/tests/path.test.js +++ b/core/database/foxx/tests/path.test.js @@ -2,7 +2,7 @@ const chai = require('chai'); const expect = chai.expect; -const pathModule = require('./pathModule'); // Replace with the actual file name +const pathModule = require('../api/posix_path'); // Replace with the actual file name describe('splitPOSIXPath', function () { it('should split a simple POSIX path into components', function () { diff --git a/core/database/foxx/tests/record.test.js b/core/database/foxx/tests/record.test.js new file mode 100644 index 000000000..4f60c35c6 --- /dev/null +++ b/core/database/foxx/tests/record.test.js @@ -0,0 +1,45 @@ +"use strict" + +const chai = require('chai'); +const expect = chai.expect; +const Record = require('../api/record'); + +describe('Record Class', () => { + let collectionMock; + + beforeEach(() => { + collectionMock = { + exists: sinon.stub() + }; + sinon.stub(db, '_collection').returns(collectionMock); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should initialize correctly and check record existence', () => { + collectionMock.exists.withArgs('validKey').returns(true); + + const record = new Record('validKey'); + expect(record.exists).to.be.true; + expect(record.key).to.equal('validKey'); + }); + + it('should return error code and message', () => { + const record = new Record(); + record.error = 'ERR_CODE'; + record.err_msg = 'Error message'; + expect(record.error()).to.equal('ERR_CODE'); + expect(record.errorMessage()).to.equal('Error message'); + }); + + it('should validate record path consistency', () => { + const record = new Record('validKey'); + record.alloc = { path: '/expected/path/' }; + record.key = 'validKey'; + + const result = record.isRecordPathConsistent('/expected/path/validKey'); + expect(result).to.be.true; + }); +}); From 420abaaf60cc46be834de836a4e9660377830a69 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Mon, 16 Dec 2024 08:14:49 -0500 Subject: [PATCH 04/38] Add unit tests to CMake --- core/database/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/database/CMakeLists.txt b/core/database/CMakeLists.txt index 34527dabd..409cb3302 100644 --- a/core/database/CMakeLists.txt +++ b/core/database/CMakeLists.txt @@ -15,6 +15,8 @@ if( ENABLE_FOXX_TESTS ) add_test(NAME foxx_teardown COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_teardown.sh") add_test(NAME foxx_version COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "version") add_test(NAME foxx_support COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "support") + add_test(NAME foxx_record COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "record") + add_test(NAME foxx_path COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "path") set_tests_properties(foxx_setup PROPERTIES FIXTURES_SETUP Foxx) set_tests_properties(foxx_teardown PROPERTIES FIXTURES_CLEANUP Foxx) From 83086b8aa25230efa59852f8ba1d9515ba46c389 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 11:38:19 -0500 Subject: [PATCH 05/38] Add clean record.js object --- core/database/foxx/api/record.js | 174 +++++++++++++++++++------------ 1 file changed, 107 insertions(+), 67 deletions(-) diff --git a/core/database/foxx/api/record.js b/core/database/foxx/api/record.js index 0e0eb8d84..a27f3bc31 100644 --- a/core/database/foxx/api/record.js +++ b/core/database/foxx/api/record.js @@ -2,6 +2,7 @@ const g_db = require('@arangodb').db; const g_lib = require('./support'); +const { errors } = require('@arangodb'); /** * @class Record @@ -9,38 +10,84 @@ const g_lib = require('./support'); */ class Record { - /** - * @brief Constructs a Record object and checks if the key exists in the database. - * @param {string} a_key - The unique identifier for the record. - */ + // ERROR code + #error = null; + // Error message should be a string if defined + #err_msg = null; + // Boolean value that determines if the record exists in the database + #exists = false; + // location object, determines what the allocation the data item is associated with + #loc = null + // Allocation object, determines what allocation data item is associated with + #alloc = null; + // The data key + #key = null; + // The data id simply the key prepended with 'd/' + #data_id = null; + + /** + * @brief Constructs a Record object and checks if the key exists in the database. + * @param {string} a_key - The unique identifier for the record. + */ constructor(a_key) { // Define the collection - const collection = db._collection('d'); + const collection = g_db._collection('d'); + // 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. - - this.exists = false; + this.#key = a_key; + this.#data_id = "d/" + a_key; if (a_key) { // Check if the document exists - this.exists = collection.exists(key); - this.key = a_key; + try { + if (collection.exists(this.#key)) { + this.#exists = true; + } else { + this.#exists = false; + this.#error = g_lib.ERR_NOT_FOUND; + this.#err_msg = "Invalid key: (" + 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); + } } - this.loc = null; - this.alloc = null; - this.error = null; - this.err_msg = null; } - /** - * @brief Checks if the record exists in the database. - * @return {boolean} True if the record exists, otherwise false. - */ + /** + * @brief will create the path to key as it should appear on the repository. + **/ + _pathToRecord(basePath) { + return basePath.endsWith('/') ? basePath + this.#key : basePath + '/' + this.#key; + } + + /** + * @brief Compares two paths and if an error is detected will save the error code and message. + **/ + _comparePaths(storedPath, inputPath) { + if (storedPath !== inputPath) { + this.#error = g_lib.ERR_PERM_DENIED; + this.#err_msg = "Record path is not consistent with repo expected path is: " + storedPath + " attempted path is " + inputPath; + return false; + } + return true; + } + + /** + * @brief Checks if the record exists in the database. + * @return {boolean} True if the record exists, otherwise false. + */ exists() { - return this.exists; + return this.#exists; } + key() { + return this.#key; + } /** * @brief Will return error code of last run method. @@ -48,76 +95,65 @@ class Record { * If no error code, will return null **/ error() { - return this.error; + return this.#error; } - /** - * @brief Retrieves the error code of the last run method. - * @return {string|null} Error code or null if no error. - */ + /** + * @brief Retrieves the error code of the last run method. + * @return {string|null} Error code or null if no error. + */ errorMessage() { - return this.err_msg; + return this.#err_msg; } - /** - * @brief Checks if the record is managed by DataFed. - * @return {boolean} True if managed, otherwise false. - */ + /** + * @brief Checks if the record is managed by DataFed. + * @return {boolean} True if managed, otherwise false. + */ isManaged() { //{ // _from: data._id, // _to: repo_alloc._to, // uid: owner_id //}; - this.loc = g_db.loc.firstExample({ - _from: data_id + this.#loc = g_db.loc.firstExample({ + _from: this.#data_id }); - if (!this.loc) { - this.error = g_lib.ERR_PERM_DENIED; - this.err_msg = "Permission denied data is not managed by DataFed. This can happen if you try to do a transfer directly from Globus."; + if (!this.#loc) { + this.#error = g_lib.ERR_PERM_DENIED; + this.#err_msg = "Permission denied data is not managed by DataFed. This can happen if you try to do a transfer directly from Globus."; return false; } - this.alloc = g_db.alloc.firstExample({ - _from: loc.uid, - _to: loc._to + this.#alloc = g_db.alloc.firstExample({ + _from: this.#loc.uid, + _to: this.#loc._to }); // If alloc is null then will return false if not null will return true. - return !!this.alloc; + return !!this.#alloc; } - /** - * @brief Validates if the provided record path is consistent with the database. - * @param {string} a_path - The path to validate. - * @return {boolean} True if consistent, otherwise false. - */ + /** + * @brief Validates if the provided record path is consistent with the database. + * @param {string} a_path - The path to validate. + * @return {boolean} True if consistent, otherwise false. + */ isPathConsistent(a_path) { - // This function will populate the this.loc member and the this.alloc + // This function will populate the this.#loc member and the this.#alloc // member if ( !this.isManaged() ) { return false; } // If path is missing the starting "/" add it back in - if (!a_path.startsWith("/") && this.alloc.path.startsWith("/") ) { + if (!a_path.startsWith("/") && this.#alloc.path.startsWith("/") ) { a_path = "/" + a_path; } - if (this.alloc.path + this.key !== a_path) { - - // This may be due to an alloc/owner change allow if a new path matches - // If it is not a new repo throw an error because we have - // already determine that it is not part of the old repository - // allocation. The path should only be different if it is a new - // repo. - if (!this.loc.new_repo) { - this.error = g_lib.ERR_PERM_DENIED; - this.err_msg = "Record path is not consistent with repo expected path is: " + this.alloc.path + this.key + " attempted path is " + a_path; - return false; - } - + // If there is a new repo we need to check the path there and use that + if (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 @@ -125,26 +161,30 @@ class Record { // 2. Using the loc.uid parameter if not inflight to get the owner // id. var new_alloc = g_db.alloc.firstExample({ - _from: this.loc.new_owner ? this.loc.new_owner : this.loc.uid, - _to: this.loc.new_repo + _from: this.#loc.new_owner ? this.#loc.new_owner : this.#loc.uid, + _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 (!new_alloc) { - this.error = g_lib.ERR_PERM_DENIED; - this.err_msg = "Permission denied, '" + this.key + "' is not part of an allocation '" + this.#error = g_lib.ERR_PERM_DENIED; + this.#err_msg = "Permission denied, '" + this.#key + "' is not part of an allocation '" return false; } - if (new_alloc.path + this.key !== a_path) { - this.error = g_lib.ERR_PERM_DENIED; - this.err_msg = "Permission denied, DataFed registered path is '" + new_alloc.path + this.key + "' Globus path is '" + a_path + "'" - return false; - } + var stored_path = this._pathToRecord(new_alloc.path); + + if ( !this._comparePaths(stored_path, a_path) ) return false; + + } else { + + var stored_path = this._pathToRecord(this.#alloc.path); + + // If there is no new repo check that the paths align + if ( !this._comparePaths(stored_path, a_path) ) return false; } return true; - } }; From e8c912188fa7810d54dedf6128dd7e424af8c205 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 11:38:38 -0500 Subject: [PATCH 06/38] Add more specific pattern matching --- core/database/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/database/CMakeLists.txt b/core/database/CMakeLists.txt index 409cb3302..6e8b14406 100644 --- a/core/database/CMakeLists.txt +++ b/core/database/CMakeLists.txt @@ -15,7 +15,7 @@ if( ENABLE_FOXX_TESTS ) add_test(NAME foxx_teardown COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_teardown.sh") add_test(NAME foxx_version COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "version") add_test(NAME foxx_support COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "support") - add_test(NAME foxx_record COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "record") + add_test(NAME foxx_record COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_record") add_test(NAME foxx_path COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "path") set_tests_properties(foxx_setup PROPERTIES FIXTURES_SETUP Foxx) From 907489775244ecf7c51e1cffbace4395601c616b Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 11:46:17 -0500 Subject: [PATCH 07/38] Add README with test instructions. --- core/database/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 core/database/README.md diff --git a/core/database/README.md b/core/database/README.md new file mode 100644 index 000000000..6b821a776 --- /dev/null +++ b/core/database/README.md @@ -0,0 +1,34 @@ +# WARNING - Adding Tests + +Note CMake is configured to run tests one at a time. The tests are specified in +CMake by passing a string that is matched against the chai test cases in the +"it()" sections of the chai unit tests.. + +i.e. + +CMakeLists.txt line + +``` +add_test(NAME foxx_record COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_record") +``` + +This will pass "unit_record" as the pattern to be matched to the test_foxx.sh +script. In turn, the test_foxx.sh script will call foxx test with +"unit_record". Tests are not matched based on the name of the test file they +are matched based on the test cases. + +i.e. + +Below is part of a test case that would be matched against the "unit_record" pattern. + +``` + +describe('Record Class', () => { + it('unit_record: isPathConsistent should return false paths are inconsistent in new and old alloc.', () => { + : + : + }); +}); +``` + +Notice that 'unit_record' is explicitly mentioned in the test case. From 99dfbf17dd75641a320f9e414dd1cd1fe3c3b335 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 11:49:35 -0500 Subject: [PATCH 08/38] Apply formatting --- core/database/foxx/api/record.js | 90 +++++++++++++++++--------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/core/database/foxx/api/record.js b/core/database/foxx/api/record.js index a27f3bc31..6b1084b78 100644 --- a/core/database/foxx/api/record.js +++ b/core/database/foxx/api/record.js @@ -1,15 +1,14 @@ -'use strict'; +"use strict"; -const g_db = require('@arangodb').db; -const g_lib = require('./support'); -const { errors } = require('@arangodb'); +const g_db = require("@arangodb").db; +const g_lib = require("./support"); +const { errors } = require("@arangodb"); /** * @class Record * @brief Represents a record in the database and provides methods to manage it. */ -class Record { - +class Record { // ERROR code #error = null; // Error message should be a string if defined @@ -17,10 +16,10 @@ class Record { // Boolean value that determines if the record exists in the database #exists = false; // location object, determines what the allocation the data item is associated with - #loc = null + #loc = null; // Allocation object, determines what allocation data item is associated with #alloc = null; - // The data key + // The data key #key = null; // The data id simply the key prepended with 'd/' #data_id = null; @@ -31,29 +30,29 @@ class Record { */ constructor(a_key) { // Define the collection - const collection = g_db._collection('d'); + const collection = g_db._collection("d"); - // 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 - // + // 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. this.#key = a_key; this.#data_id = "d/" + a_key; if (a_key) { // Check if the document exists try { - if (collection.exists(this.#key)) { - this.#exists = true; - } else { - this.#exists = false; - this.#error = g_lib.ERR_NOT_FOUND; - this.#err_msg = "Invalid key: (" + a_key + "). No record found."; - } + if (collection.exists(this.#key)) { + this.#exists = true; + } else { + this.#exists = false; + this.#error = g_lib.ERR_NOT_FOUND; + this.#err_msg = "Invalid key: (" + 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); + this.#exists = false; + this.#error = g_lib.ERR_INTERNAL_FAULT; + this.#err_msg = "Unknown error encountered."; + console.log(e); } } } @@ -62,7 +61,9 @@ class Record { * @brief will create the path to key as it should appear on the repository. **/ _pathToRecord(basePath) { - return basePath.endsWith('/') ? basePath + this.#key : basePath + '/' + this.#key; + return basePath.endsWith("/") + ? basePath + this.#key + : basePath + "/" + this.#key; } /** @@ -71,7 +72,11 @@ class Record { _comparePaths(storedPath, inputPath) { if (storedPath !== inputPath) { this.#error = g_lib.ERR_PERM_DENIED; - this.#err_msg = "Record path is not consistent with repo expected path is: " + storedPath + " attempted path is " + inputPath; + this.#err_msg = + "Record path is not consistent with repo expected path is: " + + storedPath + + " attempted path is " + + inputPath; return false; } return true; @@ -95,7 +100,7 @@ class Record { * If no error code, will return null **/ error() { - return this.#error; + return this.#error; } /** @@ -117,17 +122,18 @@ class Record { // uid: owner_id //}; this.#loc = g_db.loc.firstExample({ - _from: this.#data_id + _from: this.#data_id, }); if (!this.#loc) { this.#error = g_lib.ERR_PERM_DENIED; - this.#err_msg = "Permission denied data is not managed by DataFed. This can happen if you try to do a transfer directly from Globus."; + this.#err_msg = + "Permission denied data is not managed by DataFed. This can happen if you try to do a transfer directly from Globus."; return false; } this.#alloc = g_db.alloc.firstExample({ _from: this.#loc.uid, - _to: this.#loc._to + _to: this.#loc._to, }); // If alloc is null then will return false if not null will return true. @@ -140,15 +146,14 @@ class Record { * @return {boolean} True if consistent, otherwise false. */ isPathConsistent(a_path) { - // This function will populate the this.#loc member and the this.#alloc // member - if ( !this.isManaged() ) { + if (!this.isManaged()) { return false; } // If path is missing the starting "/" add it back in - if (!a_path.startsWith("/") && this.#alloc.path.startsWith("/") ) { + if (!a_path.startsWith("/") && this.#alloc.path.startsWith("/")) { a_path = "/" + a_path; } @@ -162,30 +167,31 @@ class Record { // id. var new_alloc = g_db.alloc.firstExample({ _from: this.#loc.new_owner ? this.#loc.new_owner : this.#loc.uid, - _to: this.#loc.new_repo + _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 (!new_alloc) { this.#error = g_lib.ERR_PERM_DENIED; - this.#err_msg = "Permission denied, '" + this.#key + "' is not part of an allocation '" + this.#err_msg = + "Permission denied, '" + + this.#key + + "' is not part of an allocation '"; return false; } var stored_path = this._pathToRecord(new_alloc.path); - - if ( !this._comparePaths(stored_path, a_path) ) return false; - - } else { - + + if (!this._comparePaths(stored_path, a_path)) return false; + } else { var stored_path = this._pathToRecord(this.#alloc.path); // If there is no new repo check that the paths align - if ( !this._comparePaths(stored_path, a_path) ) return false; + if (!this._comparePaths(stored_path, a_path)) return false; } return true; } -}; +} -module.exports = Record +module.exports = Record; From 6b4a2b6ca3c9422eb599420097939ff084364228 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 11:55:33 -0500 Subject: [PATCH 09/38] Applied formatting --- core/database/foxx/api/authz_router.js | 458 +++++++++++++------------ 1 file changed, 241 insertions(+), 217 deletions(-) diff --git a/core/database/foxx/api/authz_router.js b/core/database/foxx/api/authz_router.js index 1575b4d92..e0a297a31 100644 --- a/core/database/foxx/api/authz_router.js +++ b/core/database/foxx/api/authz_router.js @@ -1,226 +1,250 @@ -'use strict'; +"use strict"; -const createRouter = require('@arangodb/foxx/router'); +const createRouter = require("@arangodb/foxx/router"); 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 dataModule = require('./data'); // Replace with the actual file name -const authzModule = require('./authz'); // Replace with the actual file name +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 module.exports = router; +router + .get("/gridftp", function (req, res) { + try { + console.log("/gridftp start authz client", req.queryParams.client, + "repo", req.queryParams.repo, + "file", req.queryParams.file, + "act", req.queryParams.act + ); + + // 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); + + const path_components = pathModule.splitPOSIXPath(req.queryParams.file); + const data_key = path_components.at(-1); + var record = Record(data_key); + // 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"] + + // Special case - allow unknown client to read a publicly accessible record + // if record exists and if it is a public record + if (!client) { + if (record.exists()) { + if (req.queryParams.act != "read" || !g_lib.hasPublicRead(data_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 + var 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]; + } -router.get('/gridftp', function(req, res) { - try { - console.log("/gridftp start authz client", req.queryParams.client, "repo", req.queryParams.repo, "file", req.queryParams.file, "act", req.queryParams.act); - - // 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); - - const path_components = pathModule.splitPOSIXPath(req.queryParams.file); - const data_key = path_components.at(-1); - var record = Record(data_key); - // 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"] - - // Special case - allow unknown client to read a publicly accessible record - // if record exists and if it is a public record - if (!client) { - if( record.exists() ) { - if (req.queryParams.act != "read" || !g_lib.hasPublicRead(data_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 - var 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; - } - } - } - - 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. - console.log("AUTHZ act: " + req.queryParams.act + " client: " + client._id + " path " + req.queryParams.file + " FAILED"); - throw [g_lib.ERR_PERM_DENIED, "Invalid record specified: " + req.queryParams.file]; - } - console.log("AUTHZ act: " + req.queryParams.act + " client: " + client._id + " path " + req.queryParams.file + " SUCCESS"); - - } catch (e) { - g_lib.handleException(e, res); + // 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; + } + } + } + + 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. + console.log( + "AUTHZ act: " + req.queryParams.act + " client: " + client._id + " path " + req.queryParams.file + " FAILED" + ); + throw [g_lib.ERR_PERM_DENIED, "Invalid record specified: " + req.queryParams.file]; + } + console.log( + "AUTHZ act: " + req.queryParams.act + " client: " + client._id + " path " + req.queryParams.file + " SUCCESS" + ); + } catch (e) { + g_lib.handleException(e, res); + } + }) + .queryParam("client", joi.string().required(), "Client ID") + .queryParam( + "repo", + joi.string().required(), + "Originating repo ID, where the DataFed managed GridFTP server is running." + ) + .queryParam("file", joi.string().required(), "Data file name") + .queryParam("act", joi.string().required(), "GridFTP action: 'lookup', 'chdir', 'read', 'write', 'create', 'delete'") + .summary("Checks authorization") + .description("Checks authorization"); + +router + .get("/perm/check", function (req, res) { + try { + const client = g_lib.getUserFromClientID(req.queryParams.client); + var perms = req.queryParams.perms ? req.queryParams.perms : g_lib.PERM_ALL; + var obj, + result = true, + id = g_lib.resolveID(req.queryParams.id, client), + ty = id[0]; + + if (id[1] != "/") { + throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; + } + + if (ty == "p") { + var role = g_lib.getProjectRole(client._id, id); + if (role == g_lib.PROJ_NO_ROLE) { + // Non members have only VIEW permissions + if (perms != g_lib.PERM_RD_REC) result = false; + } else if (role == g_lib.PROJ_MEMBER) { + // Non members have only VIEW permissions + if ((perms & ~g_lib.PERM_MEMBER) != 0) result = false; + } else if (role == g_lib.PROJ_MANAGER) { + // Managers have all but UPDATE + if ((perms & ~g_lib.PERM_MANAGER) != 0) result = false; + } + } else if (ty == "d") { + if (!g_lib.hasAdminPermObject(client, id)) { + obj = g_db.d.document(id); + if (obj.locked) result = false; + else result = g_lib.hasPermissions(client, obj, perms); + } + } else if (ty == "c") { + // If create perm is requested, ensure owner of collection has at least one allocation + if (perms & g_lib.PERM_CREATE) { + var owner = g_db.owner.firstExample({ + _from: id + }); + if ( + !g_db.alloc.firstExample({ + _from: owner._to + }) + ) { + throw [g_lib.ERR_NO_ALLOCATION, "An allocation is required to create a collection."]; + } + } + + if (!g_lib.hasAdminPermObject(client, id)) { + obj = g_db.c.document(id); + result = g_lib.hasPermissions(client, obj, perms); + } + } else { + throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; + } + + res.send({ + granted: result + }); + } catch (e) { + g_lib.handleException(e, res); + } + }) + .queryParam("client", joi.string().required(), "Client ID") + .queryParam("id", joi.string().required(), "Object ID or alias") + .queryParam("perms", joi.number().required(), "Permission bit mask to check") + .summary("Checks client permissions for object") + .description("Checks client permissions for object (projects, data, collections"); + +router + .get("/perm/get", function (req, res) { + try { + const client = g_lib.getUserFromClientID(req.queryParams.client); + var result = req.queryParams.perms ? req.queryParams.perms : g_lib.PERM_ALL; + var obj, + id = g_lib.resolveID(req.queryParams.id, client), + ty = id[0]; + + if (id[1] != "/") throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; + + if (ty == "p") { + var role = g_lib.getProjectRole(client._id, id); + if (role == g_lib.PROJ_NO_ROLE) { + // Non members have only VIEW permissions + result &= g_lib.PERM_RD_REC; + } else if (role == g_lib.PROJ_MEMBER) { + result &= g_lib.PERM_MEMBER; + } else if (role == g_lib.PROJ_MANAGER) { + // Managers have all but UPDATE + result &= g_lib.PERM_MANAGER; } - }) - .queryParam('client', joi.string().required(), "Client ID") - .queryParam('repo', joi.string().required(), "Originating repo ID, where the DataFed managed GridFTP server is running.") - .queryParam('file', joi.string().required(), "Data file name") - .queryParam('act', joi.string().required(), "GridFTP action: 'lookup', 'chdir', 'read', 'write', 'create', 'delete'") - .summary('Checks authorization') - .description('Checks authorization'); - - -router.get('/perm/check', function(req, res) { - try { - const client = g_lib.getUserFromClientID(req.queryParams.client); - var perms = req.queryParams.perms ? req.queryParams.perms : g_lib.PERM_ALL; - var obj, result = true, - id = g_lib.resolveID(req.queryParams.id, client), - ty = id[0]; - - if (id[1] != "/") { - throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; - } - - if (ty == "p") { - var role = g_lib.getProjectRole(client._id, id); - if (role == g_lib.PROJ_NO_ROLE) { // Non members have only VIEW permissions - if (perms != g_lib.PERM_RD_REC) - result = false; - } else if (role == g_lib.PROJ_MEMBER) { // Non members have only VIEW permissions - if ((perms & ~g_lib.PERM_MEMBER) != 0) - result = false; - } else if (role == g_lib.PROJ_MANAGER) { // Managers have all but UPDATE - if ((perms & ~g_lib.PERM_MANAGER) != 0) - result = false; - } - } else if (ty == "d") { - if (!g_lib.hasAdminPermObject(client, id)) { - obj = g_db.d.document(id); - if (obj.locked) - result = false; - else - result = g_lib.hasPermissions(client, obj, perms); - } - } else if (ty == "c") { - // If create perm is requested, ensure owner of collection has at least one allocation - if (perms & g_lib.PERM_CREATE) { - var owner = g_db.owner.firstExample({ - _from: id - }); - if (!g_db.alloc.firstExample({ - _from: owner._to - })) { - throw [g_lib.ERR_NO_ALLOCATION, "An allocation is required to create a collection."]; - } - } - - if (!g_lib.hasAdminPermObject(client, id)) { - obj = g_db.c.document(id); - result = g_lib.hasPermissions(client, obj, perms); - } - } else { - throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; - } - - res.send({ - granted: result - }); - } catch (e) { - g_lib.handleException(e, res); + } else if (ty == "d") { + if (!g_lib.hasAdminPermObject(client, id)) { + obj = g_db.d.document(id); + if (obj.locked) result = 0; + else result = g_lib.getPermissions(client, obj, result); } - }) - .queryParam('client', joi.string().required(), "Client ID") - .queryParam('id', joi.string().required(), "Object ID or alias") - .queryParam('perms', joi.number().required(), "Permission bit mask to check") - .summary('Checks client permissions for object') - .description('Checks client permissions for object (projects, data, collections'); - -router.get('/perm/get', function(req, res) { - try { - const client = g_lib.getUserFromClientID(req.queryParams.client); - var result = req.queryParams.perms ? req.queryParams.perms : g_lib.PERM_ALL; - var obj, id = g_lib.resolveID(req.queryParams.id, client), - ty = id[0]; - - if (id[1] != "/") - throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; - - if (ty == "p") { - var role = g_lib.getProjectRole(client._id, id); - if (role == g_lib.PROJ_NO_ROLE) { // Non members have only VIEW permissions - result &= g_lib.PERM_RD_REC; - } else if (role == g_lib.PROJ_MEMBER) { - result &= g_lib.PERM_MEMBER; - } else if (role == g_lib.PROJ_MANAGER) { // Managers have all but UPDATE - result &= g_lib.PERM_MANAGER; - } - } else if (ty == "d") { - if (!g_lib.hasAdminPermObject(client, id)) { - obj = g_db.d.document(id); - if (obj.locked) - result = 0; - else - result = g_lib.getPermissions(client, obj, result); - } - } else if (ty == "c") { - if (!g_lib.hasAdminPermObject(client, id)) { - obj = g_db.c.document(id); - result = g_lib.getPermissions(client, obj, result); - } - } else - throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; - - res.send({ - granted: result - }); - } catch (e) { - g_lib.handleException(e, res); + } else if (ty == "c") { + if (!g_lib.hasAdminPermObject(client, id)) { + obj = g_db.c.document(id); + result = g_lib.getPermissions(client, obj, result); } - }) - .queryParam('client', joi.string().required(), "Client ID") - .queryParam('id', joi.string().required(), "Object ID or alias") - .queryParam('perms', joi.number().optional(), "Permission bit mask to get (default = all)") - .summary('Gets client permissions for object') - .description('Gets client permissions for object (projects, data, collections. Note this is potentially slower than using "check" method.'); + } else throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; + + res.send({ + granted: result + }); + } catch (e) { + g_lib.handleException(e, res); + } + }) + .queryParam("client", joi.string().required(), "Client ID") + .queryParam("id", joi.string().required(), "Object ID or alias") + .queryParam("perms", joi.number().optional(), "Permission bit mask to get (default = all)") + .summary("Gets client permissions for object") + .description( + 'Gets client permissions for object (projects, data, collections. Note this is potentially slower than using "check" method.' + ); From a71e2dd272e20a6d1dc2229048cc14645f402d26 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 11:56:17 -0500 Subject: [PATCH 10/38] Apply formatting --- core/database/foxx/tests/record.test.js | 361 ++++++++++++++++++++++-- 1 file changed, 332 insertions(+), 29 deletions(-) diff --git a/core/database/foxx/tests/record.test.js b/core/database/foxx/tests/record.test.js index 4f60c35c6..525ee924b 100644 --- a/core/database/foxx/tests/record.test.js +++ b/core/database/foxx/tests/record.test.js @@ -1,45 +1,348 @@ -"use strict" +"use strict"; -const chai = require('chai'); +const chai = require("chai"); const expect = chai.expect; -const Record = require('../api/record'); +const Record = require("../api/record"); +const g_db = require("@arangodb").db; +const g_lib = require("../api/support"); +//const sinon = require('sinon'); +//const proxyquire = require('proxyquire'); +const arangodb = require("@arangodb"); -describe('Record Class', () => { - let collectionMock; +describe("Record Class", () => { + let count = 0; - beforeEach(() => { - collectionMock = { - exists: sinon.stub() - }; - sinon.stub(db, '_collection').returns(collectionMock); + beforeEach(() => { + console.log("Clearing database collections."); + console.log("Count is " + count); + count = count + 1; + g_db.d.truncate(); + g_db.alloc.truncate(); + g_db.loc.truncate(); + g_db.repo.truncate(); + }); + + it("unit_record: should initialize correctly and check record existence is invalid", () => { + const record = new Record("invalidKey"); + expect(record.exists()).to.be.false; + expect(record.key()).to.equal("invalidKey"); + expect(record.error()).to.equal(g_lib.ERR_NOT_FOUND); + expect(record.errorMessage()).to.equal( + "Invalid key: (invalidKey). No record found.", + ); + }); + + it("unit_record: should initialize correctly and check record existence is valid", () => { + const valid_key = "1111"; + const key_id = "d/1111"; + const owner_id = "u/bob"; + const repo_id = "repo/datafed-at-com"; + // Create nodes + g_db.d.save({ + _key: valid_key, + _id: key_id, + }); + g_db.repo.save({ + _id: repo_id, + }); + g_db.u.save({ + _id: owner_id, + }); + + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); + g_db.alloc.save({ + _from: owner_id, + _to: repo_id, + }); + const record = new Record(valid_key); + expect(record.exists()).to.be.true; + expect(record.key()).to.equal(valid_key); + expect(record.error()).to.be.null; + expect(record.errorMessage()).to.be.null; + }); + + it("unit_record: isManaged should initialize correctly, but show a record as not managed.", () => { + const valid_key = "1111"; + const key_id = "d/1111"; + const owner_id = "u/bob"; + const repo_id = "repo/datafed-at-com"; + // Create nodes + g_db.d.save({ + _key: valid_key, + _id: key_id, + }); + g_db.repo.save({ + _id: repo_id, + }); + g_db.u.save({ + _id: owner_id, + }); + + const record = new Record(valid_key); + expect(record.isManaged()).to.be.false; + expect(record.exists()).to.be.true; + expect(record.key()).to.equal(valid_key); + expect(record.error()).to.equal(g_lib.ERR_PERM_DENIED); + const pattern = /^Permission denied data is not managed by DataFed/; + expect(record.errorMessage()).to.match(pattern); + }); + + it("unit_record: isManaged should initialize correctly, but without an allocation so should return false", () => { + const valid_key = "1111"; + const key_id = "d/1111"; + const owner_id = "u/bob"; + const repo_id = "repo/datafed-at-com"; + // Create nodes + g_db.d.save({ + _key: valid_key, + _id: key_id, + }); + g_db.repo.save({ + _id: repo_id, + }); + g_db.u.save({ + _id: owner_id, + }); + + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); + + const record = new Record(valid_key); + expect(record.isManaged()).to.be.false; + expect(record.exists()).to.be.true; + expect(record.key()).to.equal(valid_key); + expect(record.error()).to.be.null; + }); + + it("unit_record: isManaged should initialize correctly, and with allocation show that record is managed.", () => { + const valid_key = "1111"; + const key_id = "d/1111"; + const owner_id = "u/bob"; + const repo_id = "repo/datafed-at-com"; + // Create nodes + g_db.d.save({ + _key: valid_key, + _id: key_id, + }); + g_db.repo.save({ + _id: repo_id, + }); + g_db.u.save({ + _id: owner_id, + }); + + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); + g_db.alloc.save({ + _from: owner_id, + _to: repo_id, + }); + + const record = new Record(valid_key); + expect(record.isManaged()).to.be.true; + expect(record.exists()).to.be.true; + expect(record.key()).to.equal(valid_key); + expect(record.error()).to.be.null; + }); + + it("unit_record: isPathConsistent should return false because it is not managed.", () => { + const valid_key = "1111"; + const key_id = "d/1111"; + const owner_id = "u/bob"; + const repo_id = "repo/datafed-at-com"; + // Create nodes + g_db.d.save({ + _key: valid_key, + _id: key_id, + }); + g_db.repo.save({ + _id: repo_id, + }); + g_db.u.save({ + _id: owner_id, }); - afterEach(() => { - sinon.restore(); + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, }); - it('should initialize correctly and check record existence', () => { - collectionMock.exists.withArgs('validKey').returns(true); + const record = new Record(valid_key); + expect(record.isPathConsistent("file/path/" + valid_key)).to.be.false; + }); - const record = new Record('validKey'); - expect(record.exists).to.be.true; - expect(record.key).to.equal('validKey'); + it("unit_record: isPathConsistent should return false paths are not consistent.", () => { + const valid_key = "1111"; + const key_id = "d/1111"; + const owner_id = "u/bob"; + const repo_id = "repo/datafed-at-com"; + // Create nodes + g_db.d.save({ + _key: valid_key, + _id: key_id, + }); + g_db.repo.save({ + _id: repo_id, + }); + g_db.u.save({ + _id: owner_id, }); - it('should return error code and message', () => { - const record = new Record(); - record.error = 'ERR_CODE'; - record.err_msg = 'Error message'; - expect(record.error()).to.equal('ERR_CODE'); - expect(record.errorMessage()).to.equal('Error message'); + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); + g_db.alloc.save({ + _from: owner_id, + _to: repo_id, + path: "/correct/file/path", }); - it('should validate record path consistency', () => { - const record = new Record('validKey'); - record.alloc = { path: '/expected/path/' }; - record.key = 'validKey'; + const record = new Record(valid_key); + expect(record.isPathConsistent("/incorrect/file/path/" + 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); + }); - const result = record.isRecordPathConsistent('/expected/path/validKey'); - expect(result).to.be.true; + it("unit_record: isPathConsistent should return true paths are consistent.", () => { + const valid_key = "1111"; + const key_id = "d/1111"; + const owner_id = "u/bob"; + const repo_id = "repo/datafed-at-com"; + // Create nodes + g_db.d.save({ + _key: valid_key, + _id: key_id, + }); + g_db.repo.save({ + _id: repo_id, + }); + g_db.u.save({ + _id: owner_id, + }); + + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); + g_db.alloc.save({ + _from: owner_id, + _to: 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.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.", () => { + const valid_key = "1111"; + const key_id = "d/1111"; + const owner_id = "u/bob"; + const repo_id = "repo/datafed-at-com"; + const new_repo_id = "repo/datafed-at-org"; + // Create nodes + g_db.d.save({ + _key: valid_key, + _id: key_id, + }); + g_db.repo.save({ + _id: repo_id, + }); + g_db.u.save({ + _id: owner_id, }); + + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + new_repo: "repo/datafed-at-org", + }); + 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.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.", () => { + const valid_key = "1111"; + const key_id = "d/1111"; + const owner_id = "u/bob"; + const repo_id = "repo/datafed-at-com"; + const new_repo_id = "repo/datafed-at-org"; + // Create nodes + g_db.d.save({ + _key: valid_key, + _id: key_id, + }); + g_db.repo.save({ + _id: repo_id, + }); + g_db.u.save({ + _id: owner_id, + }); + + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + new_repo: "repo/datafed-at-org", + }); + 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.error()).to.equal(g_lib.ERR_PERM_DENIED); + const pattern = /^Record path is not consistent/; + expect(record.errorMessage()).to.match(pattern); + }); }); From 83de46c0af124469a55657d1ef10afe1addfbc2e Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 11:57:04 -0500 Subject: [PATCH 11/38] add missing pass file --- core/database/tests/test_foxx.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/database/tests/test_foxx.sh b/core/database/tests/test_foxx.sh index 5eac033e7..b818ce9cf 100755 --- a/core/database/tests/test_foxx.sh +++ b/core/database/tests/test_foxx.sh @@ -128,6 +128,7 @@ 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 @@ -142,5 +143,5 @@ else --server "tcp://${DATAFED_DATABASE_HOST}:8529" \ -p "${PATH_TO_PASSWD_FILE}" \ --database "${local_DATABASE_NAME}" \ - "/api/${local_FOXX_MAJOR_API_VERSION}" "$TEST_TO_RUN" --reporter spec + "/api/${local_FOXX_MAJOR_API_VERSION}" "$TEST_TO_RUN" --reporter spec --verbose fi From c2431b60c05f6607b301fc3c5a2487cc1419c53f Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 12:46:32 -0500 Subject: [PATCH 12/38] Add missing new command --- core/database/foxx/api/authz_router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/database/foxx/api/authz_router.js b/core/database/foxx/api/authz_router.js index e0a297a31..b84972d1b 100644 --- a/core/database/foxx/api/authz_router.js +++ b/core/database/foxx/api/authz_router.js @@ -38,7 +38,7 @@ router const path_components = pathModule.splitPOSIXPath(req.queryParams.file); const data_key = path_components.at(-1); - var record = Record(data_key); + var record = new Record(data_key); // Will split a posix path into an array // E.g. // req.queryParams.file = "/usr/local/bin" From 2625b58ea688424fb5e9d38dc4479504a29c13c3 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 15:00:25 -0500 Subject: [PATCH 13/38] Negate if statement --- core/database/foxx/api/authz_router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/database/foxx/api/authz_router.js b/core/database/foxx/api/authz_router.js index b84972d1b..3b836065d 100644 --- a/core/database/foxx/api/authz_router.js +++ b/core/database/foxx/api/authz_router.js @@ -87,7 +87,7 @@ router // 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)) { + if (!authzModule.isRecordActionAuthorized(client, data_key, req_perm)) { console.log( "AUTHZ act: " + req.queryParams.act + " client: " + client._id + From 1c7e591183db331f5d8306f1677f02ad0cc38100 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:28:17 -0500 Subject: [PATCH 14/38] Add initial authz test --- core/database/foxx/tests/authz.test.js | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 core/database/foxx/tests/authz.test.js diff --git a/core/database/foxx/tests/authz.test.js b/core/database/foxx/tests/authz.test.js new file mode 100644 index 000000000..ff59710b2 --- /dev/null +++ b/core/database/foxx/tests/authz.test.js @@ -0,0 +1,49 @@ +"use strict"; + +const chai = require("chai"); +const expect = chai.expect; +const authzModule = require("./authz"); // Replace with the actual file name +const g_db = require("@arangodb").db; +const g_lib = require("../api/support"); +const arangodb = require("@arangodb"); + +describe("Authz functions", () => { + + beforeEach(() => { + g_db.d.truncate(); + g_db.alloc.truncate(); + g_db.loc.truncate(); + g_db.repo.truncate(); + }); + + it("unit_authz: if admin 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: "george" + }); + + let owner_id = "u/not_bob"; + let client = { + _key: "bob", + _id: "u/bob", + is_admin: true + }; + + g_db.u.save(client); + + g_db.owner.save({ + _from: data_key, + _to: owner_id + }); + + let req_perm = g_lib.PERM_CREATE; + + expect(authzModule.isRecordActionAuthorized(client, data_key, req_perm)).to.be.true; + } + +}); From 6ffc681e2b177eb57c9add8a95a1f14028fe5b89 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:28:45 -0500 Subject: [PATCH 15/38] Add initial authz test --- core/database/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/database/CMakeLists.txt b/core/database/CMakeLists.txt index 6e8b14406..3a0085799 100644 --- a/core/database/CMakeLists.txt +++ b/core/database/CMakeLists.txt @@ -15,6 +15,7 @@ if( ENABLE_FOXX_TESTS ) add_test(NAME foxx_teardown COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_teardown.sh") add_test(NAME foxx_version COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "version") add_test(NAME foxx_support COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "support") + add_test(NAME foxx_record 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_path COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "path") From 839767897499922958d864eea90a8e370f1366ee Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:29:14 -0500 Subject: [PATCH 16/38] Added explantory comments --- core/database/foxx/api/authz.js | 35 ++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/core/database/foxx/api/authz.js b/core/database/foxx/api/authz.js index 789f1f222..2efa0664f 100644 --- a/core/database/foxx/api/authz.js +++ b/core/database/foxx/api/authz.js @@ -7,7 +7,39 @@ const g_lib = require('./support'); module.exports = (function() { var obj = {} - + /** + * @brief Will check to see if a client has the required permissions on a + * record. + * + * @param {string} a_data_key - A datafed key associated with a record. Is not prepended with 'd/' + * @param {obj} a_client - A user document, the user associated with the document is the one + * who we are verifying if they have permissions to on the data record. + * + * e.g. + * + * a_client id + * + * 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" + * } + * + * @param - the permission type that is being checked i.e. + * + * PERM_CREATE + * PERM_WR_DATA + * PERM_RD_DATA + **/ obj.isRecordActionAuthorized = function(a_client, a_data_key, a_perm) { const data_id = "d/" + a_data_key; // If the user is not an admin of the object we will need @@ -22,5 +54,6 @@ module.exports = (function() { } return false; }; + return obj; })(); From cc66b9b503bdc7b1e0cbad42942dba0635edd533 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:29:42 -0500 Subject: [PATCH 17/38] Addressed review feedback --- core/database/foxx/api/authz_router.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/database/foxx/api/authz_router.js b/core/database/foxx/api/authz_router.js index b84972d1b..2d708f2d1 100644 --- a/core/database/foxx/api/authz_router.js +++ b/core/database/foxx/api/authz_router.js @@ -36,9 +36,6 @@ router // } const client = g_lib.getUserFromClientID_noexcept(req.queryParams.client); - const path_components = pathModule.splitPOSIXPath(req.queryParams.file); - const data_key = path_components.at(-1); - var record = new Record(data_key); // Will split a posix path into an array // E.g. // req.queryParams.file = "/usr/local/bin" @@ -46,12 +43,15 @@ router // // 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 if (!client) { if (record.exists()) { - if (req.queryParams.act != "read" || !g_lib.hasPublicRead(data_id)) { + if (req.queryParams.act != "read" || !g_lib.hasPublicRead(record.id())) { console.log( "AUTHZ act: " + req.queryParams.act + " client: " + client._id + @@ -63,7 +63,7 @@ router } } else { // Actions: read, write, create, delete, chdir, lookup - var req_perm = 0; + let req_perm = 0; switch (req.queryParams.act) { case "read": req_perm = g_lib.PERM_RD_DATA; From 604f465a533278c99b1711e142b7c8b636a4d7e2 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:30:08 -0500 Subject: [PATCH 18/38] Addressed code review feedback --- core/database/foxx/api/record.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/database/foxx/api/record.js b/core/database/foxx/api/record.js index 6b1084b78..3c9c8b069 100644 --- a/core/database/foxx/api/record.js +++ b/core/database/foxx/api/record.js @@ -94,6 +94,9 @@ class Record { return this.#key; } + id() { + return this.#data_id; + } /** * @brief Will return error code of last run method. * @@ -165,7 +168,7 @@ class Record { // oweners id. // 2. Using the loc.uid parameter if not inflight to get the owner // id. - var new_alloc = g_db.alloc.firstExample({ + let new_alloc = g_db.alloc.firstExample({ _from: this.#loc.new_owner ? this.#loc.new_owner : this.#loc.uid, _to: this.#loc.new_repo, }); @@ -181,14 +184,14 @@ class Record { return false; } - var stored_path = this._pathToRecord(new_alloc.path); + let stored_path = this._pathToRecord(new_alloc.path); - if (!this._comparePaths(stored_path, a_path)) return false; + if (!this._comparePaths(stored_path, a_path)) { return false; } } else { - var stored_path = this._pathToRecord(this.#alloc.path); + let stored_path = this._pathToRecord(this.#alloc.path); // If there is no new repo check that the paths align - if (!this._comparePaths(stored_path, a_path)) return false; + if (!this._comparePaths(stored_path, a_path)) { return false; } } return true; } From 97f26c9b83c85e4328ebc359459960bda400fb71 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:31:42 -0500 Subject: [PATCH 19/38] add in code documentation to support.js --- core/database/foxx/api/support.js | 41 ++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/core/database/foxx/api/support.js b/core/database/foxx/api/support.js index 87bfd3fe6..4c8ec0a99 100644 --- a/core/database/foxx/api/support.js +++ b/core/database/foxx/api/support.js @@ -595,7 +595,21 @@ module.exports = (function() { return first_uuid; } - obj.getUserFromClientID = function(a_client_id) { + // The returned object 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" + // } + obj.getUserFromClientID = function(a_client_id) { // Client ID can be an SDMS uname (xxxxx...), a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), or an account (domain.uname) // UUID are defined by length and format, accounts have a "." (and known domains), SDMS unames have no "." or "-" characters @@ -935,6 +949,31 @@ module.exports = (function() { return false; }; + /** + * @breif checks to make sure the client has admin permissions on an object + * + * @param a_client - this is a user document i.e. + * + * { + * "_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" + * } + * + * @param a_object_id - the identity of a record or collection or project + * + * "d/fdakjfla" + * "p/big_thing" + * "c/my_collection" + **/ obj.hasAdminPermObject = function(a_client, a_object_id) { if (a_client.is_admin) return true; From 74f768ff484861f583fc8dae9989cd5fee808728 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:32:07 -0500 Subject: [PATCH 20/38] cleaned up log messagets from record test --- core/database/foxx/tests/record.test.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/core/database/foxx/tests/record.test.js b/core/database/foxx/tests/record.test.js index 525ee924b..230f0be15 100644 --- a/core/database/foxx/tests/record.test.js +++ b/core/database/foxx/tests/record.test.js @@ -5,17 +5,10 @@ const expect = chai.expect; const Record = require("../api/record"); const g_db = require("@arangodb").db; const g_lib = require("../api/support"); -//const sinon = require('sinon'); -//const proxyquire = require('proxyquire'); const arangodb = require("@arangodb"); describe("Record Class", () => { - let count = 0; - beforeEach(() => { - console.log("Clearing database collections."); - console.log("Count is " + count); - count = count + 1; g_db.d.truncate(); g_db.alloc.truncate(); g_db.loc.truncate(); From a0e32e6547478d519ab06fec06b8c5fe4af94ed4 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:34:16 -0500 Subject: [PATCH 21/38] Make path test consistent with record and authz test --- core/database/CMakeLists.txt | 2 +- core/database/foxx/tests/path.test.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/database/CMakeLists.txt b/core/database/CMakeLists.txt index 3a0085799..aea2d0fe4 100644 --- a/core/database/CMakeLists.txt +++ b/core/database/CMakeLists.txt @@ -17,7 +17,7 @@ if( ENABLE_FOXX_TESTS ) add_test(NAME foxx_support COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "support") add_test(NAME foxx_record 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_path COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "path") + add_test(NAME foxx_path COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_path") set_tests_properties(foxx_setup PROPERTIES FIXTURES_SETUP Foxx) set_tests_properties(foxx_teardown PROPERTIES FIXTURES_CLEANUP Foxx) diff --git a/core/database/foxx/tests/path.test.js b/core/database/foxx/tests/path.test.js index 6d3695060..c4a4a9f2c 100644 --- a/core/database/foxx/tests/path.test.js +++ b/core/database/foxx/tests/path.test.js @@ -5,31 +5,31 @@ const expect = chai.expect; const pathModule = require('../api/posix_path'); // Replace with the actual file name describe('splitPOSIXPath', function () { - it('should split a simple POSIX path into components', function () { + it('unit_path: splitPOSIXPath should split a simple POSIX path into components', function () { const result = pathModule.splitPOSIXPath('/usr/local/bin/node'); expect(result).to.deep.equal(['usr', 'local', 'bin', 'node']); }); - it('should handle root path and return an empty array', function () { + it('unit_path: splitPOSIXPath should handle root path and return an empty array', function () { const result = pathModule.splitPOSIXPath('/'); expect(result).to.deep.equal([]); }); - it('should handle paths with trailing slashes correctly', function () { + it('unit_path: splitPOSIXPath should handle paths with trailing slashes correctly', function () { const result = pathModule.splitPOSIXPath('/usr/local/bin/'); expect(result).to.deep.equal(['usr', 'local', 'bin']); }); - it('should handle empty paths and throw an error', function () { + it('unit_path: splitPOSIXPath should handle empty paths and throw an error', function () { expect(() => pathModule.splitPOSIXPath('')).to.throw('Invalid POSIX path'); }); - it('should handle null or undefined paths and throw an error', function () { + it('unit_path: splitPOSIXPath should handle null or undefined paths and throw an error', function () { expect(() => pathModule.splitPOSIXPath(null)).to.throw('Invalid POSIX path'); expect(() => pathModule.splitPOSIXPath(undefined)).to.throw('Invalid POSIX path'); }); - it('should handle non-string inputs and throw an error', function () { + it('unit_path: splitPOSIXPath should handle non-string inputs and throw an error', function () { expect(() => pathModule.splitPOSIXPath(123)).to.throw('Invalid POSIX path'); expect(() => pathModule.splitPOSIXPath({})).to.throw('Invalid POSIX path'); expect(() => pathModule.splitPOSIXPath([])).to.throw('Invalid POSIX path'); From a7fb3c91681790d4641df2dfb48e35741395867a Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:44:06 -0500 Subject: [PATCH 22/38] Revert all of the formatting changes that do not have to do with the authz check method --- core/database/foxx/api/authz_router.js | 224 ++++++++++++------------- 1 file changed, 111 insertions(+), 113 deletions(-) diff --git a/core/database/foxx/api/authz_router.js b/core/database/foxx/api/authz_router.js index 6757687a5..e354252aa 100644 --- a/core/database/foxx/api/authz_router.js +++ b/core/database/foxx/api/authz_router.js @@ -131,120 +131,118 @@ router .summary("Checks authorization") .description("Checks authorization"); -router - .get("/perm/check", function (req, res) { - try { - const client = g_lib.getUserFromClientID(req.queryParams.client); - var perms = req.queryParams.perms ? req.queryParams.perms : g_lib.PERM_ALL; - var obj, - result = true, - id = g_lib.resolveID(req.queryParams.id, client), - ty = id[0]; - - if (id[1] != "/") { - throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; - } - - if (ty == "p") { - var role = g_lib.getProjectRole(client._id, id); - if (role == g_lib.PROJ_NO_ROLE) { - // Non members have only VIEW permissions - if (perms != g_lib.PERM_RD_REC) result = false; - } else if (role == g_lib.PROJ_MEMBER) { - // Non members have only VIEW permissions - if ((perms & ~g_lib.PERM_MEMBER) != 0) result = false; - } else if (role == g_lib.PROJ_MANAGER) { - // Managers have all but UPDATE - if ((perms & ~g_lib.PERM_MANAGER) != 0) result = false; +router.get('/perm/check', function(req, res) { + try { + const client = g_lib.getUserFromClientID(req.queryParams.client); + var perms = req.queryParams.perms ? req.queryParams.perms : g_lib.PERM_ALL; + var obj, result = true, + id = g_lib.resolveID(req.queryParams.id, client), + ty = id[0]; + + if (id[1] != "/") { + throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; + } + + if (ty == "p") { + var role = g_lib.getProjectRole(client._id, id); + if (role == g_lib.PROJ_NO_ROLE) { // Non members have only VIEW permissions + if (perms != g_lib.PERM_RD_REC) + result = false; + } else if (role == g_lib.PROJ_MEMBER) { // Non members have only VIEW permissions + if ((perms & ~g_lib.PERM_MEMBER) != 0) + result = false; + } else if (role == g_lib.PROJ_MANAGER) { // Managers have all but UPDATE + if ((perms & ~g_lib.PERM_MANAGER) != 0) + result = false; + } + } else if (ty == "d") { + if (!g_lib.hasAdminPermObject(client, id)) { + obj = g_db.d.document(id); + if (obj.locked) + result = false; + else + result = g_lib.hasPermissions(client, obj, perms); + } + } else if (ty == "c") { + // If create perm is requested, ensure owner of collection has at least one allocation + if (perms & g_lib.PERM_CREATE) { + var owner = g_db.owner.firstExample({ + _from: id + }); + if (!g_db.alloc.firstExample({ + _from: owner._to + })) { + throw [g_lib.ERR_NO_ALLOCATION, "An allocation is required to create a collection."]; + } + } + + if (!g_lib.hasAdminPermObject(client, id)) { + obj = g_db.c.document(id); + result = g_lib.hasPermissions(client, obj, perms); + } + } else { + throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; + } + + res.send({ + granted: result + }); + } catch (e) { + g_lib.handleException(e, res); } - } else if (ty == "d") { - if (!g_lib.hasAdminPermObject(client, id)) { - obj = g_db.d.document(id); - if (obj.locked) result = false; - else result = g_lib.hasPermissions(client, obj, perms); + }) + .queryParam('client', joi.string().required(), "Client ID") + .queryParam('id', joi.string().required(), "Object ID or alias") + .queryParam('perms', joi.number().required(), "Permission bit mask to check") + .summary('Checks client permissions for object') + .description('Checks client permissions for object (projects, data, collections'); + +router.get('/perm/get', function(req, res) { + try { + const client = g_lib.getUserFromClientID(req.queryParams.client); + var result = req.queryParams.perms ? req.queryParams.perms : g_lib.PERM_ALL; + var obj, id = g_lib.resolveID(req.queryParams.id, client), + ty = id[0]; + + if (id[1] != "/") + throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; + + if (ty == "p") { + var role = g_lib.getProjectRole(client._id, id); + if (role == g_lib.PROJ_NO_ROLE) { // Non members have only VIEW permissions + result &= g_lib.PERM_RD_REC; + } else if (role == g_lib.PROJ_MEMBER) { + result &= g_lib.PERM_MEMBER; + } else if (role == g_lib.PROJ_MANAGER) { // Managers have all but UPDATE + result &= g_lib.PERM_MANAGER; + } + } else if (ty == "d") { + if (!g_lib.hasAdminPermObject(client, id)) { + obj = g_db.d.document(id); + if (obj.locked) + result = 0; + else + result = g_lib.getPermissions(client, obj, result); + } + } else if (ty == "c") { + if (!g_lib.hasAdminPermObject(client, id)) { + obj = g_db.c.document(id); + result = g_lib.getPermissions(client, obj, result); + } + } else + throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; + + res.send({ + granted: result + }); + } catch (e) { + g_lib.handleException(e, res); } - } else if (ty == "c") { - // If create perm is requested, ensure owner of collection has at least one allocation - if (perms & g_lib.PERM_CREATE) { - var owner = g_db.owner.firstExample({ - _from: id - }); - if ( - !g_db.alloc.firstExample({ - _from: owner._to - }) - ) { - throw [g_lib.ERR_NO_ALLOCATION, "An allocation is required to create a collection."]; - } - } - - if (!g_lib.hasAdminPermObject(client, id)) { - obj = g_db.c.document(id); - result = g_lib.hasPermissions(client, obj, perms); - } - } else { - throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; - } - - res.send({ - granted: result - }); - } catch (e) { - g_lib.handleException(e, res); - } - }) - .queryParam("client", joi.string().required(), "Client ID") - .queryParam("id", joi.string().required(), "Object ID or alias") - .queryParam("perms", joi.number().required(), "Permission bit mask to check") - .summary("Checks client permissions for object") - .description("Checks client permissions for object (projects, data, collections"); + }) + .queryParam('client', joi.string().required(), "Client ID") + .queryParam('id', joi.string().required(), "Object ID or alias") + .queryParam('perms', joi.number().optional(), "Permission bit mask to get (default = all)") + .summary('Gets client permissions for object') + .description('Gets client permissions for object (projects, data, collections. Note this is potentially slower than using "check" method.'); -router - .get("/perm/get", function (req, res) { - try { - const client = g_lib.getUserFromClientID(req.queryParams.client); - var result = req.queryParams.perms ? req.queryParams.perms : g_lib.PERM_ALL; - var obj, - id = g_lib.resolveID(req.queryParams.id, client), - ty = id[0]; - - if (id[1] != "/") throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; - - if (ty == "p") { - var role = g_lib.getProjectRole(client._id, id); - if (role == g_lib.PROJ_NO_ROLE) { - // Non members have only VIEW permissions - result &= g_lib.PERM_RD_REC; - } else if (role == g_lib.PROJ_MEMBER) { - result &= g_lib.PERM_MEMBER; - } else if (role == g_lib.PROJ_MANAGER) { - // Managers have all but UPDATE - result &= g_lib.PERM_MANAGER; - } - } else if (ty == "d") { - if (!g_lib.hasAdminPermObject(client, id)) { - obj = g_db.d.document(id); - if (obj.locked) result = 0; - else result = g_lib.getPermissions(client, obj, result); - } - } else if (ty == "c") { - if (!g_lib.hasAdminPermObject(client, id)) { - obj = g_db.c.document(id); - result = g_lib.getPermissions(client, obj, result); - } - } else throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; - res.send({ - granted: result - }); - } catch (e) { - g_lib.handleException(e, res); - } - }) - .queryParam("client", joi.string().required(), "Client ID") - .queryParam("id", joi.string().required(), "Object ID or alias") - .queryParam("perms", joi.number().optional(), "Permission bit mask to get (default = all)") - .summary("Gets client permissions for object") - .description( - 'Gets client permissions for object (projects, data, collections. Note this is potentially slower than using "check" method.' - ); From d551b7c54f6b7a21d63ea6e387989a5d0637fd09 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:51:50 -0500 Subject: [PATCH 23/38] Address code review comment --- core/database/foxx/api/authz.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/database/foxx/api/authz.js b/core/database/foxx/api/authz.js index 2efa0664f..1d8652b20 100644 --- a/core/database/foxx/api/authz.js +++ b/core/database/foxx/api/authz.js @@ -47,7 +47,7 @@ module.exports = (function() { if (g_lib.hasAdminPermObject(a_client, data_id)) { return true; } - var data = g_db.d.document(data_id); + let data = g_db.d.document(data_id); // Grab the data item if (g_lib.hasPermissions(a_client, data, a_perm)) { return true; From 5cf208bf1553dbcb4fe5756d297433fa4edff1fe Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Wed, 18 Dec 2024 16:56:10 -0500 Subject: [PATCH 24/38] Address code review --- core/database/foxx/api/posix_path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/database/foxx/api/posix_path.js b/core/database/foxx/api/posix_path.js index 761f47a03..3ae3849bb 100644 --- a/core/database/foxx/api/posix_path.js +++ b/core/database/foxx/api/posix_path.js @@ -3,7 +3,7 @@ const path = require('path'); module.exports = (function() { - var obj = {} + let obj = {} /** * \brief will split a path string into components From ed9d77e29986c44fd3c4758b85b55b0fe607b9e3 Mon Sep 17 00:00:00 2001 From: Joshua S Brown Date: Wed, 18 Dec 2024 17:04:22 -0500 Subject: [PATCH 25/38] Update README.md --- core/database/README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/core/database/README.md b/core/database/README.md index 6b821a776..3e2a5beec 100644 --- a/core/database/README.md +++ b/core/database/README.md @@ -1,8 +1,8 @@ # WARNING - Adding Tests -Note CMake is configured to run tests one at a time. The tests are specified in +Note The granularity of CMake is dependent on how they are defined in the CMakeLists.txt file. The tests are specified in CMake by passing a string that is matched against the chai test cases in the -"it()" sections of the chai unit tests.. +"it()" sections of the chai unit tests. Any test cases that match the pattern will run when that test is triggered. i.e. @@ -28,7 +28,17 @@ describe('Record Class', () => { : : }); + + + it('unit_record: isPathConsistent a different test case.', () => { + : + : + }); }); ``` -Notice that 'unit_record' is explicitly mentioned in the test case. +Notice that 'unit_record' is explicitly mentioned in the test cases. In the above exerpt, both tests will run. If ctest were to be explicitly called we could run all unit_record tests with the following. + +``` +ctest -R foxx_record +``` From f409ce5efed4e254c40a3a1ea6a58a6729fab9c5 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Thu, 19 Dec 2024 23:55:14 -0500 Subject: [PATCH 26/38] Make corrections to unit tests --- core/database/CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/database/CMakeLists.txt b/core/database/CMakeLists.txt index aea2d0fe4..5468b5431 100644 --- a/core/database/CMakeLists.txt +++ b/core/database/CMakeLists.txt @@ -15,7 +15,7 @@ if( ENABLE_FOXX_TESTS ) add_test(NAME foxx_teardown COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_teardown.sh") add_test(NAME foxx_version COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "version") add_test(NAME foxx_support COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "support") - add_test(NAME foxx_record COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_authz") + 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_path COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_path") @@ -23,4 +23,7 @@ if( ENABLE_FOXX_TESTS ) set_tests_properties(foxx_teardown PROPERTIES FIXTURES_CLEANUP Foxx) 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_record PROPERTIES FIXTURES_REQUIRED Foxx) + set_tests_properties(foxx_path PROPERTIES FIXTURES_REQUIRED Foxx) endif() From 124d378b0e31a893ce710c84e9038e3aac49105a Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Thu, 19 Dec 2024 23:55:38 -0500 Subject: [PATCH 27/38] Fix authz test --- core/database/foxx/tests/authz.test.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/database/foxx/tests/authz.test.js b/core/database/foxx/tests/authz.test.js index ff59710b2..6a0ac51c6 100644 --- a/core/database/foxx/tests/authz.test.js +++ b/core/database/foxx/tests/authz.test.js @@ -2,7 +2,7 @@ const chai = require("chai"); const expect = chai.expect; -const authzModule = require("./authz"); // Replace with the actual file name +const authzModule = require("../api/authz"); // Replace with the actual file name const g_db = require("@arangodb").db; const g_lib = require("../api/support"); const arangodb = require("@arangodb"); @@ -14,6 +14,7 @@ describe("Authz functions", () => { g_db.alloc.truncate(); g_db.loc.truncate(); g_db.repo.truncate(); + g_db.u.truncate(); }); it("unit_authz: if admin should return true", () => { @@ -28,6 +29,12 @@ describe("Authz functions", () => { }); 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", @@ -37,13 +44,13 @@ describe("Authz functions", () => { g_db.u.save(client); g_db.owner.save({ - _from: data_key, + _from: data_id, _to: owner_id }); let req_perm = g_lib.PERM_CREATE; expect(authzModule.isRecordActionAuthorized(client, data_key, req_perm)).to.be.true; - } + }); }); From 8af94927a5c94566a46e72a3cba286db0caeff7e Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Fri, 20 Dec 2024 00:31:37 -0500 Subject: [PATCH 28/38] Trigger foxx build when change in core database not web server --- .gitlab/build/build_foxx_image.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitlab/build/build_foxx_image.yml b/.gitlab/build/build_foxx_image.yml index 2bd9b67a1..f94204556 100644 --- a/.gitlab/build/build_foxx_image.yml +++ b/.gitlab/build/build_foxx_image.yml @@ -21,7 +21,8 @@ build-foxx: - changes: - docker/**/* - scripts/**/* - - web/**/* + - core/database/**/* + - core/CMakeLists.txt - common/proto/**/* - .gitlab-ci.yml when: on_success @@ -41,7 +42,8 @@ retag-image: - changes: - docker/**/* - scripts/**/* - - web/**/* + - core/database/**/* + - core/CMakeLists.txt - common/proto/**/* - .gitlab-ci.yml when: never From c7f13b6aa83561d7c08205a0485aa935150e7340 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Mon, 30 Dec 2024 16:02:16 -0500 Subject: [PATCH 29/38] Address code review comment from ai --- core/database/foxx/api/authz.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/database/foxx/api/authz.js b/core/database/foxx/api/authz.js index 1d8652b20..4f087de6a 100644 --- a/core/database/foxx/api/authz.js +++ b/core/database/foxx/api/authz.js @@ -5,7 +5,7 @@ const path = require('path'); const g_lib = require('./support'); module.exports = (function() { - var obj = {} + let obj = {} /** * @brief Will check to see if a client has the required permissions on a From 0ada97c719f44bfb6fbdad2c81d49eaa7ab8699e Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Mon, 30 Dec 2024 16:07:19 -0500 Subject: [PATCH 30/38] Add changelog comment --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd95ee1e9..fe3812380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ 15. [1086] - Address XML-RPC bug from deprecated client 16. [1149] - Docker container GCS Collection Mount Bug Fix 17. [1168] - Add authz unit testing to the CI +18. [1180] - Refactor of authz foxx module, split into objects and added unit tests # v2024.6.17.10.40 From a53f9d4fac77477aa2e4d6721ca06e748c61fcad Mon Sep 17 00:00:00 2001 From: Joshua S Brown Date: Tue, 31 Dec 2024 11:47:26 -0500 Subject: [PATCH 31/38] Switch let to const. Co-authored-by: Aaron Perez --- core/database/foxx/api/record.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/database/foxx/api/record.js b/core/database/foxx/api/record.js index 3c9c8b069..196a72065 100644 --- a/core/database/foxx/api/record.js +++ b/core/database/foxx/api/record.js @@ -168,7 +168,7 @@ class Record { // oweners id. // 2. Using the loc.uid parameter if not inflight to get the owner // id. - let new_alloc = g_db.alloc.firstExample({ + const new_alloc = g_db.alloc.firstExample({ _from: this.#loc.new_owner ? this.#loc.new_owner : this.#loc.uid, _to: this.#loc.new_repo, }); From da9608eabf1c32114062be7304bfb380225ac867 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Thu, 2 Jan 2025 08:20:52 -0500 Subject: [PATCH 32/38] Address review feedback --- core/database/foxx/tests/record.test.js | 195 ++++++++---------------- 1 file changed, 65 insertions(+), 130 deletions(-) diff --git a/core/database/foxx/tests/record.test.js b/core/database/foxx/tests/record.test.js index 230f0be15..7a460348f 100644 --- a/core/database/foxx/tests/record.test.js +++ b/core/database/foxx/tests/record.test.js @@ -7,6 +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) { + const record_id = "d/" + record_key; + g_db.d.save({ + _key: record_key, + _id: record_id, + }); + g_db.repo.save({ + _id: repo_id, + }); + + g_db.u.save({ + _id: user_id, + }); +} + describe("Record Class", () => { beforeEach(() => { g_db.d.truncate(); @@ -26,21 +41,12 @@ describe("Record Class", () => { }); it("unit_record: should initialize correctly and check record existence is valid", () => { - const valid_key = "1111"; - const key_id = "d/1111"; + const valid_key = "1120"; + const key_id = "d/" + valid_key; const owner_id = "u/bob"; const repo_id = "repo/datafed-at-com"; // Create nodes - g_db.d.save({ - _key: valid_key, - _id: key_id, - }); - g_db.repo.save({ - _id: repo_id, - }); - g_db.u.save({ - _id: owner_id, - }); + recordRepoAndUserSetup(valid_key, owner_id, repo_id); // Create edges g_db.loc.save({ @@ -60,21 +66,12 @@ describe("Record Class", () => { }); it("unit_record: isManaged should initialize correctly, but show a record as not managed.", () => { - const valid_key = "1111"; - const key_id = "d/1111"; - const owner_id = "u/bob"; - const repo_id = "repo/datafed-at-com"; + const valid_key = "1121"; + const key_id = "d/" + valid_key; + const owner_id = "u/jim"; + const repo_id = "repo/datafed-at-org"; // Create nodes - g_db.d.save({ - _key: valid_key, - _id: key_id, - }); - g_db.repo.save({ - _id: repo_id, - }); - g_db.u.save({ - _id: owner_id, - }); + recordRepoAndUserSetup(valid_key, owner_id, repo_id); const record = new Record(valid_key); expect(record.isManaged()).to.be.false; @@ -86,21 +83,12 @@ describe("Record Class", () => { }); it("unit_record: isManaged should initialize correctly, but without an allocation so should return false", () => { - const valid_key = "1111"; - const key_id = "d/1111"; - const owner_id = "u/bob"; - const repo_id = "repo/datafed-at-com"; + const valid_key = "1122"; + const key_id = "d/" + valid_key; + const owner_id = "u/tom"; + const repo_id = "repo/datafed-banana-com"; // Create nodes - g_db.d.save({ - _key: valid_key, - _id: key_id, - }); - g_db.repo.save({ - _id: repo_id, - }); - g_db.u.save({ - _id: owner_id, - }); + recordRepoAndUserSetup(valid_key, owner_id, repo_id); // Create edges g_db.loc.save({ @@ -117,21 +105,12 @@ describe("Record Class", () => { }); it("unit_record: isManaged should initialize correctly, and with allocation show that record is managed.", () => { - const valid_key = "1111"; - const key_id = "d/1111"; - const owner_id = "u/bob"; - const repo_id = "repo/datafed-at-com"; + const valid_key = "1123"; + const key_id = "d/" + valid_key; + const owner_id = "u/drake"; + const repo_id = "repo/datafed-best-com"; // Create nodes - g_db.d.save({ - _key: valid_key, - _id: key_id, - }); - g_db.repo.save({ - _id: repo_id, - }); - g_db.u.save({ - _id: owner_id, - }); + recordRepoAndUserSetup(valid_key, owner_id, repo_id); // Create edges g_db.loc.save({ @@ -152,21 +131,12 @@ describe("Record Class", () => { }); it("unit_record: isPathConsistent should return false because it is not managed.", () => { - const valid_key = "1111"; - const key_id = "d/1111"; - const owner_id = "u/bob"; - const repo_id = "repo/datafed-at-com"; + const valid_key = "1124"; + const key_id = "d/" + valid_key; + const owner_id = "u/carl"; + const repo_id = "repo/datafed-at-super"; // Create nodes - g_db.d.save({ - _key: valid_key, - _id: key_id, - }); - g_db.repo.save({ - _id: repo_id, - }); - g_db.u.save({ - _id: owner_id, - }); + recordRepoAndUserSetup(valid_key, owner_id, repo_id); // Create edges g_db.loc.save({ @@ -180,22 +150,13 @@ describe("Record Class", () => { }); it("unit_record: isPathConsistent should return false paths are not consistent.", () => { - const valid_key = "1111"; - const key_id = "d/1111"; - const owner_id = "u/bob"; - const repo_id = "repo/datafed-at-com"; + const valid_key = "1125"; + const key_id = "d/" + valid_key; + const owner_id = "u/red"; + const repo_id = "repo/datafed-fine-com"; // Create nodes - g_db.d.save({ - _key: valid_key, - _id: key_id, - }); - g_db.repo.save({ - _id: repo_id, - }); - g_db.u.save({ - _id: owner_id, - }); - + recordRepoAndUserSetup(valid_key, owner_id, repo_id); + // Create edges g_db.loc.save({ _from: key_id, @@ -217,22 +178,13 @@ describe("Record Class", () => { }); it("unit_record: isPathConsistent should return true paths are consistent.", () => { - const valid_key = "1111"; - const key_id = "d/1111"; - const owner_id = "u/bob"; - const repo_id = "repo/datafed-at-com"; + const valid_key = "1126"; + const key_id = "d/" + valid_key; + const owner_id = "u/karen"; + const repo_id = "repo/datafed-cool-com"; // Create nodes - g_db.d.save({ - _key: valid_key, - _id: key_id, - }); - g_db.repo.save({ - _id: repo_id, - }); - g_db.u.save({ - _id: owner_id, - }); - + recordRepoAndUserSetup(valid_key, owner_id, repo_id); + // Create edges g_db.loc.save({ _from: key_id, @@ -253,29 +205,21 @@ describe("Record Class", () => { }); it("unit_record: isPathConsistent should return true paths are inconsistent, but new path in new alloc is valid.", () => { - const valid_key = "1111"; - const key_id = "d/1111"; - const owner_id = "u/bob"; - const repo_id = "repo/datafed-at-com"; - const new_repo_id = "repo/datafed-at-org"; + const valid_key = "1127"; + const key_id = "d/" + valid_key; + const owner_id = "u/john"; + const repo_id = "repo/orange-at-com"; + const new_repo_id = "repo/watermelon-at-org"; + // Create nodes - g_db.d.save({ - _key: valid_key, - _id: key_id, - }); - g_db.repo.save({ - _id: repo_id, - }); - g_db.u.save({ - _id: owner_id, - }); + recordRepoAndUserSetup(valid_key, owner_id, repo_id); // Create edges g_db.loc.save({ _from: key_id, _to: repo_id, uid: owner_id, - new_repo: "repo/datafed-at-org", + new_repo: new_repo_id, }); g_db.alloc.save({ _from: owner_id, @@ -296,29 +240,20 @@ describe("Record Class", () => { }); it("unit_record: isPathConsistent should return false paths are inconsistent in new and old alloc.", () => { - const valid_key = "1111"; - const key_id = "d/1111"; - const owner_id = "u/bob"; - const repo_id = "repo/datafed-at-com"; - const new_repo_id = "repo/datafed-at-org"; + 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 - g_db.d.save({ - _key: valid_key, - _id: key_id, - }); - g_db.repo.save({ - _id: repo_id, - }); - g_db.u.save({ - _id: owner_id, - }); + recordRepoAndUserSetup(valid_key, owner_id, repo_id); // Create edges g_db.loc.save({ _from: key_id, _to: repo_id, uid: owner_id, - new_repo: "repo/datafed-at-org", + new_repo: new_repo_id, }); g_db.alloc.save({ _from: owner_id, From 88be445262ed97a9a03ca1f054e88a04012bea08 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Thu, 2 Jan 2025 16:31:51 -0500 Subject: [PATCH 33/38] Add better comments around database method --- core/database/foxx/api/support.js | 47 ++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/core/database/foxx/api/support.js b/core/database/foxx/api/support.js index 4c8ec0a99..d24ba8f6d 100644 --- a/core/database/foxx/api/support.js +++ b/core/database/foxx/api/support.js @@ -595,20 +595,39 @@ module.exports = (function() { return first_uuid; } - // The returned object 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" - // } + /** + * Retrieves user information based on the provided client ID. + * + * The return value should be a client containing 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" + * } + * + * The client ID can be in the following formats: + * - SDMS uname (e.g., "xxxxx...") + * - UUID (e.g., "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") + * - Account (e.g., "domain.uname") + * + * UUIDs are defined by length and format, accounts have a "." (and known domains), + * and SDMS unames have no "." or "-" characters. + * + * @param {string} a_client_id - The client ID, which can be in various formats (SDMS uname, UUID, or Account). + * @throws {Array} Throws an error if the user does not exist, or the client ID is invalid. + * @returns {Object} The user record containing details such as name, admin status, and email. + * + * @example + * const user = obj.getUserFromClientID('u/bob'); + * console.log(user.name); // "bob junior" + */ obj.getUserFromClientID = function(a_client_id) { // Client ID can be an SDMS uname (xxxxx...), a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), or an account (domain.uname) // UUID are defined by length and format, accounts have a "." (and known domains), SDMS unames have no "." or "-" characters From 9d3622b72e30fcd9ff2254f1cd563dcf439bae01 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Thu, 2 Jan 2025 16:39:26 -0500 Subject: [PATCH 34/38] Apply formatting --- core/database/foxx/api/authz_router.js | 360 +++-- core/database/foxx/api/support.js | 1855 ++++++++++++++---------- 2 files changed, 1270 insertions(+), 945 deletions(-) diff --git a/core/database/foxx/api/authz_router.js b/core/database/foxx/api/authz_router.js index e354252aa..cf68e834a 100644 --- a/core/database/foxx/api/authz_router.js +++ b/core/database/foxx/api/authz_router.js @@ -12,130 +12,169 @@ const authzModule = require("./authz"); // Replace with the actual file name module.exports = router; router - .get("/gridftp", function (req, res) { - try { - console.log("/gridftp start authz client", req.queryParams.client, - "repo", req.queryParams.repo, - "file", req.queryParams.file, - "act", req.queryParams.act - ); - - // 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 - if (!client) { - if (record.exists()) { - if (req.queryParams.act != "read" || !g_lib.hasPublicRead(record.id())) { + .get("/gridftp", function (req, res) { + try { console.log( - "AUTHZ act: " + req.queryParams.act + - " client: " + client._id + - " path " + req.queryParams.file + - " FAILED" + "/gridftp start authz client", + req.queryParams.client, + "repo", + req.queryParams.repo, + "file", + req.queryParams.file, + "act", + req.queryParams.act, ); - 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)) { + // 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 + 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; + } + } + } + + 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. + console.log( + "AUTHZ act: " + + req.queryParams.act + + " client: " + + client._id + + " path " + + req.queryParams.file + + " FAILED", + ); + throw [g_lib.ERR_PERM_DENIED, "Invalid record specified: " + req.queryParams.file]; + } console.log( - "AUTHZ act: " + req.queryParams.act + - " client: " + client._id + - " path " + req.queryParams.file + - " FAILED" + "AUTHZ act: " + + req.queryParams.act + + " client: " + + client._id + + " path " + + req.queryParams.file + + " SUCCESS", ); - throw g_lib.ERR_PERM_DENIED; - } - } - } - - 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()]; + } catch (e) { + g_lib.handleException(e, res); } - } else { - // If the record does not exist then the path would noe be consistent. - console.log( - "AUTHZ act: " + req.queryParams.act + " client: " + client._id + " path " + req.queryParams.file + " FAILED" - ); - throw [g_lib.ERR_PERM_DENIED, "Invalid record specified: " + req.queryParams.file]; - } - console.log( - "AUTHZ act: " + req.queryParams.act + " client: " + client._id + " path " + req.queryParams.file + " SUCCESS" - ); - } catch (e) { - g_lib.handleException(e, res); - } - }) - .queryParam("client", joi.string().required(), "Client ID") - .queryParam( - "repo", - joi.string().required(), - "Originating repo ID, where the DataFed managed GridFTP server is running." - ) - .queryParam("file", joi.string().required(), "Data file name") - .queryParam("act", joi.string().required(), "GridFTP action: 'lookup', 'chdir', 'read', 'write', 'create', 'delete'") - .summary("Checks authorization") - .description("Checks authorization"); - -router.get('/perm/check', function(req, res) { + }) + .queryParam("client", joi.string().required(), "Client ID") + .queryParam( + "repo", + joi.string().required(), + "Originating repo ID, where the DataFed managed GridFTP server is running.", + ) + .queryParam("file", joi.string().required(), "Data file name") + .queryParam( + "act", + joi.string().required(), + "GridFTP action: 'lookup', 'chdir', 'read', 'write', 'create', 'delete'", + ) + .summary("Checks authorization") + .description("Checks authorization"); + +router + .get("/perm/check", function (req, res) { try { const client = g_lib.getUserFromClientID(req.queryParams.client); var perms = req.queryParams.perms ? req.queryParams.perms : g_lib.PERM_ALL; - var obj, result = true, + var obj, + result = true, id = g_lib.resolveID(req.queryParams.id, client), ty = id[0]; @@ -145,34 +184,37 @@ router.get('/perm/check', function(req, res) { if (ty == "p") { var role = g_lib.getProjectRole(client._id, id); - if (role == g_lib.PROJ_NO_ROLE) { // Non members have only VIEW permissions - if (perms != g_lib.PERM_RD_REC) - result = false; - } else if (role == g_lib.PROJ_MEMBER) { // Non members have only VIEW permissions - if ((perms & ~g_lib.PERM_MEMBER) != 0) - result = false; - } else if (role == g_lib.PROJ_MANAGER) { // Managers have all but UPDATE - if ((perms & ~g_lib.PERM_MANAGER) != 0) - result = false; + if (role == g_lib.PROJ_NO_ROLE) { + // Non members have only VIEW permissions + if (perms != g_lib.PERM_RD_REC) result = false; + } else if (role == g_lib.PROJ_MEMBER) { + // Non members have only VIEW permissions + if ((perms & ~g_lib.PERM_MEMBER) != 0) result = false; + } else if (role == g_lib.PROJ_MANAGER) { + // Managers have all but UPDATE + if ((perms & ~g_lib.PERM_MANAGER) != 0) result = false; } } else if (ty == "d") { if (!g_lib.hasAdminPermObject(client, id)) { obj = g_db.d.document(id); - if (obj.locked) - result = false; - else - result = g_lib.hasPermissions(client, obj, perms); + if (obj.locked) result = false; + else result = g_lib.hasPermissions(client, obj, perms); } } else if (ty == "c") { // If create perm is requested, ensure owner of collection has at least one allocation if (perms & g_lib.PERM_CREATE) { var owner = g_db.owner.firstExample({ - _from: id + _from: id, }); - if (!g_db.alloc.firstExample({ - _from: owner._to - })) { - throw [g_lib.ERR_NO_ALLOCATION, "An allocation is required to create a collection."]; + if ( + !g_db.alloc.firstExample({ + _from: owner._to, + }) + ) { + throw [ + g_lib.ERR_NO_ALLOCATION, + "An allocation is required to create a collection.", + ]; } } @@ -185,64 +227,64 @@ router.get('/perm/check', function(req, res) { } res.send({ - granted: result + granted: result, }); } catch (e) { g_lib.handleException(e, res); } }) - .queryParam('client', joi.string().required(), "Client ID") - .queryParam('id', joi.string().required(), "Object ID or alias") - .queryParam('perms', joi.number().required(), "Permission bit mask to check") - .summary('Checks client permissions for object') - .description('Checks client permissions for object (projects, data, collections'); + .queryParam("client", joi.string().required(), "Client ID") + .queryParam("id", joi.string().required(), "Object ID or alias") + .queryParam("perms", joi.number().required(), "Permission bit mask to check") + .summary("Checks client permissions for object") + .description("Checks client permissions for object (projects, data, collections"); -router.get('/perm/get', function(req, res) { +router + .get("/perm/get", function (req, res) { try { const client = g_lib.getUserFromClientID(req.queryParams.client); var result = req.queryParams.perms ? req.queryParams.perms : g_lib.PERM_ALL; - var obj, id = g_lib.resolveID(req.queryParams.id, client), + var obj, + id = g_lib.resolveID(req.queryParams.id, client), ty = id[0]; - if (id[1] != "/") - throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; + if (id[1] != "/") throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; if (ty == "p") { var role = g_lib.getProjectRole(client._id, id); - if (role == g_lib.PROJ_NO_ROLE) { // Non members have only VIEW permissions + if (role == g_lib.PROJ_NO_ROLE) { + // Non members have only VIEW permissions result &= g_lib.PERM_RD_REC; } else if (role == g_lib.PROJ_MEMBER) { result &= g_lib.PERM_MEMBER; - } else if (role == g_lib.PROJ_MANAGER) { // Managers have all but UPDATE + } else if (role == g_lib.PROJ_MANAGER) { + // Managers have all but UPDATE result &= g_lib.PERM_MANAGER; } } else if (ty == "d") { if (!g_lib.hasAdminPermObject(client, id)) { obj = g_db.d.document(id); - if (obj.locked) - result = 0; - else - result = g_lib.getPermissions(client, obj, result); + if (obj.locked) result = 0; + else result = g_lib.getPermissions(client, obj, result); } } else if (ty == "c") { if (!g_lib.hasAdminPermObject(client, id)) { obj = g_db.c.document(id); result = g_lib.getPermissions(client, obj, result); } - } else - throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; + } else throw [g_lib.ERR_INVALID_PARAM, "Invalid ID, " + req.queryParams.id]; res.send({ - granted: result + granted: result, }); } catch (e) { g_lib.handleException(e, res); } }) - .queryParam('client', joi.string().required(), "Client ID") - .queryParam('id', joi.string().required(), "Object ID or alias") - .queryParam('perms', joi.number().optional(), "Permission bit mask to get (default = all)") - .summary('Gets client permissions for object') - .description('Gets client permissions for object (projects, data, collections. Note this is potentially slower than using "check" method.'); - - + .queryParam("client", joi.string().required(), "Client ID") + .queryParam("id", joi.string().required(), "Object ID or alias") + .queryParam("perms", joi.number().optional(), "Permission bit mask to get (default = all)") + .summary("Gets client permissions for object") + .description( + 'Gets client permissions for object (projects, data, collections. Note this is potentially slower than using "check" method.', + ); diff --git a/core/database/foxx/api/support.js b/core/database/foxx/api/support.js index d24ba8f6d..0441f860b 100644 --- a/core/database/foxx/api/support.js +++ b/core/database/foxx/api/support.js @@ -1,13 +1,12 @@ -'use strict'; +"use strict"; -const joi = require('joi'); +const joi = require("joi"); - -module.exports = (function() { +module.exports = (function () { var obj = {}; - obj.db = require('@arangodb').db; - obj.graph = require('@arangodb/general-graph')._graph('sdmsg'); + obj.db = require("@arangodb").db; + obj.graph = require("@arangodb/general-graph")._graph("sdmsg"); obj.PERM_RD_REC = 0x0001; // Read record info (description, keywords, details) obj.PERM_RD_META = 0x0002; // Read structured metadata @@ -28,7 +27,7 @@ module.exports = (function() { obj.PERM_NONE = 0x0000; obj.PERM_RD_ALL = 0x0007; // Read all obj.PERM_WR_ALL = 0x0038; // Write all - obj.PERM_ALL = 0x7FFF; + obj.PERM_ALL = 0x7fff; obj.PERM_MEMBER = 0x0047; // Project record perms obj.PERM_MANAGER = 0x0407; // Project record perms obj.PERM_PUBLIC = 0x0047; @@ -122,14 +121,14 @@ module.exports = (function() { obj.NOTE_MASK_INH_WARN = 0x0400; // Questions & info are not inherited obj.NOTE_MASK_INH_ERR = 0x0800; obj.NOTE_MASK_CLS_ANY = 0x1000; - obj.NOTE_MASK_LOC_ALL = 0x00FF; - obj.NOTE_MASK_INH_ALL = 0x0C00; + obj.NOTE_MASK_LOC_ALL = 0x00ff; + obj.NOTE_MASK_INH_ALL = 0x0c00; obj.NOTE_MASK_MD_ERR = 0x2000; obj.acl_schema = joi.object().keys({ id: joi.string().required(), grant: joi.number().optional(), - inhgrant: joi.number().optional() + inhgrant: joi.number().optional(), }); obj.ERR_INFO = []; @@ -166,7 +165,6 @@ module.exports = (function() { obj.ERR_ALLOCATION_EXCEEDED = obj.ERR_COUNT++; obj.ERR_INFO.push([400, "Storage allocation exceeded"]); - obj.CHARSET_ID = 0; obj.CHARSET_ALIAS = 1; obj.CHARSET_TOPIC = 2; @@ -182,7 +180,7 @@ module.exports = (function() { required: true, update: true, max_len: 80, - label: 'title' + label: "title", }, alias: { required: false, @@ -190,13 +188,13 @@ module.exports = (function() { max_len: 40, lower: true, charset: obj.CHARSET_ALIAS, - label: 'alias' + label: "alias", }, desc: { required: false, update: true, max_len: 2000, - label: 'description' + label: "description", }, summary: { required: false, @@ -204,7 +202,7 @@ module.exports = (function() { max_len: 500, in_field: "desc", out_field: "desc", - label: 'description' + label: "description", }, comment: { required: true, @@ -212,7 +210,7 @@ module.exports = (function() { max_len: 2000, in_field: "comment", out_field: "comment", - label: 'comment' + label: "comment", }, topic: { required: false, @@ -220,7 +218,7 @@ module.exports = (function() { max_len: 500, lower: true, charset: obj.CHARSET_TOPIC, - label: 'topic' + label: "topic", }, domain: { required: false, @@ -228,21 +226,21 @@ module.exports = (function() { max_len: 40, lower: true, charset: obj.CHARSET_ID, - label: 'domain' + label: "domain", }, source: { required: false, update: true, max_len: 4096, lower: false, - label: 'source' + label: "source", }, ext: { required: false, update: true, max_len: 40, lower: false, - label: 'extension' + label: "extension", }, gid: { required: true, @@ -250,7 +248,7 @@ module.exports = (function() { max_len: 40, lower: true, charset: obj.CHARSET_ID, - label: 'group ID' + label: "group ID", }, id: { required: true, @@ -259,7 +257,7 @@ module.exports = (function() { lower: true, charset: obj.CHARSET_ID, out_field: "_key", - label: 'ID' + label: "ID", }, doi: { required: false, @@ -267,7 +265,7 @@ module.exports = (function() { max_len: 40, lower: true, charset: obj.CHARSET_DOI, - label: 'doi' + label: "doi", }, data_url: { required: false, @@ -275,7 +273,7 @@ module.exports = (function() { max_len: 200, lower: false, charset: obj.CHARSET_URL, - label: 'data URL' + label: "data URL", }, sch_id: { required: false, @@ -283,7 +281,7 @@ module.exports = (function() { max_len: 120, lower: true, charset: obj.CHARSET_SCH_ID, - label: 'schema' + label: "schema", }, _sch_id: { required: true, @@ -293,8 +291,8 @@ module.exports = (function() { charset: obj.CHARSET_SCH_ID, in_field: "id", out_field: "id", - label: 'schema' - } + label: "schema", + }, }; obj.DEF_MAX_COLL = 50; @@ -304,21 +302,24 @@ module.exports = (function() { obj.GLOB_MAX_XFR_SIZE = 10000000000; // ~10GB //obj.GLOB_MAX_XFR_SIZE = 2000000; - obj.procInputParam = function(a_in, a_field, a_update, a_out) { - var val, spec = obj.field_reqs[a_field]; + obj.procInputParam = function (a_in, a_field, a_update, a_out) { + var val, + spec = obj.field_reqs[a_field]; //console.log("procInput",a_field,",update:",a_update); if (!spec) { - throw [obj.ERR_INTERNAL_FAULT, "Input specification for '" + a_field + "' not found. Please contact system administrator."]; + throw [ + obj.ERR_INTERNAL_FAULT, + "Input specification for '" + + a_field + + "' not found. Please contact system administrator.", + ]; } - if (typeof a_in == "string") - val = a_in; - else if (spec.in_field) - val = a_in[spec.in_field]; - else - val = a_in[a_field]; + if (typeof a_in == "string") val = a_in; + else if (spec.in_field) val = a_in[spec.in_field]; + else val = a_in[a_field]; //console.log("init val",val); @@ -328,16 +329,21 @@ module.exports = (function() { return; } - if (val && val.length) - val = val.trim(); + if (val && val.length) val = val.trim(); if (val && val.length) { // Check length if specified - if (spec.max_len && (val.length > spec.max_len)) - throw [obj.ERR_INPUT_TOO_LONG, "'" + spec.label + "' field is too long. Maximum length is " + spec.max_len + "."]; - - if (spec.lower) - val = val.toLowerCase(); + if (spec.max_len && val.length > spec.max_len) + throw [ + obj.ERR_INPUT_TOO_LONG, + "'" + + spec.label + + "' field is too long. Maximum length is " + + spec.max_len + + ".", + ]; + + if (spec.lower) val = val.toLowerCase(); if (spec.charset != undefined) { var extra = obj.extra_chars[spec.charset]; @@ -345,51 +351,62 @@ module.exports = (function() { for (i = 0, len = val.length; i < len; i++) { code = val.charCodeAt(i); - if (!(code > 47 && code < 58) && // numeric (0-9) + if ( + !(code > 47 && code < 58) && // numeric (0-9) !(code > 64 && code < 91) && // upper alpha (A-Z) - !(code > 96 && code < 123)) { // lower alpha (a-z) + !(code > 96 && code < 123) + ) { + // lower alpha (a-z) if (extra.indexOf(val.charAt(i)) == -1) - throw [obj.ERR_INVALID_CHAR, "Invalid character(s) in '" + spec.label + "' field."]; + throw [ + obj.ERR_INVALID_CHAR, + "Invalid character(s) in '" + spec.label + "' field.", + ]; } } } //console.log("save new val:",val); - if (spec.out_field) - a_out[spec.out_field] = val; - else - a_out[a_field] = val; + if (spec.out_field) a_out[spec.out_field] = val; + else a_out[a_field] = val; } else { // Required params must have a value if (a_update) { if (val === "") { if (spec.required) - throw [obj.ERR_MISSING_REQ_PARAM, "Required field '" + spec.label + "' cannot be deleted."]; + throw [ + obj.ERR_MISSING_REQ_PARAM, + "Required field '" + spec.label + "' cannot be deleted.", + ]; - if (spec.out_field) - a_out[spec.out_field] = null; - else - a_out[a_field] = null; + if (spec.out_field) a_out[spec.out_field] = null; + else a_out[a_field] = null; } } else if (spec.required) throw [obj.ERR_MISSING_REQ_PARAM, "Missing required field '" + spec.label + "'."]; } }; - obj.isInteger = function(x) { - return (typeof x === 'number') && (x % 1 === 0); + obj.isInteger = function (x) { + return typeof x === "number" && x % 1 === 0; }; - obj.validatePassword = function(pw) { + obj.validatePassword = function (pw) { if (pw.length < obj.PASSWORD_MIN_LEN) { - throw [obj.ERR_INVALID_PARAM, "ERROR: password must be at least " + obj.PASSWORD_MIN_LEN + " characters in length."]; + throw [ + obj.ERR_INVALID_PARAM, + "ERROR: password must be at least " + + obj.PASSWORD_MIN_LEN + + " characters in length.", + ]; } - var i, j = 0, + var i, + j = 0, c; for (i in pw) { c = pw[i]; - if (c >= '0' && c <= '9') { + if (c >= "0" && c <= "9") { j |= 1; } else if (obj.pw_chars.indexOf(c) != -1) { j |= 2; @@ -400,9 +417,14 @@ module.exports = (function() { } if (j != 3) { - throw [obj.ERR_INVALID_PARAM, "ERROR: password must contain at least one number (0-9) and one special character (" + obj.pw_chars + ")."]; + throw [ + obj.ERR_INVALID_PARAM, + "ERROR: password must contain at least one number (0-9) and one special character (" + + obj.pw_chars + + ").", + ]; } - } + }; /* obj.isAlphaNumeric = function(str) { @@ -419,7 +441,7 @@ module.exports = (function() { return true; };*/ - obj.handleException = function(e, res) { + obj.handleException = function (e, res) { console.log("Service exception:", e); if (obj.isInteger(e) && e >= 0 && e < obj.ERR_COUNT) { @@ -451,7 +473,7 @@ module.exports = (function() { } }; - obj.isDomainAccount = function(a_client_id) { + obj.isDomainAccount = function (a_client_id) { // TODO This needs to have a test that doesn't conflict with email-style uids /*if ( a_client_id.indexOf( "." ) != -1 ) @@ -461,18 +483,16 @@ module.exports = (function() { }; // Quick check to determine if ID looks like a UUID (does not verify) - obj.isUUID = function(a_client_id) { - if (a_client_id.length == 36 && a_client_id.charAt(8) == "-") - return true; - else - return false; + obj.isUUID = function (a_client_id) { + if (a_client_id.length == 36 && a_client_id.charAt(8) == "-") return true; + else return false; }; // Quick check to see if a comma separated list of UUIDs has been provided // Must contain at least one comma - obj.isUUIDList = function(a_client_ids) { - if (a_client_ids.indexOf(',') > -1) { - var potential_uuids = a_client_ids.split(',') + obj.isUUIDList = function (a_client_ids) { + if (a_client_ids.indexOf(",") > -1) { + var potential_uuids = a_client_ids.split(","); for (var index in potential_uuids) { if (!obj.isUUID(potential_uuids[index])) { return false; @@ -485,7 +505,7 @@ module.exports = (function() { }; // Verify a_is is a valid UUID AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA (full check) - obj.isValidUUID = function(a_id) { + obj.isValidUUID = function (a_id) { if (a_id.length == 36) { var code; for (var i = 0; i < 36; i++) { @@ -495,9 +515,12 @@ module.exports = (function() { } } else { code = a_id.charCodeAt(i); - if (!(code > 47 && code < 58) && // numeric (0-9) + if ( + !(code > 47 && code < 58) && // numeric (0-9) !(code > 64 && code < 71) && // upper alpha (A-F) - !(code > 96 && code < 103)) { // lower alpha (a-F) + !(code > 96 && code < 103) + ) { + // lower alpha (a-F) return false; } } @@ -509,7 +532,7 @@ module.exports = (function() { return false; }; - obj.isFullGlobusPath = function(a_path, a_is_file = true) { + obj.isFullGlobusPath = function (a_path, a_is_file = true) { // Full Globus path can be UUID/ or legacy#name/ var idx = a_path.indexOf("/"); if (idx > 0) { @@ -538,53 +561,62 @@ module.exports = (function() { return false; }; - - obj.resolveUUIDsToID = function(uuid_list) { + obj.resolveUUIDsToID = function (uuid_list) { // This function will take a comma separated list of uuids as a string separate them and then either resolve them to a single uuid or // throw an error - var potential_uuids = uuid_list.split(',') + var potential_uuids = uuid_list.split(","); var uuids = []; for (var i in potential_uuids) { uuids.push("uuid/" + potential_uuids[i]); } - console.log("resolveUUIDsToID"); - console.log("uuids: ", uuids); - var result = obj.db._query("for i in ident filter i._to in @ids return distinct document(i._from)", { - ids: uuids - }).toArray(); + console.log("resolveUUIDsToID"); + console.log("uuids: ", uuids); + var result = obj.db + ._query("for i in ident filter i._to in @ids return distinct document(i._from)", { + ids: uuids, + }) + .toArray(); if (result.length !== 1) { throw [obj.ERR_NOT_FOUND, "No user matching Globus IDs found"]; } - var first_uuid = result[0]._id + var first_uuid = result[0]._id; // Next we need to make sure the provided ids are all the same if there is more than one for (var i = 1; i < result.length; i++) { if (first_uuid != result[i]._id) { - throw [obj.ERR_INVALID_PARAM, "uuid_list does not resolve to a single user, unable to unambiguously resolve user, it is possible that you have multiple accounts when you should have only a single one problematic ids are: " + first_uuid + " and " + array[i]]; + throw [ + obj.ERR_INVALID_PARAM, + "uuid_list does not resolve to a single user, unable to unambiguously resolve user, it is possible that you have multiple accounts when you should have only a single one problematic ids are: " + + first_uuid + + " and " + + array[i], + ]; } } return first_uuid; - } + }; - obj.resolveUUIDsToID_noexcept = function(uuid_list) { + obj.resolveUUIDsToID_noexcept = function (uuid_list) { // This function will take a comma separated list of uuids as a string separate them and then either resolve them to a single uuid or // throw an error - var potential_uuids = uuid_list.split(',') + var potential_uuids = uuid_list.split(","); var uuids = []; for (var i in potential_uuids) { uuids.push("uuid/" + potential_uuids[i]); } - console.log("resolveUUIDsToID_noexcept"); - console.log("uuids: ", uuids); - var result = obj.db._query("for i in ident filter i._to in @ids return distinct document(i._from)", { - ids: uuids - }).toArray(); + console.log("resolveUUIDsToID_noexcept"); + console.log("uuids: ", uuids); + var result = obj.db + ._query("for i in ident filter i._to in @ids return distinct document(i._from)", { + ids: uuids, + }) + .toArray(); if (result.length != 1) { throw [obj.ERR_NOT_FOUND, "No user matching Globus IDs found"]; } - var first_uuid = result[0]._id + var first_uuid = result[0]._id; // Next we need to make sure the provided ids are all the same if there is more than one for (var i = 1; i < result.length; i++) { console.log("resolveUUID comparing " + first_uuid + " with " + result[i]); @@ -593,42 +625,42 @@ module.exports = (function() { } } return first_uuid; - } + }; - /** - * Retrieves user information based on the provided client ID. - * - * The return value should be a client containing 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" - * } - * - * The client ID can be in the following formats: - * - SDMS uname (e.g., "xxxxx...") - * - UUID (e.g., "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") - * - Account (e.g., "domain.uname") - * - * UUIDs are defined by length and format, accounts have a "." (and known domains), - * and SDMS unames have no "." or "-" characters. - * - * @param {string} a_client_id - The client ID, which can be in various formats (SDMS uname, UUID, or Account). - * @throws {Array} Throws an error if the user does not exist, or the client ID is invalid. - * @returns {Object} The user record containing details such as name, admin status, and email. - * - * @example - * const user = obj.getUserFromClientID('u/bob'); - * console.log(user.name); // "bob junior" - */ - obj.getUserFromClientID = function(a_client_id) { + /** + * Retrieves user information based on the provided client ID. + * + * The return value should be a client containing 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" + * } + * + * The client ID can be in the following formats: + * - SDMS uname (e.g., "xxxxx...") + * - UUID (e.g., "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") + * - Account (e.g., "domain.uname") + * + * UUIDs are defined by length and format, accounts have a "." (and known domains), + * and SDMS unames have no "." or "-" characters. + * + * @param {string} a_client_id - The client ID, which can be in various formats (SDMS uname, UUID, or Account). + * @throws {Array} Throws an error if the user does not exist, or the client ID is invalid. + * @returns {Object} The user record containing details such as name, admin status, and email. + * + * @example + * const user = obj.getUserFromClientID('u/bob'); + * console.log(user.name); // "bob junior" + */ + obj.getUserFromClientID = function (a_client_id) { // Client ID can be an SDMS uname (xxxxx...), a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), or an account (domain.uname) // UUID are defined by length and format, accounts have a "." (and known domains), SDMS unames have no "." or "-" characters @@ -638,24 +670,24 @@ module.exports = (function() { if (a_client_id.startsWith("u/")) { if (!obj.db.u.exists(a_client_id)) { throw [obj.ERR_INVALID_PARAM, "No such user '" + a_client_id + "'"]; - } + } return obj.db._document({ - _id: a_client_id + _id: a_client_id, }); } else if (obj.isDomainAccount(a_client_id)) { // Account params = { - 'id': 'accn/' + a_client_id + id: "accn/" + a_client_id, }; } else if (obj.isUUID(a_client_id)) { // UUID params = { - 'id': 'uuid/' + a_client_id + id: "uuid/" + a_client_id, }; } else if (obj.isUUIDList(a_client_id)) { // Check to make sure the UUIDs provided are all associated with the same DataFed account, if they are we can unambiguously - // determine the UUID, if they are not, then we will throw an error for now, + // determine the UUID, if they are not, then we will throw an error for now, var unambiguous_id = obj.resolveUUIDsToID(a_client_id); if (!unambiguous_id) { console.log("Undefined"); @@ -663,22 +695,22 @@ module.exports = (function() { } //params = { 'id': unambiguous_id }; return obj.db._document({ - _id: unambiguous_id + _id: unambiguous_id, }); - - } else { if (!obj.db.u.exists("u/" + a_client_id)) { throw [obj.ERR_INVALID_PARAM, "No such user 'u/" + a_client_id + "'"]; } return obj.db._document({ - _id: "u/" + a_client_id + _id: "u/" + a_client_id, }); } - var result = obj.db._query("for j in inbound @id ident return j", params, { - cache: true - }).toArray(); + var result = obj.db + ._query("for j in inbound @id ident return j", params, { + cache: true, + }) + .toArray(); if (result.length != 1) { //console.log("Client", a_client_id, "not found, params:", params ); @@ -688,32 +720,31 @@ module.exports = (function() { return result[0]; }; - obj.getUserFromClientID_noexcept = function(a_client_id) { + obj.getUserFromClientID_noexcept = function (a_client_id) { // Client ID can be an SDMS uname (xxxxx...), a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), or an account (domain.uname) // UUID are defined by length and format, accounts have a "." (and known domains), SDMS unames have no "." or "-" characters var params; if (a_client_id.startsWith("u/")) { - if (!obj.db.u.exists(a_client_id)) - return; + if (!obj.db.u.exists(a_client_id)) return; return obj.db._document({ - _id: a_client_id + _id: a_client_id, }); } else if (obj.isDomainAccount(a_client_id)) { // Account params = { - 'id': 'accn/' + a_client_id + id: "accn/" + a_client_id, }; } else if (obj.isUUID(a_client_id)) { // UUID params = { - 'id': 'uuid/' + a_client_id + id: "uuid/" + a_client_id, }; } else if (obj.isUUIDList(a_client_id)) { // Check to make sure the UUIDs provided are all associated with the same DataFed account, if they are we can unambiguously - // determine the UUID, if they are not, then we will throw an error for now, + // determine the UUID, if they are not, then we will throw an error for now, var unambiguous_id = obj.resolveUUIDsToID_noexcept(a_client_id); if (!unambiguous_id) { console.log("Undefined"); @@ -721,21 +752,21 @@ module.exports = (function() { } //params = { 'id': unambiguous_id }; return obj.db._document({ - _id: unambiguous_id + _id: unambiguous_id, }); - } else { - if (!obj.db.u.exists("u/" + a_client_id)) - return; + if (!obj.db.u.exists("u/" + a_client_id)) return; return obj.db._document({ - _id: "u/" + a_client_id + _id: "u/" + a_client_id, }); } - var result = obj.db._query("for j in inbound @id ident return j", params, { - cache: true - }).toArray(); + var result = obj.db + ._query("for j in inbound @id ident return j", params, { + cache: true, + }) + .toArray(); if (result.length != 1) { return; @@ -744,37 +775,50 @@ module.exports = (function() { return result[0]; }; - obj.findUserFromUUIDs = function(a_uuids) { - console.log("findUserFromUUIDs"); - console.log("a_uuids: ", a_uuids); - var result = obj.db._query("for i in ident filter i._to in @ids return distinct document(i._from)", { - ids: a_uuids - }).toArray(); + obj.findUserFromUUIDs = function (a_uuids) { + console.log("findUserFromUUIDs"); + console.log("a_uuids: ", a_uuids); + var result = obj.db + ._query("for i in ident filter i._to in @ids return distinct document(i._from)", { + ids: a_uuids, + }) + .toArray(); if (result.length === 0) { throw [obj.ERR_NOT_FOUND, "No user matching Globus IDs found"]; - } else if (result.length > 1) { - throw [obj.ERR_NOT_FOUND, "Multiple DataFed accounts associated with the provided Globus identities" + result.toString() ]; - } + } else if (result.length > 1) { + throw [ + obj.ERR_NOT_FOUND, + "Multiple DataFed accounts associated with the provided Globus identities" + + result.toString(), + ]; + } return result[0]; }; - obj.uidFromPubKey = function(a_pub_key) { + obj.uidFromPubKey = function (a_pub_key) { //var result = obj.db._query( "for i in accn filter i.pub_key == @key let u = (for v in inbound i._id ident return v._key) return u[0]", { key: a_pub_key }).toArray(); - var result = obj.db._query("for i in u filter i.pub_key == @key return i._id", { - key: a_pub_key - }).toArray(); + var result = obj.db + ._query("for i in u filter i.pub_key == @key return i._id", { + key: a_pub_key, + }) + .toArray(); if (result.length !== 1) throw [obj.ERR_NOT_FOUND, "No user matching authentication key found"]; return result[0]; }; - obj.findUserFromPubKey = function(a_pub_key) { - var result = obj.db._query("for i in accn filter i.pub_key == @key let u = (for v in inbound i._id ident return v) return u[0]", { - key: a_pub_key - }).toArray(); + obj.findUserFromPubKey = function (a_pub_key) { + var result = obj.db + ._query( + "for i in accn filter i.pub_key == @key let u = (for v in inbound i._id ident return v) return u[0]", + { + key: a_pub_key, + }, + ) + .toArray(); //console.log( "key res:", result ); if (result.length != 1) @@ -783,62 +827,66 @@ module.exports = (function() { return result[0]; }; - obj.getAccessToken = function(a_user_id) { + obj.getAccessToken = function (a_user_id) { var user = obj.db.u.document(a_user_id); var exp_in = user.expiration - Math.floor(Date.now() / 1000); var result = { acc_tok: user.access, ref_tok: user.refresh, - acc_tok_exp_in: (exp_in > 0 ? exp_in : 0) + acc_tok_exp_in: exp_in > 0 ? exp_in : 0, }; return result; }; - obj.getProjectRole = function(a_client_id, a_proj_id) { - if (obj.db.owner.firstExample({ + obj.getProjectRole = function (a_client_id, a_proj_id) { + if ( + obj.db.owner.firstExample({ _from: a_proj_id, - _to: a_client_id - })) + _to: a_client_id, + }) + ) return obj.PROJ_ADMIN; - if (obj.db.admin.firstExample({ + if ( + obj.db.admin.firstExample({ _from: a_proj_id, - _to: a_client_id - })) + _to: a_client_id, + }) + ) return obj.PROJ_MANAGER; var grp = obj.db.g.firstExample({ uid: a_proj_id, - gid: "members" + gid: "members", }); - if (!grp) - return obj.PROJ_NO_ROLE; + if (!grp) return obj.PROJ_NO_ROLE; - if (obj.db.member.firstExample({ + if ( + obj.db.member.firstExample({ _from: grp._id, - _to: a_client_id - })) + _to: a_client_id, + }) + ) return obj.PROJ_MEMBER; - else - return obj.PROJ_NO_ROLE; + else return obj.PROJ_NO_ROLE; }; - obj.sortAllocations = function(allocs) { - allocs.sort(function(a, b) { - if (a.is_def) - return -1; - else if (b.is_def) - return 1; - else - return a._to < b._to ? -1 : 1; + obj.sortAllocations = function (allocs) { + allocs.sort(function (a, b) { + if (a.is_def) return -1; + else if (b.is_def) return 1; + else return a._to < b._to ? -1 : 1; }); }; - obj.assignRepo = function(a_user_id) { - var alloc, allocs = obj.db.alloc.byExample({ - _from: a_user_id - }).toArray(); + obj.assignRepo = function (a_user_id) { + var alloc, + allocs = obj.db.alloc + .byExample({ + _from: a_user_id, + }) + .toArray(); obj.sortAllocations(allocs); @@ -853,49 +901,54 @@ module.exports = (function() { return null; }; - obj.verifyRepo = function(a_user_id, a_repo_id) { + obj.verifyRepo = function (a_user_id, a_repo_id) { var alloc = obj.db.alloc.firstExample({ _from: a_user_id, - _to: a_repo_id + _to: a_repo_id, }); - if (!alloc) - throw [obj.ERR_NO_ALLOCATION, "No allocation on repo " + a_repo_id]; + if (!alloc) throw [obj.ERR_NO_ALLOCATION, "No allocation on repo " + a_repo_id]; if (alloc.data_size >= alloc.data_limit) - throw [obj.ERR_ALLOCATION_EXCEEDED, "Allocation data size exceeded (max: " + alloc.data_limit + ")"]; + throw [ + obj.ERR_ALLOCATION_EXCEEDED, + "Allocation data size exceeded (max: " + alloc.data_limit + ")", + ]; if (alloc.rec_count >= alloc.rec_limit) - throw [obj.ERR_ALLOCATION_EXCEEDED, "Allocation record count exceeded (max: " + alloc.rec_limit + ")"]; + throw [ + obj.ERR_ALLOCATION_EXCEEDED, + "Allocation record count exceeded (max: " + alloc.rec_limit + ")", + ]; return alloc; }; - obj.getRootID = function(owner_id) { + obj.getRootID = function (owner_id) { return "c/" + owner_id[0] + "_" + owner_id.substr(2) + "_root"; }; - obj.computeDataPath = function(a_loc, a_export) { + obj.computeDataPath = function (a_loc, a_export) { var repo = obj.db._document(a_loc._to); var repo_path = a_export ? repo.export_path : repo.path; - if (a_loc.uid.charAt(0) == 'u') { + if (a_loc.uid.charAt(0) == "u") { return repo_path + "user" + a_loc.uid.substr(1) + a_loc._from.substr(1); } else { return repo_path + "project" + a_loc.uid.substr(1) + a_loc._from.substr(1); } }; - obj.computeDataPathPrefix = function(a_repo_id, a_owner_id) { + obj.computeDataPathPrefix = function (a_repo_id, a_owner_id) { var repo = obj.db._document(a_repo_id); - if (a_owner_id.charAt(0) == 'u') { + if (a_owner_id.charAt(0) == "u") { return repo.path + "user" + a_owner_id.substr(1) + "/"; } else { return repo.path + "project" + a_owner_id.substr(1) + "/"; } }; - obj.getObject = function(a_obj_id, a_client) { + obj.getObject = function (a_obj_id, a_client) { var id = obj.resolveID(a_obj_id, a_client); if (!obj.db._exists(id)) @@ -906,13 +959,15 @@ module.exports = (function() { return doc; }; - obj.getDataCollectionLinkCount = function(id) { - return obj.db._query("for v in 1..1 inbound @id item return v._id", { - id: id - }).count(); + obj.getDataCollectionLinkCount = function (id) { + return obj.db + ._query("for v in 1..1 inbound @id item return v._id", { + id: id, + }) + .count(); }; - obj.hasAdminPermUser = function(a_client, a_user_id) { + obj.hasAdminPermUser = function (a_client, a_user_id) { //if ( a_client._id != a_user_id && !a_client.is_admin && !obj.db.owner.firstExample({ _from: a_user_id, _to: a_client._id }) && !obj.db.admin.firstExample({ _from: a_user_id, _to: a_client._id })){ if (a_client._id != a_user_id && !a_client.is_admin) { return false; @@ -921,47 +976,58 @@ module.exports = (function() { } }; - obj.hasAdminPermProj = function(a_client, a_proj_id) { - if (!a_client.is_admin && !obj.db.owner.firstExample({ + obj.hasAdminPermProj = function (a_client, a_proj_id) { + if ( + !a_client.is_admin && + !obj.db.owner.firstExample({ _from: a_proj_id, - _to: a_client._id - })) { + _to: a_client._id, + }) + ) { return false; } else { return true; } }; - obj.hasManagerPermProj = function(a_client, a_proj_id) { - if (!a_client.is_admin && !obj.db.owner.firstExample({ + obj.hasManagerPermProj = function (a_client, a_proj_id) { + if ( + !a_client.is_admin && + !obj.db.owner.firstExample({ _from: a_proj_id, - _to: a_client._id - }) && !obj.db.admin.firstExample({ + _to: a_client._id, + }) && + !obj.db.admin.firstExample({ _from: a_proj_id, - _to: a_client._id - })) { + _to: a_client._id, + }) + ) { return false; } else { return true; } }; - obj.hasAdminPermObjectLoaded = function(a_client, a_object) { + obj.hasAdminPermObjectLoaded = function (a_client, a_object) { // TODO Should collection creator have admin rights? if (a_object.owner == a_client._id || a_object.creator == a_client._id || a_client.is_admin) return true; - if (a_object.owner.charAt(0) == 'p') { - if (obj.db.owner.firstExample({ + if (a_object.owner.charAt(0) == "p") { + if ( + obj.db.owner.firstExample({ _from: a_object.owner, - _to: a_client._id - })) + _to: a_client._id, + }) + ) return true; - if (obj.db.admin.firstExample({ + if ( + obj.db.admin.firstExample({ _from: a_object.owner, - _to: a_client._id - })) + _to: a_client._id, + }) + ) return true; } @@ -993,121 +1059,116 @@ module.exports = (function() { * "p/big_thing" * "c/my_collection" **/ - obj.hasAdminPermObject = function(a_client, a_object_id) { - if (a_client.is_admin) - return true; + obj.hasAdminPermObject = function (a_client, a_object_id) { + if (a_client.is_admin) return true; var first_owner = obj.db.owner.firstExample({ - _from: a_object_id + _from: a_object_id, }); if (first_owner !== null) { var owner_id = first_owner._to; // obj.db.owner.firstExample({ _from: a_object_id })._to; } else { throw [obj.ERR_NOT_FOUND, "Data record for owner not found " + a_object_id + "."]; } - if (owner_id == a_client._id) - return true; + if (owner_id == a_client._id) return true; if (owner_id[0] == "p") { // Object owned by a project - if (obj.db.admin.firstExample({ + if ( + obj.db.admin.firstExample({ _from: owner_id, - _to: a_client._id - })) + _to: a_client._id, + }) + ) return true; - if (obj.db.owner.firstExample({ + if ( + obj.db.owner.firstExample({ _from: owner_id, - _to: a_client._id - })) + _to: a_client._id, + }) + ) return true; } - if (a_object_id[0] == 'd') { + if (a_object_id[0] == "d") { var data = obj.db._query("for i in d filter i._id == @id return i.creator", { - id: a_object_id + id: a_object_id, }); if (!data.hasNext()) { throw [obj.ERR_NOT_FOUND, "Data record " + a_object_id + " not found."]; } data = data.next(); - if (a_client._id == data) - return true; + if (a_client._id == data) return true; } return false; }; - obj.hasAdminPermRepo = function(a_client, a_repo_id) { - if (!a_client.is_admin && !obj.db.admin.firstExample({ + obj.hasAdminPermRepo = function (a_client, a_repo_id) { + if ( + !a_client.is_admin && + !obj.db.admin.firstExample({ _from: a_repo_id, - _to: a_client._id - })) { + _to: a_client._id, + }) + ) { return false; } else { return true; } }; - obj.ensureAdminPermUser = function(a_client, a_user_id) { - if (!obj.hasAdminPermUser(a_client, a_user_id)) - throw obj.ERR_PERM_DENIED; + obj.ensureAdminPermUser = function (a_client, a_user_id) { + if (!obj.hasAdminPermUser(a_client, a_user_id)) throw obj.ERR_PERM_DENIED; }; - obj.ensureAdminPermProj = function(a_client, a_user_id) { - if (!obj.hasAdminPermProj(a_client, a_user_id)) - throw obj.ERR_PERM_DENIED; + obj.ensureAdminPermProj = function (a_client, a_user_id) { + if (!obj.hasAdminPermProj(a_client, a_user_id)) throw obj.ERR_PERM_DENIED; }; - obj.ensureManagerPermProj = function(a_client, a_user_id) { - if (!obj.hasManagerPermProj(a_client, a_user_id)) - throw obj.ERR_PERM_DENIED; + obj.ensureManagerPermProj = function (a_client, a_user_id) { + if (!obj.hasManagerPermProj(a_client, a_user_id)) throw obj.ERR_PERM_DENIED; }; - obj.ensureAdminPermObject = function(a_client, a_object_id) { - if (!obj.hasAdminPermObject(a_client, a_object_id)) - throw obj.ERR_PERM_DENIED; + obj.ensureAdminPermObject = function (a_client, a_object_id) { + if (!obj.hasAdminPermObject(a_client, a_object_id)) throw obj.ERR_PERM_DENIED; }; - obj.ensureAdminPermRepo = function(a_client, a_repo_id) { - if (!obj.hasAdminPermRepo(a_client, a_repo_id)) - throw obj.ERR_PERM_DENIED; + obj.ensureAdminPermRepo = function (a_client, a_repo_id) { + if (!obj.hasAdminPermRepo(a_client, a_repo_id)) throw obj.ERR_PERM_DENIED; }; - obj.isSrcParentOfDest = function(a_src_id, a_dest_id) { + obj.isSrcParentOfDest = function (a_src_id, a_dest_id) { var parent; var child_id = a_dest_id; for (;;) { parent = obj.db.item.firstExample({ - _to: child_id + _to: child_id, }); - if (!parent) - return false; - if (parent._from == a_src_id) - return true; + if (!parent) return false; + if (parent._from == a_src_id) return true; child_id = parent._from; } }; // Data or Collection ID or alias - obj.resolveID = function(a_id, a_client) { - var id, i = a_id.indexOf('/'); + obj.resolveID = function (a_id, a_client) { + var id, + i = a_id.indexOf("/"); if (i != -1) { - if (!a_id.startsWith('d/') && !a_id.startsWith('c/') && !a_id.startsWith('p/')) + if (!a_id.startsWith("d/") && !a_id.startsWith("c/") && !a_id.startsWith("p/")) throw [obj.ERR_INVALID_PARAM, "Invalid ID '" + a_id + "'"]; id = a_id; } else { var alias_id = "a/"; - if (a_id.indexOf(":") == -1) - alias_id += "u:" + a_client._key + ":" + a_id; - else - alias_id += a_id; + if (a_id.indexOf(":") == -1) alias_id += "u:" + a_client._key + ":" + a_id; + else alias_id += a_id; var alias = obj.db.alias.firstExample({ - _to: alias_id + _to: alias_id, }); - if (!alias) - throw [obj.ERR_NOT_FOUND, "Alias '" + a_id + "' does not exist"]; + if (!alias) throw [obj.ERR_NOT_FOUND, "Alias '" + a_id + "' does not exist"]; id = alias._from; } @@ -1119,30 +1180,32 @@ module.exports = (function() { return id; }; - obj.resolveDataID = function(a_id, a_client) { - var alias, id, i = a_id.indexOf('/'); + obj.resolveDataID = function (a_id, a_client) { + var alias, + id, + i = a_id.indexOf("/"); if (i != -1) { - if (!a_id.startsWith('d/')) + if (!a_id.startsWith("d/")) throw [obj.ERR_INVALID_PARAM, "Invalid data record ID '" + a_id + "'"]; id = a_id; } else { var alias_id = "a/"; - if (a_id.indexOf(":") == -1) - alias_id += "u:" + a_client._key + ":" + a_id; - else - alias_id += a_id; + if (a_id.indexOf(":") == -1) alias_id += "u:" + a_client._key + ":" + a_id; + else alias_id += a_id; alias = obj.db.alias.firstExample({ - _to: alias_id + _to: alias_id, }); - if (!alias) - throw [obj.ERR_NOT_FOUND, "Alias '" + a_id + "' does not exist"]; + if (!alias) throw [obj.ERR_NOT_FOUND, "Alias '" + a_id + "' does not exist"]; id = alias._from; - if (!id.startsWith('d/')) - throw [obj.ERR_INVALID_PARAM, "Alias '" + a_id + "' does not identify a data record"]; + if (!id.startsWith("d/")) + throw [ + obj.ERR_INVALID_PARAM, + "Alias '" + a_id + "' does not identify a data record", + ]; } if (!obj.db.d.exists(id)) { @@ -1152,30 +1215,31 @@ module.exports = (function() { return id; }; - obj.resolveCollID = function(a_id, a_client) { - var id, i = a_id.indexOf('/'); + obj.resolveCollID = function (a_id, a_client) { + var id, + i = a_id.indexOf("/"); if (i != -1) { - if (!a_id.startsWith('c/')) + if (!a_id.startsWith("c/")) throw [obj.ERR_INVALID_PARAM, "Invalid collection ID '" + a_id + "'"]; id = a_id; } else { var alias_id = "a/"; - if (a_client && a_id.indexOf(":") == -1) - alias_id += "u:" + a_client._key + ":" + a_id; - else - alias_id += a_id; + if (a_client && a_id.indexOf(":") == -1) alias_id += "u:" + a_client._key + ":" + a_id; + else alias_id += a_id; var alias = obj.db.alias.firstExample({ - _to: alias_id + _to: alias_id, }); - if (!alias) - throw [obj.ERR_NOT_FOUND, "Alias '" + a_id + "' does not exist"]; + if (!alias) throw [obj.ERR_NOT_FOUND, "Alias '" + a_id + "' does not exist"]; id = alias._from; - if (!id.startsWith('c/')) - throw [obj.ERR_INVALID_PARAM, "Alias '" + a_id + "' does not identify a collection"]; + if (!id.startsWith("c/")) + throw [ + obj.ERR_INVALID_PARAM, + "Alias '" + a_id + "' does not identify a collection", + ]; } if (!obj.db.c.exists(id)) { @@ -1185,30 +1249,32 @@ module.exports = (function() { return id; }; - obj.resolveCollID2 = function(a_id, a_ctxt) { - var id, i = a_id.indexOf('/'); + obj.resolveCollID2 = function (a_id, a_ctxt) { + var id, + i = a_id.indexOf("/"); if (i != -1) { - if (!a_id.startsWith('c/')) + if (!a_id.startsWith("c/")) throw [obj.ERR_INVALID_PARAM, "Invalid collection ID '" + a_id + "'"]; id = a_id; } else { var alias_id = "a/"; if (a_ctxt && a_id.indexOf(":") == -1) alias_id += a_ctxt.charAt(0) + ":" + a_ctxt.substr(2) + ":" + a_id; - else - alias_id += a_id; + else alias_id += a_id; var alias = obj.db.alias.firstExample({ - _to: alias_id + _to: alias_id, }); - if (!alias) - throw [obj.ERR_NOT_FOUND, "Alias '" + alias_id + "' does not exist"]; + if (!alias) throw [obj.ERR_NOT_FOUND, "Alias '" + alias_id + "' does not exist"]; id = alias._from; - if (!id.startsWith('c/')) - throw [obj.ERR_INVALID_PARAM, "Alias '" + alias_id + "' does not identify a collection"]; + if (!id.startsWith("c/")) + throw [ + obj.ERR_INVALID_PARAM, + "Alias '" + alias_id + "' does not identify a collection", + ]; } if (!obj.db.c.exists(id)) { @@ -1218,31 +1284,32 @@ module.exports = (function() { return id; }; - obj.resolveDataCollID = function(a_id, a_client) { - var id, i = a_id.indexOf('/'); + obj.resolveDataCollID = function (a_id, a_client) { + var id, + i = a_id.indexOf("/"); if (i != -1) { - if (!a_id.startsWith('d/') && !a_id.startsWith('c/')) + if (!a_id.startsWith("d/") && !a_id.startsWith("c/")) throw [obj.ERR_INVALID_PARAM, "Invalid ID '" + a_id + "'"]; id = a_id; } else { var alias_id = "a/"; - if (a_client && a_id.indexOf(":") == -1) - alias_id += "u:" + a_client._key + ":" + a_id; - else - alias_id += a_id; + if (a_client && a_id.indexOf(":") == -1) alias_id += "u:" + a_client._key + ":" + a_id; + else alias_id += a_id; var alias = obj.db.alias.firstExample({ - _to: alias_id + _to: alias_id, }); - if (!alias) - throw [obj.ERR_NOT_FOUND, "Alias '" + a_id + "' does not exist"]; + if (!alias) throw [obj.ERR_NOT_FOUND, "Alias '" + a_id + "' does not exist"]; id = alias._from; } if (!obj.db._exists(id)) { - throw [obj.ERR_INVALID_PARAM, (id.charAt(0) == 'd' ? "Data record '" : "Collection '") + id + "' does not exist."]; + throw [ + obj.ERR_INVALID_PARAM, + (id.charAt(0) == "d" ? "Data record '" : "Collection '") + id + "' does not exist.", + ]; } return id; @@ -1257,22 +1324,25 @@ module.exports = (function() { */ // For use when creating a new record - obj.getCollCategoryTags = function(a_coll_id) { + obj.getCollCategoryTags = function (a_coll_id) { var coll = obj.db.c.document(a_coll_id), ctx = obj.catalogCalcParCtxt(coll, {}); - if (ctx.pub) - return Array.from(ctx.tags); + if (ctx.pub) return Array.from(ctx.tags); }; - obj.catalogUpdateRecord = function(a_data, a_coll, a_ctx, a_visited = {}) { - var p, par = obj.db.item.byExample({ - _to: a_data._id + obj.catalogUpdateRecord = function (a_data, a_coll, a_ctx, a_visited = {}) { + var p, + par = obj.db.item.byExample({ + _to: a_data._id, }), - tmp, _ctx = (a_ctx ? a_ctx : { - pub: false, - tags: new Set() - }); + tmp, + _ctx = a_ctx + ? a_ctx + : { + pub: false, + tags: new Set(), + }; while (par.hasNext()) { p = par.next(); @@ -1291,33 +1361,38 @@ module.exports = (function() { if (_ctx === a_ctx) { _ctx = { pub: a_ctx.pub, - tags: new Set(a_ctx.tags) + tags: new Set(a_ctx.tags), }; } - _ctx.pub = (_ctx.pub || tmp.pub); + _ctx.pub = _ctx.pub || tmp.pub; tmp.tags.forEach(_ctx.tags.add, _ctx.tags); } } } // Update record with pub flag & tags - obj.db._update(a_data._id, { - public: _ctx.pub, - cat_tags: _ctx.pub ? Array.from(_ctx.tags) : null - }, { - keepNull: false - }); + obj.db._update( + a_data._id, + { + public: _ctx.pub, + cat_tags: _ctx.pub ? Array.from(_ctx.tags) : null, + }, + { + keepNull: false, + }, + ); }; - obj.catalogCalcParCtxt = function(a_coll, a_visited) { + obj.catalogCalcParCtxt = function (a_coll, a_visited) { //console.log("catalogCalcParCtxt",a_coll._id); - var c, ctx = { + var c, + ctx = { pub: a_coll.public ? true : false, - tags: new Set(a_coll.cat_tags ? a_coll.cat_tags : []) + tags: new Set(a_coll.cat_tags ? a_coll.cat_tags : []), }, item = obj.db.item.firstExample({ - _to: a_coll._id + _to: a_coll._id, }); while (item) { @@ -1325,7 +1400,7 @@ module.exports = (function() { c = a_visited[item._from]; if (c) { - ctx.pub = (ctx.pub || c.pub); + ctx.pub = ctx.pub || c.pub; if (c.tags) { c.tags.forEach(ctx.tags.add, ctx.tags); } @@ -1333,14 +1408,14 @@ module.exports = (function() { break; } else { c = obj.db.c.document(item._from); - ctx.pub = (ctx.pub || c.public); + ctx.pub = ctx.pub || c.public; if (c.cat_tags) { c.cat_tags.forEach(ctx.tags.add, ctx.tags); } } item = obj.db.item.firstExample({ - _to: item._from + _to: item._from, }); } //console.log("update visited",ctx); @@ -1355,13 +1430,13 @@ module.exports = (function() { calling this function. Note that the public state and category tags of child collections are not changed by this function. */ - obj.catalogUpdateColl = function(a_coll, a_ctx, a_visited = {}) { + obj.catalogUpdateColl = function (a_coll, a_ctx, a_visited = {}) { var ctx; if (a_ctx) { ctx = { - pub: (a_coll.public || a_ctx.pub), - tags: new Set(a_ctx.tags) + pub: a_coll.public || a_ctx.pub, + tags: new Set(a_ctx.tags), }; if (a_coll.cat_tags) { a_coll.cat_tags.forEach(ctx.tags.add, ctx.tags); @@ -1372,14 +1447,19 @@ module.exports = (function() { ctx = obj.catalogCalcParCtxt(a_coll, a_visited); } - var p, par, _ctx, tmp, item, items = obj.db.item.byExample({ - _from: a_coll._id - }); + var p, + par, + _ctx, + tmp, + item, + items = obj.db.item.byExample({ + _from: a_coll._id, + }); while (items.hasNext()) { item = items.next(); - if (item._to.charAt(0) == 'c') { + if (item._to.charAt(0) == "c") { par = obj.db.c.document(item._to); obj.catalogUpdateColl(par, ctx, a_visited); } else { @@ -1387,7 +1467,7 @@ module.exports = (function() { // Determine if this record is published by other collections par = obj.db.item.byExample({ - _to: item._to + _to: item._to, }); _ctx = ctx; @@ -1416,11 +1496,11 @@ module.exports = (function() { if (_ctx === ctx) { _ctx = { pub: ctx.pub, - tags: new Set(ctx.tags) + tags: new Set(ctx.tags), }; } - _ctx.pub = (_ctx.pub || tmp.pub); + _ctx.pub = _ctx.pub || tmp.pub; tmp.tags.forEach(_ctx.tags.add, _ctx.tags); } } else { @@ -1429,19 +1509,23 @@ module.exports = (function() { } // Update record with pub flag & tags - obj.db._update(item._to, { - public: _ctx.pub, - cat_tags: _ctx.pub ? Array.from(_ctx.tags) : null - }, { - keepNull: false - }); + obj.db._update( + item._to, + { + public: _ctx.pub, + cat_tags: _ctx.pub ? Array.from(_ctx.tags) : null, + }, + { + keepNull: false, + }, + ); } } }; - - obj.topicCreate = function(a_topics, a_idx, a_par_id, a_owner_id) { - var topic, par_id = a_par_id; //, doc; + obj.topicCreate = function (a_topics, a_idx, a_par_id, a_owner_id) { + var topic, + par_id = a_par_id; //, doc; for (var i = a_idx; i < a_topics.length; i++) { topic = a_topics[i]; @@ -1452,16 +1536,19 @@ module.exports = (function() { obj.db.tag.save({ _key: topic, count: 1 }); }*/ - topic = obj.db.t.save({ - title: topic, - creator: a_owner_id, - coll_cnt: 1 - }, { - returnNew: true - }); + topic = obj.db.t.save( + { + title: topic, + creator: a_owner_id, + coll_cnt: 1, + }, + { + returnNew: true, + }, + ); obj.db.top.save({ _from: topic._id, - _to: par_id + _to: par_id, }); par_id = topic._id; } @@ -1469,20 +1556,20 @@ module.exports = (function() { return par_id; }; - obj.topicLink = function(a_topic, a_coll_id, a_owner_id) { - var i, topics = a_topic.split("."); + obj.topicLink = function (a_topic, a_coll_id, a_owner_id) { + var i, + topics = a_topic.split("."); // Detect misplaced topic delimiters for (i in topics) { - if (topics[i].length === 0) - throw [obj.ERR_INVALID_PARAM, "Invalid category"]; + if (topics[i].length === 0) throw [obj.ERR_INVALID_PARAM, "Invalid category"]; } var topic, parent; //,tag; // Find or create top-level topics parent = obj.db._query("for i in t filter i.top == true && i.title == @title return i", { - title: topics[0] + title: topics[0], }); if (parent.hasNext()) { @@ -1490,7 +1577,7 @@ module.exports = (function() { // Increment coll_cnt obj.db.t.update(parent._id, { - coll_cnt: parent.coll_cnt + 1 + coll_cnt: parent.coll_cnt + 1, }); parent = parent._id; @@ -1501,15 +1588,18 @@ module.exports = (function() { }*/ for (i = 1; i < topics.length; i++) { - topic = obj.db._query("for v in 1..1 inbound @par top filter v.title == @title filter is_same_collection('t',v) return v", { - par: parent, - title: topics[i] - }); + topic = obj.db._query( + "for v in 1..1 inbound @par top filter v.title == @title filter is_same_collection('t',v) return v", + { + par: parent, + title: topics[i], + }, + ); if (topic.hasNext()) { parent = topic.next(); // Increment coll_cnt obj.db.t.update(parent._id, { - coll_cnt: parent.coll_cnt + 1 + coll_cnt: parent.coll_cnt + 1, }); /*if (( tag = obj.db.tag.firstExample({ _key: topics[i] })) != null ){ @@ -1525,40 +1615,46 @@ module.exports = (function() { } } } else { - parent = obj.db.t.save({ - title: topics[0], - top: true, - creator: a_owner_id, - coll_cnt: 1 - }, { - returnNew: true - })._id; + parent = obj.db.t.save( + { + title: topics[0], + top: true, + creator: a_owner_id, + coll_cnt: 1, + }, + { + returnNew: true, + }, + )._id; parent = this.topicCreate(topics, 1, parent, a_owner_id); } - if (!obj.db.top.firstExample({ + if ( + !obj.db.top.firstExample({ _from: a_coll_id, - _to: parent - })) { + _to: parent, + }) + ) { obj.db.top.save({ _from: a_coll_id, - _to: parent + _to: parent, }); } }; - obj.topicUnlink = function(a_coll_id) { + obj.topicUnlink = function (a_coll_id) { //console.log("topicUnlink"); var top_lnk = obj.db.top.firstExample({ - _from: a_coll_id + _from: a_coll_id, }); if (!top_lnk) { return; } // Save parent topic id, delete link from collection - var topic, topic_id = top_lnk._to, + var topic, + topic_id = top_lnk._to, dec_only = false; //, tags = []; //console.log("rem top lnk",top_lnk._id); obj.db.top.remove(top_lnk); @@ -1569,19 +1665,23 @@ module.exports = (function() { // Decrement coll_cnt obj.db.t.update(topic._id, { - coll_cnt: topic.coll_cnt - 1 + coll_cnt: topic.coll_cnt - 1, }); //tags.push( topic.title ); // If parent is admin controlled, or other topics are linked to parent, stop - if (dec_only || topic.admin || obj.db.top.firstExample({ - _to: topic_id - })) { + if ( + dec_only || + topic.admin || + obj.db.top.firstExample({ + _to: topic_id, + }) + ) { //console.log("stop no del topic",topic); dec_only = true; top_lnk = obj.db.top.firstExample({ - _from: topic_id + _from: topic_id, }); if (top_lnk) { topic_id = top_lnk._to; @@ -1590,7 +1690,7 @@ module.exports = (function() { } } else { top_lnk = obj.db.top.firstExample({ - _from: topic_id + _from: topic_id, }); if (top_lnk) { @@ -1608,62 +1708,59 @@ module.exports = (function() { //obj.removeTags( tags ); }; - obj.getParents = function(item_id) { - var p, idx = 0, - parents, results = []; + obj.getParents = function (item_id) { + var p, + idx = 0, + parents, + results = []; parents = obj.db.item.byExample({ - _to: item_id + _to: item_id, }); - if (!parents.hasNext()) - return [ - [] - ]; + if (!parents.hasNext()) return [[]]; while (parents.hasNext()) { p = parents.next(); p = obj.db.c.document(p._from); - results.push([{ - id: p._id, - title: p.title, - alias: p.alias - }]); + results.push([ + { + id: p._id, + title: p.title, + alias: p.alias, + }, + ]); p = obj.db.item.firstExample({ - _to: p._id + _to: p._id, }); while (p) { p = obj.db.c.document(p._from); results[idx].push({ id: p._id, title: p.title, - alias: p.alias + alias: p.alias, }); p = obj.db.item.firstExample({ - _to: p._id + _to: p._id, }); } idx++; } // Sort paths alphabetically as in collection listings - results.sort(function(a, b) { + results.sort(function (a, b) { var i, j; if (a.length < b.length) { for (i = a.length - 1, j = b.length - 1; i >= 0; i--, j--) { - if (a[i].title < b[j].title) - return -1; - else if (a[i].title > b[j].title) - return 1; + if (a[i].title < b[j].title) return -1; + else if (a[i].title > b[j].title) return 1; } return 1; } else { for (i = a.length - 1, j = b.length - 1; j >= 0; i--, j--) { - if (a[i].title < b[j].title) - return -1; - else if (a[i].title > b[j].title) - return 1; + if (a[i].title < b[j].title) return -1; + else if (a[i].title > b[j].title) return 1; } return -1; } @@ -1672,60 +1769,59 @@ module.exports = (function() { return results; }; - - obj.makeTitleUnique = function(a_parent_id, a_doc) { - var conflicts = obj.db._query("for v in 1..1 outbound @coll item filter is_same_collection(@type,v) and v.title == @title return {id:v._id}", { - coll: a_parent_id, - title: a_doc.title, - type: a_doc._id.charAt(0) - }); + obj.makeTitleUnique = function (a_parent_id, a_doc) { + var conflicts = obj.db._query( + "for v in 1..1 outbound @coll item filter is_same_collection(@type,v) and v.title == @title return {id:v._id}", + { + coll: a_parent_id, + title: a_doc.title, + type: a_doc._id.charAt(0), + }, + ); if (conflicts.hasNext()) { obj.db._update(a_doc._id, { - title: a_doc.title + "_" + a_doc._key + title: a_doc.title + "_" + a_doc._key, }); } }; - - obj.hasAnyCommonAccessScope = function(src_item_id, dst_coll_id) { + obj.hasAnyCommonAccessScope = function (src_item_id, dst_coll_id) { //console.log("hasAnyCommonAccessScope",src_item_id, dst_coll_id); - if (src_item_id[0] == 'c') { + if (src_item_id[0] == "c") { // Collections can only be linked in one place, can use hasCommonAccessScope on parent var parent = obj.db.item.firstExample({ - _to: src_item_id + _to: src_item_id, }); - if (!parent) - return false; + if (!parent) return false; else { return obj.hasCommonAccessScope(parent._from, dst_coll_id); } } else { var parents = obj.db.item.byExample({ - _to: src_item_id + _to: src_item_id, }); while (parents.hasNext()) { - if (obj.hasCommonAccessScope(parents.next()._from, dst_coll_id)) - return true; + if (obj.hasCommonAccessScope(parents.next()._from, dst_coll_id)) return true; } } return false; }; - obj.hasCommonAccessScope = function(src_coll_id, dst_coll_id) { + obj.hasCommonAccessScope = function (src_coll_id, dst_coll_id) { //console.log("hasCommonAccessScope",src_coll_id, dst_coll_id); var p1 = [src_coll_id], p2 = [dst_coll_id]; - var parent, child = src_coll_id; + var parent, + child = src_coll_id; for (;;) { parent = obj.db.item.firstExample({ - _to: child + _to: child, }); - if (!parent) - break; + if (!parent) break; p1.unshift(parent._from); child = parent._from; } @@ -1734,19 +1830,18 @@ module.exports = (function() { for (;;) { parent = obj.db.item.firstExample({ - _to: child + _to: child, }); - if (!parent) - break; + if (!parent) break; p2.unshift(parent._from); child = parent._from; } - var i, len = Math.min(p1.length, p2.length); + var i, + len = Math.min(p1.length, p2.length); for (i = 0; i < len; i++) { - if (p1[i] != p2[i]) - break; + if (p1[i] != p2[i]) break; } //console.log("hasCommonAccessScope",p1, p2,i); @@ -1758,17 +1853,21 @@ module.exports = (function() { var j; for (j = i; j < p1.length; j++) { - if (obj.db.acl.firstExample({ - _from: p1[j] - })) { + if ( + obj.db.acl.firstExample({ + _from: p1[j], + }) + ) { return false; } } for (j = i; j < p2.length; j++) { - if (obj.db.acl.firstExample({ - _from: p2[j] - })) { + if ( + obj.db.acl.firstExample({ + _from: p2[j], + }) + ) { return false; } } @@ -1776,26 +1875,30 @@ module.exports = (function() { return true; }; - obj.hasPublicRead = function(a_id) { + obj.hasPublicRead = function (a_id) { console.log("Has public read a_id is ", a_id); // Check for local topic on collections if (a_id.startsWith("c/")) { var col = obj.db.c.document(a_id); - if (col.topic) - return true; + if (col.topic) return true; } - var i, children = [a_id], + var i, + children = [a_id], parents; for (;;) { // Find all parent collections owned by object owner - parents = obj.db._query("for i in @children for v in 1..1 inbound i item return {_id:v._id,topic:v.topic}", { - children: children - }).toArray(); + parents = obj.db + ._query( + "for i in @children for v in 1..1 inbound i item return {_id:v._id,topic:v.topic}", + { + children: children, + }, + ) + .toArray(); console.log("children are ", children, " parents are, ", parents); - if (parents.length === 0) - return false; + if (parents.length === 0) return false; for (i in parents) { if (parents[i].topic) { @@ -1812,12 +1915,21 @@ module.exports = (function() { * known not to be owned by the client (and that the client is not an admin). In this case, those checks * would add performance cost for no benefit. */ - obj.hasPermissions = function(a_client, a_object, a_req_perm, a_inherited = false, any = false) { + obj.hasPermissions = function ( + a_client, + a_object, + a_req_perm, + a_inherited = false, + any = false, + ) { //console.log("check perm:", a_req_perm, "client:", a_client._id, "object:", a_object._id, "any:", any ); //console.log("grant:", a_object.grant ); var perm_found = 0, - acl, acls, result, i; + acl, + acls, + result, + i; // If object is marked "public", everyone is granted VIEW, and READ permissions // The current implementation allows users to be denied access to public data (maybe wrong?) @@ -1826,50 +1938,52 @@ module.exports = (function() { perm_found = obj.PERM_PUBLIC; result = obj.evalPermissions(a_req_perm, perm_found, any); - if (result != null) - return result; + if (result != null) return result; } // Evaluate user permissions set directly on object if (a_object.acls & 1) { - acls = obj.db._query("for v, e in 1..1 outbound @object acl filter v._id == @client return e", { - object: a_object._id, - client: a_client._id - }).toArray(); + acls = obj.db + ._query("for v, e in 1..1 outbound @object acl filter v._id == @client return e", { + object: a_object._id, + client: a_client._id, + }) + .toArray(); if (acls.length) { for (i in acls) { acl = acls[i]; //console.log("user_perm:",acl); perm_found |= acl.grant; - if (a_inherited && acl.inhgrant) - perm_found |= acl.inhgrant; + if (a_inherited && acl.inhgrant) perm_found |= acl.inhgrant; } result = obj.evalPermissions(a_req_perm, perm_found, any); - if (result != null) - return result; + if (result != null) return result; } } // Evaluate group permissions on object if (a_object.acls & 2) { - acls = obj.db._query("for v, e, p in 2..2 outbound @object acl, outbound member filter p.vertices[2]._id == @client return p.edges[0]", { - object: a_object._id, - client: a_client._id - }).toArray(); + acls = obj.db + ._query( + "for v, e, p in 2..2 outbound @object acl, outbound member filter p.vertices[2]._id == @client return p.edges[0]", + { + object: a_object._id, + client: a_client._id, + }, + ) + .toArray(); if (acls.length) { for (i in acls) { acl = acls[i]; //console.log("group_perm:",acl); perm_found |= acl.grant; - if (a_inherited && acl.inhgrant) - perm_found |= acl.inhgrant; + if (a_inherited && acl.inhgrant) perm_found |= acl.inhgrant; } result = obj.evalPermissions(a_req_perm, perm_found, any); - if (result != null) - return result; + if (result != null) return result; } } @@ -1883,12 +1997,16 @@ module.exports = (function() { for (;;) { // Find all parent collections owned by object owner - parents = obj.db._query("for i in @children for v in 1..1 inbound i item return {_id:v._id,topic:v.topic,acls:v.acls}", { - children: children - }).toArray(); + parents = obj.db + ._query( + "for i in @children for v in 1..1 inbound i item return {_id:v._id,topic:v.topic,acls:v.acls}", + { + children: children, + }, + ) + .toArray(); - if (parents.length == 0) - break; + if (parents.length == 0) break; for (i in parents) { parent = parents[i]; @@ -1897,16 +2015,20 @@ module.exports = (function() { perm_found |= obj.PERM_PUBLIC; result = obj.evalPermissions(a_req_perm, perm_found, any); - if (result != null) - return result; + if (result != null) return result; } // User ACL first - if (parent.acls && ((parent.acls & 1) !== 0)) { - acls = obj.db._query("for v, e in 1..1 outbound @object acl filter v._id == @client return e", { - object: parent._id, - client: a_client._id - }).toArray(); + if (parent.acls && (parent.acls & 1) !== 0) { + acls = obj.db + ._query( + "for v, e in 1..1 outbound @object acl filter v._id == @client return e", + { + object: parent._id, + client: a_client._id, + }, + ) + .toArray(); if (acls.length) { for (i in acls) { acl = acls[i]; @@ -1914,17 +2036,21 @@ module.exports = (function() { } result = obj.evalPermissions(a_req_perm, perm_found, any); - if (result != null) - return result; + if (result != null) return result; } } // Group ACL next - if (parent.acls && ((parent.acls & 2) !== 0)) { - acls = obj.db._query("for v, e, p in 2..2 outbound @object acl, outbound member filter is_same_collection('g',p.vertices[1]) and p.vertices[2]._id == @client return p.edges[0]", { - object: parent._id, - client: a_client._id - }).toArray(); + if (parent.acls && (parent.acls & 2) !== 0) { + acls = obj.db + ._query( + "for v, e, p in 2..2 outbound @object acl, outbound member filter is_same_collection('g',p.vertices[1]) and p.vertices[2]._id == @client return p.edges[0]", + { + object: parent._id, + client: a_client._id, + }, + ) + .toArray(); if (acls.length) { for (i in acls) { acl = acls[i]; @@ -1932,8 +2058,7 @@ module.exports = (function() { } result = obj.evalPermissions(a_req_perm, perm_found, any); - if (result != null) - return result; + if (result != null) return result; } } } @@ -1947,28 +2072,26 @@ module.exports = (function() { return false; }; - obj.evalPermissions = function(a_req_perm, a_perm_found, any) { + obj.evalPermissions = function (a_req_perm, a_perm_found, any) { if (any) { // If any requested permission have been found, return true (granted) - if (a_perm_found & a_req_perm) - return true; - else - return null; // Else, keep looking + if (a_perm_found & a_req_perm) return true; + else return null; // Else, keep looking } else { // If not all requested permissions have been found return NULL (keep looking) - if ((a_perm_found & a_req_perm) != a_req_perm) - return null; - else - return true; // Else, permission granted + if ((a_perm_found & a_req_perm) != a_req_perm) return null; + else return true; // Else, permission granted } }; - obj.getPermissions = function(a_client, a_object, a_req_perm, a_inherited = false) { + obj.getPermissions = function (a_client, a_object, a_req_perm, a_inherited = false) { //console.log("get perm:", a_req_perm, "client:", a_client._id, "object:", a_object._id, "any:", any ); //console.log("grant:", a_object.grant ); var perm_found = 0, - acl, acls, i; + acl, + acls, + i; // If object has a topic (collections only), everyone is granted VIEW, and READ permissions // The current implementation allows users to be denied access to public data (maybe wrong?) @@ -1976,51 +2099,53 @@ module.exports = (function() { if (a_object.topic) { perm_found = obj.PERM_PUBLIC; - if ((a_req_perm & perm_found) == a_req_perm) - return a_req_perm; + if ((a_req_perm & perm_found) == a_req_perm) return a_req_perm; } // Evaluate permissions set directly on object - if (a_object.acls && ((a_object.acls & 1) !== 0)) { - acls = obj.db._query("for v, e in 1..1 outbound @object acl filter v._id == @client return e", { - object: a_object._id, - client: a_client._id - }).toArray(); + if (a_object.acls && (a_object.acls & 1) !== 0) { + acls = obj.db + ._query("for v, e in 1..1 outbound @object acl filter v._id == @client return e", { + object: a_object._id, + client: a_client._id, + }) + .toArray(); if (acls.length) { for (i in acls) { acl = acls[i]; //console.log("user_perm:",acl); perm_found |= acl.grant; - if (a_inherited && acl.inhgrant) - perm_found |= acl.inhgrant; + if (a_inherited && acl.inhgrant) perm_found |= acl.inhgrant; } - if ((a_req_perm & perm_found) == a_req_perm) - return a_req_perm; + if ((a_req_perm & perm_found) == a_req_perm) return a_req_perm; } } // Evaluate group permissions on object - if (a_object.acls && ((a_object.acls & 2) !== 0)) { - acls = obj.db._query("for v, e, p in 2..2 outbound @object acl, outbound member filter p.vertices[2]._id == @client return p.edges[0]", { - object: a_object._id, - client: a_client._id - }).toArray(); + if (a_object.acls && (a_object.acls & 2) !== 0) { + acls = obj.db + ._query( + "for v, e, p in 2..2 outbound @object acl, outbound member filter p.vertices[2]._id == @client return p.edges[0]", + { + object: a_object._id, + client: a_client._id, + }, + ) + .toArray(); if (acls.length) { for (i in acls) { acl = acls[i]; //console.log("group_perm:",acl); perm_found |= acl.grant; - if (a_inherited && acl.inhgrant) - perm_found |= acl.inhgrant; + if (a_inherited && acl.inhgrant) perm_found |= acl.inhgrant; } - if ((a_req_perm & perm_found) == a_req_perm) - return a_req_perm; + if ((a_req_perm & perm_found) == a_req_perm) return a_req_perm; } } @@ -2033,12 +2158,16 @@ module.exports = (function() { for (;;) { // Find all parent collections owned by object owner - parents = obj.db._query("for i in @children for v in 1..1 inbound i item return {_id:v._id,topic:v.topic,acls:v.acls}", { - children: children - }).toArray(); + parents = obj.db + ._query( + "for i in @children for v in 1..1 inbound i item return {_id:v._id,topic:v.topic,acls:v.acls}", + { + children: children, + }, + ) + .toArray(); - if (parents.length == 0) - break; + if (parents.length == 0) break; for (i in parents) { parent = parents[i]; @@ -2046,41 +2175,48 @@ module.exports = (function() { if (parent.topic) { perm_found |= obj.PERM_PUBLIC; - if ((a_req_perm & perm_found) == a_req_perm) - return a_req_perm; + if ((a_req_perm & perm_found) == a_req_perm) return a_req_perm; } // User ACL - if (parent.acls && ((parent.acls & 1) != 0)) { - acls = obj.db._query("for v, e in 1..1 outbound @object acl filter v._id == @client return e", { - object: parent._id, - client: a_client._id - }).toArray(); + if (parent.acls && (parent.acls & 1) != 0) { + acls = obj.db + ._query( + "for v, e in 1..1 outbound @object acl filter v._id == @client return e", + { + object: parent._id, + client: a_client._id, + }, + ) + .toArray(); if (acls.length) { for (i in acls) { acl = acls[i]; perm_found |= acl.inhgrant; } - if ((a_req_perm & perm_found) == a_req_perm) - return a_req_perm; + if ((a_req_perm & perm_found) == a_req_perm) return a_req_perm; } } // Group ACL - if (parent.acls && ((parent.acls & 2) != 0)) { - acls = obj.db._query("for v, e, p in 2..2 outbound @object acl, outbound member filter is_same_collection('g',p.vertices[1]) and p.vertices[2]._id == @client return p.edges[0]", { - object: parent._id, - client: a_client._id - }).toArray(); + if (parent.acls && (parent.acls & 2) != 0) { + acls = obj.db + ._query( + "for v, e, p in 2..2 outbound @object acl, outbound member filter is_same_collection('g',p.vertices[1]) and p.vertices[2]._id == @client return p.edges[0]", + { + object: parent._id, + client: a_client._id, + }, + ) + .toArray(); if (acls.length) { for (i in acls) { acl = acls[i]; perm_found |= acl.inhgrant; } - if ((a_req_perm & perm_found) == a_req_perm) - return a_req_perm; + if ((a_req_perm & perm_found) == a_req_perm) return a_req_perm; } } } @@ -2093,13 +2229,15 @@ module.exports = (function() { return perm_found & a_req_perm; }; - obj.getPermissionsLocal = function(a_client_id, a_object, a_get_inherited, a_req_perm) { + obj.getPermissionsLocal = function (a_client_id, a_object, a_get_inherited, a_req_perm) { var perm = { grant: 0, inhgrant: 0, - inherited: 0 + inherited: 0, }, - acl, acls, i; + acl, + acls, + i; //console.log("getPermissionsLocal",a_object._id); @@ -2112,10 +2250,12 @@ module.exports = (function() { if (a_object.acls & 1) { //console.log("chk local user acls"); - acls = obj.db._query("for v, e in 1..1 outbound @object acl filter v._id == @client return e", { - object: a_object._id, - client: a_client_id - }).toArray(); + acls = obj.db + ._query("for v, e in 1..1 outbound @object acl filter v._id == @client return e", { + object: a_object._id, + client: a_client_id, + }) + .toArray(); for (i in acls) { acl = acls[i]; @@ -2128,10 +2268,15 @@ module.exports = (function() { if (a_object.acls & 2) { //console.log("chk local group acls"); - acls = obj.db._query("for v, e, p in 2..2 outbound @object acl, outbound member filter p.vertices[2]._id == @client return p.edges[0]", { - object: a_object._id, - client: a_client_id - }).toArray(); + acls = obj.db + ._query( + "for v, e, p in 2..2 outbound @object acl, outbound member filter p.vertices[2]._id == @client return p.edges[0]", + { + object: a_object._id, + client: a_client_id, + }, + ) + .toArray(); for (i in acls) { acl = acls[i]; perm.grant |= acl.grant; @@ -2148,14 +2293,18 @@ module.exports = (function() { for (;;) { // Find all parent collections owned by object owner - parents = obj.db._query("for i in @children for v in 1..1 inbound i item return {_id:v._id,topic:v.topic,acls:v.acls}", { - children: children - }).toArray(); + parents = obj.db + ._query( + "for i in @children for v in 1..1 inbound i item return {_id:v._id,topic:v.topic,acls:v.acls}", + { + children: children, + }, + ) + .toArray(); //console.log("parents",parents); - if (parents.length == 0) - break; + if (parents.length == 0) break; for (i in parents) { parent = parents[i]; @@ -2165,45 +2314,52 @@ module.exports = (function() { perm.inherited |= obj.PERM_PUBLIC; - if ((a_req_perm & perm.inherited) == a_req_perm) - break; + if ((a_req_perm & perm.inherited) == a_req_perm) break; } // User ACL - if (parent.acls && ((parent.acls & 1) != 0)) { + if (parent.acls && (parent.acls & 1) != 0) { //console.log("chk par user acls"); - acls = obj.db._query("for v, e in 1..1 outbound @object acl filter v._id == @client return e", { - object: parent._id, - client: a_client_id - }).toArray(); + acls = obj.db + ._query( + "for v, e in 1..1 outbound @object acl filter v._id == @client return e", + { + object: parent._id, + client: a_client_id, + }, + ) + .toArray(); if (acls.length) { for (i in acls) { acl = acls[i]; perm.inherited |= acl.inhgrant; } - if ((a_req_perm & perm.inherited) == a_req_perm) - break; + if ((a_req_perm & perm.inherited) == a_req_perm) break; } } // Group ACL - if (parent.acls && ((parent.acls & 2) != 0)) { + if (parent.acls && (parent.acls & 2) != 0) { //console.log("chk par group acls"); - acls = obj.db._query("for v, e, p in 2..2 outbound @object acl, outbound member filter is_same_collection('g',p.vertices[1]) and p.vertices[2]._id == @client return p.edges[0]", { - object: parent._id, - client: a_client_id - }).toArray(); + acls = obj.db + ._query( + "for v, e, p in 2..2 outbound @object acl, outbound member filter is_same_collection('g',p.vertices[1]) and p.vertices[2]._id == @client return p.edges[0]", + { + object: parent._id, + client: a_client_id, + }, + ) + .toArray(); if (acls.length) { for (i in acls) { acl = acls[i]; perm.inherited |= acl.inhgrant; } - if ((a_req_perm & perm.inherited) == a_req_perm) - break; + if ((a_req_perm & perm.inherited) == a_req_perm) break; } } } @@ -2217,7 +2373,7 @@ module.exports = (function() { return perm; }; - obj.getACLOwnersBySubject = function(subject, inc_users, inc_projects) { + obj.getACLOwnersBySubject = function (subject, inc_users, inc_projects) { var results = []; /* Get users and projects that have shared data or collections with subject: @@ -2229,21 +2385,22 @@ module.exports = (function() { if (inc_users || inc_projects) { var ids = new Set(), ignore = new Set(), - owner_id, acl, acls = obj.db.acl.byExample({ - _to: subject + owner_id, + acl, + acls = obj.db.acl.byExample({ + _to: subject, }); // Find direct ACLs (not through a group) while (acls.hasNext()) { acl = acls.next(); owner_id = obj.db.owner.firstExample({ - _from: acl._from + _from: acl._from, })._to; - if (owner_id.charAt(0) == 'p') { + if (owner_id.charAt(0) == "p") { if (inc_projects) { - if (ids.has(owner_id) || ignore.has(owner_id)) - continue; + if (ids.has(owner_id) || ignore.has(owner_id)) continue; if (obj.getProjectRole(subject, owner_id) == obj.PROJ_NO_ROLE) { ids.add(owner_id); @@ -2257,23 +2414,25 @@ module.exports = (function() { } // Find indirect ACLs (through a group) - var mem, members = obj.db.member.byExample({ - _to: subject - }); + var mem, + members = obj.db.member.byExample({ + _to: subject, + }); while (members.hasNext()) { mem = members.next(); // Group must have at least one ACL set; otherwise ignore it - if (obj.db.acl.firstExample({ - _to: mem._from - })) { + if ( + obj.db.acl.firstExample({ + _to: mem._from, + }) + ) { owner_id = obj.db.owner.firstExample({ - _from: mem._from + _from: mem._from, })._to; - if (owner_id.charAt(0) == 'p') { + if (owner_id.charAt(0) == "p") { if (inc_projects) { - if (ids.has(owner_id) || ignore.has(owner_id)) - continue; + if (ids.has(owner_id) || ignore.has(owner_id)) continue; if (obj.getProjectRole(subject, owner_id) == obj.PROJ_NO_ROLE) { ids.add(owner_id); @@ -2289,76 +2448,91 @@ module.exports = (function() { var doc, title; - ids.forEach(function(id) { + ids.forEach(function (id) { doc = obj.db._document(id); - if (doc.title) - title = doc.title; - else - title = doc.name_last + ", " + doc.name_first; + if (doc.title) title = doc.title; + else title = doc.name_last + ", " + doc.name_first; results.push({ id: doc._id, title: title, - owner: doc.owner + owner: doc.owner, }); }); - results.sort(function(a, b) { - if (a.id < b.id) - return -1; - else if (a.id > b.id) - return 1; - else - return 0; + results.sort(function (a, b) { + if (a.id < b.id) return -1; + else if (a.id > b.id) return 1; + else return 0; }); } return results; }; - obj.usersWithClientACLs = function(client_id, id_only) { + obj.usersWithClientACLs = function (client_id, id_only) { var result; if (id_only) { - result = obj.db._query("for x in union_distinct((for v in 2..2 inbound @user acl, outbound owner filter is_same_collection('u',v) return v._id),(for v,e,p in 3..3 inbound @user member, acl, outbound owner filter is_same_collection('g',p.vertices[1]) and is_same_collection('acl',p.edges[1]) and is_same_collection('u',v) return v._id)) return x", { - user: client_id - }).toArray(); + result = obj.db + ._query( + "for x in union_distinct((for v in 2..2 inbound @user acl, outbound owner filter is_same_collection('u',v) return v._id),(for v,e,p in 3..3 inbound @user member, acl, outbound owner filter is_same_collection('g',p.vertices[1]) and is_same_collection('acl',p.edges[1]) and is_same_collection('u',v) return v._id)) return x", + { + user: client_id, + }, + ) + .toArray(); } else { - result = obj.db._query("for x in union_distinct((for v in 2..2 inbound @user acl, outbound owner filter is_same_collection('u',v) return {uid:v._id,name:v.name}),(for v,e,p in 3..3 inbound @user member, acl, outbound owner filter is_same_collection('g',p.vertices[1]) and is_same_collection('acl',p.edges[1]) and is_same_collection('u',v) return {uid:v._id,name:v.name})) sort x.name return x", { - user: client_id - }).toArray(); + result = obj.db + ._query( + "for x in union_distinct((for v in 2..2 inbound @user acl, outbound owner filter is_same_collection('u',v) return {uid:v._id,name:v.name}),(for v,e,p in 3..3 inbound @user member, acl, outbound owner filter is_same_collection('g',p.vertices[1]) and is_same_collection('acl',p.edges[1]) and is_same_collection('u',v) return {uid:v._id,name:v.name})) sort x.name return x", + { + user: client_id, + }, + ) + .toArray(); } //console.log("usersWithACLs:",result); return result; }; - obj.projectsWithClientACLs = function(client_id, id_only) { + obj.projectsWithClientACLs = function (client_id, id_only) { // Get projects that have ACLs set for client AND where client is not owner, admin, or member of project var result; if (id_only) { - result = obj.db._query("for i in minus((for v in 2..2 inbound @user member, acl, outbound owner filter is_same_collection('p',v) return v._id),(for v,e,p in 2..2 inbound @user member, outbound owner filter p.vertices[1].gid == 'members' and is_same_collection('p',v) return v._id)) return i", { - user: client_id - }); + result = obj.db._query( + "for i in minus((for v in 2..2 inbound @user member, acl, outbound owner filter is_same_collection('p',v) return v._id),(for v,e,p in 2..2 inbound @user member, outbound owner filter p.vertices[1].gid == 'members' and is_same_collection('p',v) return v._id)) return i", + { + user: client_id, + }, + ); } else { - result = obj.db._query("for i in minus((for v in 2..2 inbound @user member, acl, outbound owner filter is_same_collection('p',v) return {id:v._id,title:v.title}),(for v,e,p in 2..2 inbound @user member, outbound owner filter p.vertices[1].gid == 'members' and is_same_collection('p',v) return {id:v._id,title:v.title})) return i", { - user: client_id - }); + result = obj.db._query( + "for i in minus((for v in 2..2 inbound @user member, acl, outbound owner filter is_same_collection('p',v) return {id:v._id,title:v.title}),(for v,e,p in 2..2 inbound @user member, outbound owner filter p.vertices[1].gid == 'members' and is_same_collection('p',v) return {id:v._id,title:v.title})) return i", + { + user: client_id, + }, + ); } //console.log("projectsWithACLs:",result); return result.toArray(); }; - obj.checkDependencies = function(id, src, depth) { + obj.checkDependencies = function (id, src, depth) { //console.log("checkdep ",id,src,depth); - var dep, deps = obj.db._query("for v in 1..1 outbound @id dep return v._id", { - id: id - }); + var dep, + deps = obj.db._query("for v in 1..1 outbound @id dep return v._id", { + id: id, + }); if (!depth || depth < 50) { //console.log("checkdep depth ok"); while (deps.hasNext()) { //console.log("has next"); dep = deps.next(); if (dep == src) - throw [obj.ERR_INVALID_PARAM, "Circular dependency detected in references, from " + id]; + throw [ + obj.ERR_INVALID_PARAM, + "Circular dependency detected in references, from " + id, + ]; obj.checkDependencies(dep, src ? src : id, depth + 1); } } @@ -2366,31 +2540,42 @@ module.exports = (function() { // Returns bitmask: CLOSED_BIT (1) | OPEN_TYPE_INH (4) | OPEN_TYPE (4) | ACTIVE_TYPE (4) - //obj.annotationGetMask = function( a_client, a_subj_id, a_admin ){ - obj.getNoteMask = function(a_client, a_subj, a_admin) { + obj.getNoteMask = function (a_client, a_subj, a_admin) { var mask = 0, - res, n, b, id = a_subj.id || a_subj._id; + res, + n, + b, + id = a_subj.id || a_subj._id; if (a_client) { if (a_admin || (a_admin === undefined && obj.hasAdminPermObject(a_client, id))) { // Owner/admin - return notes that are open or active - res = obj.db._query("for n in 1..1 outbound @id note return {type:n.type,state:n.state,parent_id:n.parent_id}", { - id: id - }); + res = obj.db._query( + "for n in 1..1 outbound @id note return {type:n.type,state:n.state,parent_id:n.parent_id}", + { + id: id, + }, + ); } else { // Non-owner - return notes that are active // Creator - return notes that are open or active - res = obj.db._query("for n in 1..1 outbound @id note filter n.state == 2 || n.creator == @client return {type:n.type,state:n.state,parent_id:n.parent_id}", { - id: id, - client: a_client._id - }); + res = obj.db._query( + "for n in 1..1 outbound @id note filter n.state == 2 || n.creator == @client return {type:n.type,state:n.state,parent_id:n.parent_id}", + { + id: id, + client: a_client._id, + }, + ); } } else { // Annonymous - return notes that are active - res = obj.db._query("for n in 1..1 outbound @id note filter n.state == 2 return {type:n.type,state:n.state,parent_id:n.parent_id}", { - id: id - }); + res = obj.db._query( + "for n in 1..1 outbound @id note filter n.state == 2 return {type:n.type,state:n.state,parent_id:n.parent_id}", + { + id: id, + }, + ); } // Shift note type bits based on inherited, open, active state @@ -2401,10 +2586,8 @@ module.exports = (function() { mask |= obj.NOTE_MASK_CLS_ANY; } else { b = 1 << n.type; - if (n.parent_id && n.state == obj.NOTE_OPEN) - b <<= 8; - else if (n.state == obj.NOTE_OPEN) - b <<= 4; + if (n.parent_id && n.state == obj.NOTE_OPEN) b <<= 8; + else if (n.state == obj.NOTE_OPEN) b <<= 4; mask |= b; } @@ -2417,12 +2600,14 @@ module.exports = (function() { return mask; }; - obj.annotationInitDependents = function(a_client, a_parent_note, a_updates) { + obj.annotationInitDependents = function (a_client, a_parent_note, a_updates) { //console.log("annotationInitDependents",a_parent_note._id); var subj = obj.db._document(a_parent_note.subject_id), - note, dep, deps = obj.db._query("for v,e in 1..1 inbound @id dep filter e.type < 2 return v", { - id: subj._id + note, + dep, + deps = obj.db._query("for v,e in 1..1 inbound @id dep filter e.type < 2 return v", { + id: subj._id, }), time = Math.floor(Date.now() / 1000), new_note = { @@ -2433,14 +2618,20 @@ module.exports = (function() { ct: time, ut: time, title: a_parent_note.title, - comments: [{ - user: a_parent_note.creator, - new_type: a_parent_note.type, - new_state: obj.NOTE_OPEN, - time: time, - comment: "Impact assessment needed due to issue on direct ancestor '" + a_parent_note.subject_id + - "'. Original issue description: \"" + a_parent_note.comments[0].comment + "\"" - }] + comments: [ + { + user: a_parent_note.creator, + new_type: a_parent_note.type, + new_state: obj.NOTE_OPEN, + time: time, + comment: + "Impact assessment needed due to issue on direct ancestor '" + + a_parent_note.subject_id + + "'. Original issue description: \"" + + a_parent_note.comments[0].comment + + '"', + }, + ], }; // Create new linked annotation for each dependent @@ -2449,15 +2640,15 @@ module.exports = (function() { //console.log("dep:",dep._id); new_note.subject_id = dep._id; note = obj.db.n.save(new_note, { - returnNew: true + returnNew: true, }); obj.db.note.save({ _from: dep._id, - _to: note.new._id + _to: note.new._id, }); obj.db.note.save({ _from: note.new._id, - _to: a_parent_note._id + _to: a_parent_note._id, }); // Add update listing data if not present @@ -2475,25 +2666,33 @@ module.exports = (function() { children to match. For other types, close all children. For errors and warnings, if state is changed to active, reopen any closed children; otherwise, if open or closed, close all children. For child notes that have already been activated, must recurse to all children as needed. */ - obj.annotationUpdateDependents = function(a_client, a_parent_note, a_prev_type, a_prev_state, a_updates) { - if (a_parent_note.type == a_prev_type && (a_parent_note.state == a_prev_state || (a_parent_note.state != obj.NOTE_ACTIVE && a_prev_state != obj.NOTE_ACTIVE))) + obj.annotationUpdateDependents = function ( + a_client, + a_parent_note, + a_prev_type, + a_prev_state, + a_updates, + ) { + if ( + a_parent_note.type == a_prev_type && + (a_parent_note.state == a_prev_state || + (a_parent_note.state != obj.NOTE_ACTIVE && a_prev_state != obj.NOTE_ACTIVE)) + ) return; var time = Math.floor(Date.now() / 1000), upd = { type: a_parent_note.type, - ut: time + ut: time, }, comment = { user: a_client._id, - time: time + time: time, }; - if (a_parent_note.type != a_prev_type) - comment.new_type = a_parent_note.type; + if (a_parent_note.type != a_prev_type) comment.new_type = a_parent_note.type; - if (a_parent_note.state != a_prev_state) - comment.new_state = a_parent_note.state; + if (a_parent_note.state != a_prev_state) comment.new_state = a_parent_note.state; if (a_parent_note.type >= obj.NOTE_WARN && a_parent_note.state == obj.NOTE_ACTIVE) { upd.state = obj.NOTE_OPEN; @@ -2501,11 +2700,8 @@ module.exports = (function() { if (a_parent_note.type != a_prev_type && a_parent_note.state != a_prev_state) comment.comment += "type and state"; - else if (a_parent_note.state != a_prev_state) - comment.comment += "type"; - else - comment.comment += "state"; - + else if (a_parent_note.state != a_prev_state) comment.comment += "type"; + else comment.comment += "state"; } else { upd.state = obj.NOTE_CLOSED; comment.comment = "Impact assessment invalidated due to change of state"; @@ -2517,20 +2713,29 @@ module.exports = (function() { client: a_client, note_upd: upd, comment: comment, - updates: a_updates + updates: a_updates, }; // Recurse full tree only if type is changing or state is changed from active to open or closed - if (comment.new_type != undefined || (a_parent_note.state != obj.NOTE_ACTIVE && a_prev_state == obj.NOTE_ACTIVE)) + if ( + comment.new_type != undefined || + (a_parent_note.state != obj.NOTE_ACTIVE && a_prev_state == obj.NOTE_ACTIVE) + ) context.recurse = true; obj.annotationUpdateDependents_Recurse(a_parent_note._id, context); }; - obj.annotationUpdateDependents_Recurse = function(a_note_id, a_context, a_close) { - var old_state, note, subj, deps = obj.db._query("for v in 1..1 inbound @id note filter is_same_collection('n',v) return v", { - id: a_note_id - }); + obj.annotationUpdateDependents_Recurse = function (a_note_id, a_context, a_close) { + var old_state, + note, + subj, + deps = obj.db._query( + "for v in 1..1 inbound @id note filter is_same_collection('n',v) return v", + { + id: a_note_id, + }, + ); if (a_close) { old_state = a_context.note_upd.state; @@ -2547,7 +2752,10 @@ module.exports = (function() { // Add/refresh update listing data if (note.subject_id in a_context.updates) { - a_context.updates[note.subject_id].notes = obj.getNoteMask(a_context.client, a_context.updates[note.subject_id]); + a_context.updates[note.subject_id].notes = obj.getNoteMask( + a_context.client, + a_context.updates[note.subject_id], + ); } else { subj = obj.db._document(note.subject_id); subj.notes = obj.getNoteMask(a_context.client, subj); @@ -2566,11 +2774,12 @@ module.exports = (function() { } }; - obj.annotationDelete = function(a_id, a_update_ids) { + obj.annotationDelete = function (a_id, a_update_ids) { //console.log("delete note:",a_id); - var n, notes = obj.db.note.byExample({ - _to: a_id - }); + var n, + notes = obj.db.note.byExample({ + _to: a_id, + }); while (notes.hasNext()) { n = notes.next(); @@ -2580,12 +2789,16 @@ module.exports = (function() { } n = obj.db.n.document(a_id); - if (a_update_ids) - a_update_ids.add(n.subject_id); + if (a_update_ids) a_update_ids.add(n.subject_id); obj.graph.n.remove(a_id); }; - obj.annotationDependenciesUpdated = function(a_data, a_dep_ids_added, a_dep_ids_removed, a_update_ids) { + obj.annotationDependenciesUpdated = function ( + a_data, + a_dep_ids_added, + a_dep_ids_removed, + a_update_ids, + ) { //console.log("annotationDependenciesUpdated",a_data._id); // Called when dependencies are added/removed to/from existing/new data record var res, qry_res; @@ -2596,9 +2809,12 @@ module.exports = (function() { //console.log("deletings notes from:",a_data._id); // Find local annotations linked to upstream dependencies - qry_res = obj.db._query("for v,e,p in 3..3 any @src note filter is_same_collection('d',v) && p.edges[1]._from == p.vertices[1]._id return {src: p.vertices[1], dst: p.vertices[3]}", { - src: a_data._id - }); + qry_res = obj.db._query( + "for v,e,p in 3..3 any @src note filter is_same_collection('d',v) && p.edges[1]._from == p.vertices[1]._id return {src: p.vertices[1], dst: p.vertices[3]}", + { + src: a_data._id, + }, + ); while (qry_res.hasNext()) { res = qry_res.next(); @@ -2616,9 +2832,12 @@ module.exports = (function() { if (a_dep_ids_added) { //console.log("deps added:",Array.from( a_dep_ids_added )); // Get all annotations from new dependencies that may need to be propagated - qry_res = obj.db._query("for i in @src for v in 1..1 outbound i note filter v.type > 1 return v", { - src: Array.from(a_dep_ids_added) - }); + qry_res = obj.db._query( + "for i in @src for v in 1..1 outbound i note filter v.type > 1 return v", + { + src: Array.from(a_dep_ids_added), + }, + ); if (qry_res.hasNext()) { var time = Math.floor(Date.now() / 1000), @@ -2629,9 +2848,14 @@ module.exports = (function() { //console.log("dep:",res); // Only need to propagate if dependents are already present or state is active - if (res.state == obj.NOTE_ACTIVE || obj.db.note.byExample({ - _to: res._id - }).count() > 1) { + if ( + res.state == obj.NOTE_ACTIVE || + obj.db.note + .byExample({ + _to: res._id, + }) + .count() > 1 + ) { //console.log("propagate"); new_note = { state: obj.NOTE_OPEN, @@ -2642,26 +2866,32 @@ module.exports = (function() { ct: time, ut: time, title: res.title, - comments: [{ - user: res.creator, - new_type: res.type, - new_state: obj.NOTE_OPEN, - time: time, - comment: "Impact assessment needed due to issue on direct ancestor '" + res.subject_id + - "'. Original issue description: \"" + res.comments[0].comment + "\"" - }] + comments: [ + { + user: res.creator, + new_type: res.type, + new_state: obj.NOTE_OPEN, + time: time, + comment: + "Impact assessment needed due to issue on direct ancestor '" + + res.subject_id + + "'. Original issue description: \"" + + res.comments[0].comment + + '"', + }, + ], }; new_note = obj.db.n.save(new_note, { - returnNew: true + returnNew: true, }).new; obj.db.note.save({ _from: a_data._id, - _to: new_note._id + _to: new_note._id, }); obj.db.note.save({ _from: new_note._id, - _to: res._id + _to: res._id, }); } } @@ -2669,7 +2899,7 @@ module.exports = (function() { } }; - obj.addTags = function(a_tags) { + obj.addTags = function (a_tags) { //console.log("addTags",a_tags); var id, tag, j, code; @@ -2681,7 +2911,7 @@ module.exports = (function() { //console.log( "update", id ); tag = obj.db.tag.document(id); obj.db._update(id, { - count: tag.count + 1 + count: tag.count + 1, }); } else { //console.log( "save", id ); @@ -2690,21 +2920,24 @@ module.exports = (function() { for (j = 0; j < tag.length; j++) { code = tag.charCodeAt(j); - if (!(code > 47 && code < 58) && // numeric (0-9) + if ( + !(code > 47 && code < 58) && // numeric (0-9) !(code > 96 && code < 123) && // lower alpha (a-z) - code !== 45) // "-" + code !== 45 + ) + // "-" throw [obj.ERR_INVALID_CHAR, "Invalid character(s) in tag."]; } obj.db.tag.save({ _key: tag, - count: 1 + count: 1, }); } } }; - obj.removeTags = function(a_tags) { + obj.removeTags = function (a_tags) { //console.log("removeTags",a_tags); var id, tag; @@ -2715,7 +2948,7 @@ module.exports = (function() { if (tag.count > 1) { //console.log("update",id); obj.db._update(id, { - count: tag.count - 1 + count: tag.count - 1, }); } else { //console.log("remove",id); @@ -2725,18 +2958,16 @@ module.exports = (function() { } }; - obj.saveRecentGlobusPath = function(a_client, a_path, a_mode) { + obj.saveRecentGlobusPath = function (a_client, a_path, a_mode) { var path = a_path, idx = a_path.lastIndexOf("/"); if (a_mode == obj.TT_DATA_PUT) { // For PUT, strip off filename but keep last slash - if (idx > 0) - path = a_path.substr(0, idx + 1); + if (idx > 0) path = a_path.substr(0, idx + 1); } else { // For GET, make sure path ends in a slash - if (idx != a_path.length - 1) - path += "/"; + if (idx != a_path.length - 1) path += "/"; } if (a_client.eps && a_client.eps.length) { @@ -2754,17 +2985,21 @@ module.exports = (function() { } obj.db._update(a_client._id, { - eps: a_client.eps + eps: a_client.eps, }); }; // All col IDs are from same scope (owner), exapnd to include all readable sub-collections // Collections must exist // TODO This would be more efficient as a recursive function - obj.expandSearchCollections2 = function(a_client, a_col_ids) { + obj.expandSearchCollections2 = function (a_client, a_col_ids) { var cols = new Set(a_col_ids), - c, col, cur, next = a_col_ids, - perm, child; + c, + col, + cur, + next = a_col_ids, + perm, + child; while (next) { cur = next; @@ -2774,9 +3009,12 @@ module.exports = (function() { col = obj.db.c.document(cur[c]); if (obj.hasAdminPermObject(a_client, col._id)) { - child = obj.db._query("for i in 1..10 outbound @col item filter is_same_collection('c',i) return i._id", { - col: col._id - }); + child = obj.db._query( + "for i in 1..10 outbound @col item filter is_same_collection('c',i) return i._id", + { + col: col._id, + }, + ); if (!cols.has(col._id)) { cols.add(col._id); @@ -2789,20 +3027,37 @@ module.exports = (function() { } } } else { - perm = obj.getPermissionsLocal(a_client._id, col, true, obj.PERM_RD_REC | obj.PERM_LIST); - - if (perm.grant & (obj.PERM_RD_REC | obj.PERM_LIST) != (obj.PERM_RD_REC | obj.PERM_LIST)) { - throw [obj.ERR_PERM_DENIED, "Permission denied for collection '" + col._id + "'"]; + perm = obj.getPermissionsLocal( + a_client._id, + col, + true, + obj.PERM_RD_REC | obj.PERM_LIST, + ); + + if ( + perm.grant & + ((obj.PERM_RD_REC | obj.PERM_LIST) != (obj.PERM_RD_REC | obj.PERM_LIST)) + ) { + throw [ + obj.ERR_PERM_DENIED, + "Permission denied for collection '" + col._id + "'", + ]; } if (!cols.has(col._id)) { cols.add(col._id); } - if (perm.inhgrant & (obj.PERM_RD_REC | obj.PERM_LIST) == (obj.PERM_RD_REC | obj.PERM_LIST)) { - child = obj.db._query("for i in 1..10 outbound @col item filter is_same_collection('c',i) return i._id", { - col: col._id - }); + if ( + perm.inhgrant & + ((obj.PERM_RD_REC | obj.PERM_LIST) == (obj.PERM_RD_REC | obj.PERM_LIST)) + ) { + child = obj.db._query( + "for i in 1..10 outbound @col item filter is_same_collection('c',i) return i._id", + { + col: col._id, + }, + ); while (child.hasNext()) { col = child.next(); if (!cols.has(col)) { @@ -2810,9 +3065,12 @@ module.exports = (function() { } } } else { - child = obj.db._query("for i in 1..1 outbound @col item filter is_same_collection('c',i) return i._id", { - col: col._id - }); + child = obj.db._query( + "for i in 1..1 outbound @col item filter is_same_collection('c',i) return i._id", + { + col: col._id, + }, + ); while (child.hasNext()) { next.push(child.next()); } @@ -2824,7 +3082,7 @@ module.exports = (function() { return Array.from(cols); }; - obj.expandSearchCollections = function(a_client, a_col_ids) { + obj.expandSearchCollections = function (a_client, a_col_ids) { var cols = new Set(); for (var c in a_col_ids) { obj.expandSearchCollections_recurse(a_client, cols, a_col_ids[c]); @@ -2832,16 +3090,19 @@ module.exports = (function() { return Array.from(cols); }; - obj.expandSearchCollections_recurse = function(a_client, a_cols, a_col_id, a_inh_perm) { + obj.expandSearchCollections_recurse = function (a_client, a_cols, a_col_id, a_inh_perm) { if (!a_cols.has(a_col_id)) { //console.log("expColl",a_col_id,"inh:",a_inh_perm); var col, res; if (obj.hasAdminPermObject(a_client, a_col_id)) { a_cols.add(a_col_id); //console.log("has admin"); - res = obj.db._query("for i in 1..10 outbound @col item filter is_same_collection('c',i) return i._id", { - col: a_col_id - }); + res = obj.db._query( + "for i in 1..10 outbound @col item filter is_same_collection('c',i) return i._id", + { + col: a_col_id, + }, + ); while (res.hasNext()) { col = res.next(); if (!a_cols.has(col)) { @@ -2851,13 +3112,25 @@ module.exports = (function() { } else { col = obj.db.c.document(a_col_id); - var perm = obj.getPermissionsLocal(a_client._id, col, a_inh_perm == undefined ? true : false, obj.PERM_RD_REC | obj.PERM_LIST); + var perm = obj.getPermissionsLocal( + a_client._id, + col, + a_inh_perm == undefined ? true : false, + obj.PERM_RD_REC | obj.PERM_LIST, + ); //console.log("perm",perm); - if (((perm.grant | perm.inherited | a_inh_perm) & (obj.PERM_RD_REC | obj.PERM_LIST)) != (obj.PERM_RD_REC | obj.PERM_LIST)) { + if ( + ((perm.grant | perm.inherited | a_inh_perm) & + (obj.PERM_RD_REC | obj.PERM_LIST)) != + (obj.PERM_RD_REC | obj.PERM_LIST) + ) { if (a_inh_perm == undefined) { // Only throw a PERM_DENIED error if this is one of the user-specified collections (not a child) - throw [obj.ERR_PERM_DENIED, "Permission denied for collection '" + col._id + "'"]; + throw [ + obj.ERR_PERM_DENIED, + "Permission denied for collection '" + col._id + "'", + ]; } else { // Don't have access - skip return; @@ -2868,12 +3141,19 @@ module.exports = (function() { a_cols.add(a_col_id); - if (((perm.inhgrant | perm.inherited | a_inh_perm) & (obj.PERM_RD_REC | obj.PERM_LIST)) == (obj.PERM_RD_REC | obj.PERM_LIST)) { + if ( + ((perm.inhgrant | perm.inherited | a_inh_perm) & + (obj.PERM_RD_REC | obj.PERM_LIST)) == + (obj.PERM_RD_REC | obj.PERM_LIST) + ) { //console.log("have all inh perms"); - res = obj.db._query("for i in 1..10 outbound @col item filter is_same_collection('c',i) return i._id", { - col: a_col_id - }); + res = obj.db._query( + "for i in 1..10 outbound @col item filter is_same_collection('c',i) return i._id", + { + col: a_col_id, + }, + ); while (res.hasNext()) { col = res.next(); if (!a_cols.has(col)) { @@ -2881,10 +3161,13 @@ module.exports = (function() { } } } else { - res = obj.db._query("for i in 1..1 outbound @col item filter is_same_collection('c',i) return i._id", { - col: col._id - }); - perm = (perm.inhgrant | perm.inherited | a_inh_perm); + res = obj.db._query( + "for i in 1..1 outbound @col item filter is_same_collection('c',i) return i._id", + { + col: col._id, + }, + ); + perm = perm.inhgrant | perm.inherited | a_inh_perm; //console.log("not all inh perms", perm); while (res.hasNext()) { @@ -2896,4 +3179,4 @@ module.exports = (function() { }; return obj; -}()); +})(); From eaf34fa168d21f3916a83d9e79ec90b6947983a6 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Thu, 2 Jan 2025 16:49:36 -0500 Subject: [PATCH 35/38] Format authz test --- core/database/foxx/tests/authz.test.js | 87 +++++++++++++------------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/core/database/foxx/tests/authz.test.js b/core/database/foxx/tests/authz.test.js index 6a0ac51c6..a6a8bef35 100644 --- a/core/database/foxx/tests/authz.test.js +++ b/core/database/foxx/tests/authz.test.js @@ -8,49 +8,46 @@ const g_lib = require("../api/support"); const arangodb = require("@arangodb"); describe("Authz functions", () => { - - beforeEach(() => { - g_db.d.truncate(); - g_db.alloc.truncate(); - g_db.loc.truncate(); - g_db.repo.truncate(); - g_db.u.truncate(); - }); - - it("unit_authz: if admin 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: "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.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; - }); - + beforeEach(() => { + g_db.d.truncate(); + g_db.alloc.truncate(); + g_db.loc.truncate(); + g_db.repo.truncate(); + g_db.u.truncate(); + }); + + it("unit_authz: if admin 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: "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.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; + }); }); From 3771f9cae50451e83dc48398600bbfd7442a805c Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Thu, 2 Jan 2025 16:50:24 -0500 Subject: [PATCH 36/38] Format authz file --- core/database/foxx/api/authz.js | 108 ++++++++++++++++---------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/core/database/foxx/api/authz.js b/core/database/foxx/api/authz.js index 4f087de6a..f6307824e 100644 --- a/core/database/foxx/api/authz.js +++ b/core/database/foxx/api/authz.js @@ -1,59 +1,59 @@ -'use strict'; +"use strict"; -const g_db = require('@arangodb').db; -const path = require('path'); -const g_lib = require('./support'); +const g_db = require("@arangodb").db; +const path = require("path"); +const g_lib = require("./support"); -module.exports = (function() { - let obj = {} +module.exports = (function () { + let obj = {}; - /** - * @brief Will check to see if a client has the required permissions on a - * record. - * - * @param {string} a_data_key - A datafed key associated with a record. Is not prepended with 'd/' - * @param {obj} a_client - A user document, the user associated with the document is the one - * who we are verifying if they have permissions to on the data record. - * - * e.g. - * - * a_client id - * - * 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" - * } - * - * @param - the permission type that is being checked i.e. - * - * PERM_CREATE - * PERM_WR_DATA - * PERM_RD_DATA - **/ - obj.isRecordActionAuthorized = function(a_client, a_data_key, a_perm) { - const data_id = "d/" + a_data_key; - // If the user is not an admin of the object we will need - // to check if the user has the write authorization - if (g_lib.hasAdminPermObject(a_client, data_id)) { - return true; - } - let data = g_db.d.document(data_id); - // Grab the data item - if (g_lib.hasPermissions(a_client, data, a_perm)) { - return true; - } - return false; - }; + /** + * @brief Will check to see if a client has the required permissions on a + * record. + * + * @param {string} a_data_key - A datafed key associated with a record. Is not prepended with 'd/' + * @param {obj} a_client - A user document, the user associated with the document is the one + * who we are verifying if they have permissions to on the data record. + * + * e.g. + * + * a_client id + * + * 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" + * } + * + * @param - the permission type that is being checked i.e. + * + * PERM_CREATE + * PERM_WR_DATA + * PERM_RD_DATA + **/ + obj.isRecordActionAuthorized = function (a_client, a_data_key, a_perm) { + const data_id = "d/" + a_data_key; + // If the user is not an admin of the object we will need + // to check if the user has the write authorization + if (g_lib.hasAdminPermObject(a_client, data_id)) { + return true; + } + let data = g_db.d.document(data_id); + // Grab the data item + if (g_lib.hasPermissions(a_client, data, a_perm)) { + return true; + } + return false; + }; - return obj; + return obj; })(); From 7dae7ca830e8529ae4923137663ea3fdbeb95238 Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Thu, 2 Jan 2025 16:51:04 -0500 Subject: [PATCH 37/38] Apply formatting to posix path --- core/database/foxx/api/posix_path.js | 51 ++++++++++++++-------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/core/database/foxx/api/posix_path.js b/core/database/foxx/api/posix_path.js index 3ae3849bb..fb6fce612 100644 --- a/core/database/foxx/api/posix_path.js +++ b/core/database/foxx/api/posix_path.js @@ -1,32 +1,31 @@ -'use strict'; +"use strict"; -const path = require('path'); +const path = require("path"); -module.exports = (function() { - let obj = {} +module.exports = (function () { + let obj = {}; - /** - * \brief will split a path string into components - * - * Example POSIX path - * const posixPath = '/usr/local/bin/node'; - * - * output: ['usr', 'local', 'bin', 'node'] - * - **/ - obj.splitPOSIXPath = function(a_posix_path) { + /** + * \brief will split a path string into components + * + * Example POSIX path + * const posixPath = '/usr/local/bin/node'; + * + * output: ['usr', 'local', 'bin', 'node'] + * + **/ + obj.splitPOSIXPath = function (a_posix_path) { + if (!a_posix_path || typeof a_posix_path !== "string") { + throw new Error("Invalid POSIX path"); + } + // Split the path into components + // components: ['', 'usr', 'local', 'bin', 'node'] + // The empty '' is for root + const components = a_posix_path.split(path.posix.sep); - if (!a_posix_path || typeof a_posix_path !== 'string') { - throw new Error('Invalid POSIX path'); - } - // Split the path into components - // components: ['', 'usr', 'local', 'bin', 'node'] - // The empty '' is for root - const components = a_posix_path.split(path.posix.sep); + // components: ['usr', 'local', 'bin', 'node'] + return components.filter((component) => component !== ""); + }; - // components: ['usr', 'local', 'bin', 'node'] - return components.filter(component => component !== ''); - } - - return obj; + return obj; })(); From 2bb1213c945ee94523f895a4137a223fca55d6ea Mon Sep 17 00:00:00 2001 From: JoshuaSBrown Date: Thu, 2 Jan 2025 16:54:20 -0500 Subject: [PATCH 38/38] Address formatting issues --- core/database/foxx/api/record.js | 354 +++++++++---------- core/database/foxx/tests/path.test.js | 56 +-- core/database/foxx/tests/record.test.js | 438 ++++++++++++------------ 3 files changed, 421 insertions(+), 427 deletions(-) diff --git a/core/database/foxx/api/record.js b/core/database/foxx/api/record.js index 196a72065..5f395790f 100644 --- a/core/database/foxx/api/record.js +++ b/core/database/foxx/api/record.js @@ -9,192 +9,192 @@ const { errors } = require("@arangodb"); * @brief Represents a record in the database and provides methods to manage it. */ class Record { - // ERROR code - #error = null; - // Error message should be a string if defined - #err_msg = null; - // Boolean value that determines if the record exists in the database - #exists = false; - // location object, determines what the allocation the data item is associated with - #loc = null; - // Allocation object, determines what allocation data item is associated with - #alloc = null; - // The data key - #key = null; - // The data id simply the key prepended with 'd/' - #data_id = null; - - /** - * @brief Constructs a Record object and checks if the key exists in the database. - * @param {string} a_key - The unique identifier for the record. - */ - constructor(a_key) { - // Define the collection - const collection = g_db._collection("d"); - - // 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. - this.#key = a_key; - this.#data_id = "d/" + a_key; - if (a_key) { - // Check if the document exists - try { - if (collection.exists(this.#key)) { - this.#exists = true; - } else { - this.#exists = false; - this.#error = g_lib.ERR_NOT_FOUND; - this.#err_msg = "Invalid key: (" + a_key + "). No record found."; + // ERROR code + #error = null; + // Error message should be a string if defined + #err_msg = null; + // Boolean value that determines if the record exists in the database + #exists = false; + // location object, determines what the allocation the data item is associated with + #loc = null; + // Allocation object, determines what allocation data item is associated with + #alloc = null; + // The data key + #key = null; + // The data id simply the key prepended with 'd/' + #data_id = null; + + /** + * @brief Constructs a Record object and checks if the key exists in the database. + * @param {string} a_key - The unique identifier for the record. + */ + constructor(a_key) { + // Define the collection + const collection = g_db._collection("d"); + + // 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. + this.#key = a_key; + this.#data_id = "d/" + a_key; + if (a_key) { + // Check if the document exists + try { + if (collection.exists(this.#key)) { + this.#exists = true; + } else { + this.#exists = false; + this.#error = g_lib.ERR_NOT_FOUND; + this.#err_msg = "Invalid key: (" + 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); + } } - } catch (e) { - this.#exists = false; - this.#error = g_lib.ERR_INTERNAL_FAULT; - this.#err_msg = "Unknown error encountered."; - console.log(e); - } } - } - - /** - * @brief will create the path to key as it should appear on the repository. - **/ - _pathToRecord(basePath) { - return basePath.endsWith("/") - ? basePath + this.#key - : basePath + "/" + this.#key; - } - - /** - * @brief Compares two paths and if an error is detected will save the error code and message. - **/ - _comparePaths(storedPath, inputPath) { - if (storedPath !== inputPath) { - this.#error = g_lib.ERR_PERM_DENIED; - this.#err_msg = - "Record path is not consistent with repo expected path is: " + - storedPath + - " attempted path is " + - inputPath; - return false; + + /** + * @brief will create the path to key as it should appear on the repository. + **/ + _pathToRecord(basePath) { + return basePath.endsWith("/") ? basePath + this.#key : basePath + "/" + this.#key; } - return true; - } - - /** - * @brief Checks if the record exists in the database. - * @return {boolean} True if the record exists, otherwise false. - */ - exists() { - return this.#exists; - } - - key() { - return this.#key; - } - - id() { - return this.#data_id; - } - /** - * @brief Will return error code of last run method. - * - * If no error code, will return null - **/ - error() { - return this.#error; - } - - /** - * @brief Retrieves the error code of the last run method. - * @return {string|null} Error code or null if no error. - */ - errorMessage() { - return this.#err_msg; - } - - /** - * @brief Checks if the record is managed by DataFed. - * @return {boolean} True if managed, otherwise false. - */ - isManaged() { - //{ - // _from: data._id, - // _to: repo_alloc._to, - // uid: owner_id - //}; - this.#loc = g_db.loc.firstExample({ - _from: this.#data_id, - }); - - if (!this.#loc) { - this.#error = g_lib.ERR_PERM_DENIED; - this.#err_msg = - "Permission denied data is not managed by DataFed. This can happen if you try to do a transfer directly from Globus."; - return false; + + /** + * @brief Compares two paths and if an error is detected will save the error code and message. + **/ + _comparePaths(storedPath, inputPath) { + if (storedPath !== inputPath) { + this.#error = g_lib.ERR_PERM_DENIED; + this.#err_msg = + "Record path is not consistent with repo expected path is: " + + storedPath + + " attempted path is " + + inputPath; + return false; + } + return true; } - this.#alloc = g_db.alloc.firstExample({ - _from: this.#loc.uid, - _to: this.#loc._to, - }); - - // If alloc is null then will return false if not null will return true. - return !!this.#alloc; - } - - /** - * @brief Validates if the provided record path is consistent with the database. - * @param {string} a_path - The path to validate. - * @return {boolean} True if consistent, otherwise false. - */ - isPathConsistent(a_path) { - // This function will populate the this.#loc member and the this.#alloc - // member - if (!this.isManaged()) { - return false; + + /** + * @brief Checks if the record exists in the database. + * @return {boolean} True if the record exists, otherwise false. + */ + exists() { + return this.#exists; } - // If path is missing the starting "/" add it back in - if (!a_path.startsWith("/") && this.#alloc.path.startsWith("/")) { - a_path = "/" + a_path; + key() { + return this.#key; } - // If there is a new repo we need to check the path there and use that - if (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 - // oweners id. - // 2. Using the loc.uid parameter if not inflight to get the owner - // id. - const new_alloc = g_db.alloc.firstExample({ - _from: this.#loc.new_owner ? this.#loc.new_owner : this.#loc.uid, - _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 (!new_alloc) { - this.#error = g_lib.ERR_PERM_DENIED; - this.#err_msg = - "Permission denied, '" + - this.#key + - "' is not part of an allocation '"; - return false; - } - - let stored_path = this._pathToRecord(new_alloc.path); - - if (!this._comparePaths(stored_path, a_path)) { return false; } - } else { - let stored_path = this._pathToRecord(this.#alloc.path); - - // If there is no new repo check that the paths align - if (!this._comparePaths(stored_path, a_path)) { return false; } + id() { + return this.#data_id; + } + /** + * @brief Will return error code of last run method. + * + * If no error code, will return null + **/ + error() { + return this.#error; + } + + /** + * @brief Retrieves the error code of the last run method. + * @return {string|null} Error code or null if no error. + */ + errorMessage() { + return this.#err_msg; + } + + /** + * @brief Checks if the record is managed by DataFed. + * @return {boolean} True if managed, otherwise false. + */ + isManaged() { + //{ + // _from: data._id, + // _to: repo_alloc._to, + // uid: owner_id + //}; + this.#loc = g_db.loc.firstExample({ + _from: this.#data_id, + }); + + if (!this.#loc) { + this.#error = g_lib.ERR_PERM_DENIED; + this.#err_msg = + "Permission denied data is not managed by DataFed. This can happen if you try to do a transfer directly from Globus."; + return false; + } + this.#alloc = g_db.alloc.firstExample({ + _from: this.#loc.uid, + _to: this.#loc._to, + }); + + // If alloc is null then will return false if not null will return true. + return !!this.#alloc; + } + + /** + * @brief Validates if the provided record path is consistent with the database. + * @param {string} a_path - The path to validate. + * @return {boolean} True if consistent, otherwise false. + */ + isPathConsistent(a_path) { + // This function will populate the this.#loc member and the this.#alloc + // member + if (!this.isManaged()) { + 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) { + // 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 + // oweners id. + // 2. Using the loc.uid parameter if not inflight to get the owner + // id. + const new_alloc = g_db.alloc.firstExample({ + _from: this.#loc.new_owner ? this.#loc.new_owner : this.#loc.uid, + _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 (!new_alloc) { + this.#error = g_lib.ERR_PERM_DENIED; + this.#err_msg = + "Permission denied, '" + this.#key + "' is not part of an allocation '"; + return false; + } + + let stored_path = this._pathToRecord(new_alloc.path); + + if (!this._comparePaths(stored_path, a_path)) { + return false; + } + } else { + let stored_path = this._pathToRecord(this.#alloc.path); + + // If there is no new repo check that the paths align + if (!this._comparePaths(stored_path, a_path)) { + return false; + } + } + return true; } - return true; - } } module.exports = Record; diff --git a/core/database/foxx/tests/path.test.js b/core/database/foxx/tests/path.test.js index c4a4a9f2c..34633a8b1 100644 --- a/core/database/foxx/tests/path.test.js +++ b/core/database/foxx/tests/path.test.js @@ -1,37 +1,37 @@ -"use strict" +"use strict"; -const chai = require('chai'); +const chai = require("chai"); const expect = chai.expect; -const pathModule = require('../api/posix_path'); // Replace with the actual file name +const pathModule = require("../api/posix_path"); // Replace with the actual file name -describe('splitPOSIXPath', function () { - it('unit_path: splitPOSIXPath should split a simple POSIX path into components', function () { - const result = pathModule.splitPOSIXPath('/usr/local/bin/node'); - expect(result).to.deep.equal(['usr', 'local', 'bin', 'node']); - }); +describe("splitPOSIXPath", function () { + it("unit_path: splitPOSIXPath should split a simple POSIX path into components", function () { + const result = pathModule.splitPOSIXPath("/usr/local/bin/node"); + expect(result).to.deep.equal(["usr", "local", "bin", "node"]); + }); - it('unit_path: splitPOSIXPath should handle root path and return an empty array', function () { - const result = pathModule.splitPOSIXPath('/'); - expect(result).to.deep.equal([]); - }); + it("unit_path: splitPOSIXPath should handle root path and return an empty array", function () { + const result = pathModule.splitPOSIXPath("/"); + expect(result).to.deep.equal([]); + }); - it('unit_path: splitPOSIXPath should handle paths with trailing slashes correctly', function () { - const result = pathModule.splitPOSIXPath('/usr/local/bin/'); - expect(result).to.deep.equal(['usr', 'local', 'bin']); - }); + it("unit_path: splitPOSIXPath should handle paths with trailing slashes correctly", function () { + const result = pathModule.splitPOSIXPath("/usr/local/bin/"); + expect(result).to.deep.equal(["usr", "local", "bin"]); + }); - it('unit_path: splitPOSIXPath should handle empty paths and throw an error', function () { - expect(() => pathModule.splitPOSIXPath('')).to.throw('Invalid POSIX path'); - }); + it("unit_path: splitPOSIXPath should handle empty paths and throw an error", function () { + expect(() => pathModule.splitPOSIXPath("")).to.throw("Invalid POSIX path"); + }); - it('unit_path: splitPOSIXPath should handle null or undefined paths and throw an error', function () { - expect(() => pathModule.splitPOSIXPath(null)).to.throw('Invalid POSIX path'); - expect(() => pathModule.splitPOSIXPath(undefined)).to.throw('Invalid POSIX path'); - }); + it("unit_path: splitPOSIXPath should handle null or undefined paths and throw an error", function () { + expect(() => pathModule.splitPOSIXPath(null)).to.throw("Invalid POSIX path"); + expect(() => pathModule.splitPOSIXPath(undefined)).to.throw("Invalid POSIX path"); + }); - it('unit_path: splitPOSIXPath should handle non-string inputs and throw an error', function () { - expect(() => pathModule.splitPOSIXPath(123)).to.throw('Invalid POSIX path'); - expect(() => pathModule.splitPOSIXPath({})).to.throw('Invalid POSIX path'); - expect(() => pathModule.splitPOSIXPath([])).to.throw('Invalid POSIX path'); - }); + it("unit_path: splitPOSIXPath should handle non-string inputs and throw an error", function () { + expect(() => pathModule.splitPOSIXPath(123)).to.throw("Invalid POSIX path"); + expect(() => pathModule.splitPOSIXPath({})).to.throw("Invalid POSIX path"); + expect(() => pathModule.splitPOSIXPath([])).to.throw("Invalid POSIX path"); + }); }); diff --git a/core/database/foxx/tests/record.test.js b/core/database/foxx/tests/record.test.js index 7a460348f..a272b259d 100644 --- a/core/database/foxx/tests/record.test.js +++ b/core/database/foxx/tests/record.test.js @@ -10,267 +10,261 @@ const arangodb = require("@arangodb"); function recordRepoAndUserSetup(record_key, user_id, repo_id) { const record_id = "d/" + record_key; g_db.d.save({ - _key: record_key, - _id: record_id, + _key: record_key, + _id: record_id, }); g_db.repo.save({ - _id: repo_id, + _id: repo_id, }); g_db.u.save({ - _id: user_id, + _id: user_id, }); } describe("Record Class", () => { - beforeEach(() => { - g_db.d.truncate(); - g_db.alloc.truncate(); - g_db.loc.truncate(); - g_db.repo.truncate(); - }); + beforeEach(() => { + g_db.d.truncate(); + g_db.alloc.truncate(); + g_db.loc.truncate(); + g_db.repo.truncate(); + }); - it("unit_record: should initialize correctly and check record existence is invalid", () => { - const record = new Record("invalidKey"); - expect(record.exists()).to.be.false; - expect(record.key()).to.equal("invalidKey"); - expect(record.error()).to.equal(g_lib.ERR_NOT_FOUND); - expect(record.errorMessage()).to.equal( - "Invalid key: (invalidKey). No record found.", - ); - }); + it("unit_record: should initialize correctly and check record existence is invalid", () => { + const record = new Record("invalidKey"); + expect(record.exists()).to.be.false; + expect(record.key()).to.equal("invalidKey"); + expect(record.error()).to.equal(g_lib.ERR_NOT_FOUND); + expect(record.errorMessage()).to.equal("Invalid key: (invalidKey). No record found."); + }); - it("unit_record: should initialize correctly and check record existence is valid", () => { - const valid_key = "1120"; - const key_id = "d/" + valid_key; - const owner_id = "u/bob"; - const repo_id = "repo/datafed-at-com"; - // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + it("unit_record: should initialize correctly and check record existence is valid", () => { + const valid_key = "1120"; + const key_id = "d/" + valid_key; + const owner_id = "u/bob"; + const repo_id = "repo/datafed-at-com"; + // Create nodes + recordRepoAndUserSetup(valid_key, owner_id, repo_id); - // Create edges - g_db.loc.save({ - _from: key_id, - _to: repo_id, - uid: owner_id, + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); + g_db.alloc.save({ + _from: owner_id, + _to: repo_id, + }); + const record = new Record(valid_key); + expect(record.exists()).to.be.true; + expect(record.key()).to.equal(valid_key); + expect(record.error()).to.be.null; + expect(record.errorMessage()).to.be.null; }); - g_db.alloc.save({ - _from: owner_id, - _to: repo_id, - }); - const record = new Record(valid_key); - expect(record.exists()).to.be.true; - expect(record.key()).to.equal(valid_key); - expect(record.error()).to.be.null; - expect(record.errorMessage()).to.be.null; - }); - it("unit_record: isManaged should initialize correctly, but show a record as not managed.", () => { - const valid_key = "1121"; - const key_id = "d/" + valid_key; - const owner_id = "u/jim"; - const repo_id = "repo/datafed-at-org"; - // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + it("unit_record: isManaged should initialize correctly, but show a record as not managed.", () => { + const valid_key = "1121"; + const key_id = "d/" + valid_key; + const owner_id = "u/jim"; + const repo_id = "repo/datafed-at-org"; + // Create nodes + recordRepoAndUserSetup(valid_key, owner_id, repo_id); + + const record = new Record(valid_key); + expect(record.isManaged()).to.be.false; + expect(record.exists()).to.be.true; + expect(record.key()).to.equal(valid_key); + expect(record.error()).to.equal(g_lib.ERR_PERM_DENIED); + const pattern = /^Permission denied data is not managed by DataFed/; + expect(record.errorMessage()).to.match(pattern); + }); - const record = new Record(valid_key); - expect(record.isManaged()).to.be.false; - expect(record.exists()).to.be.true; - expect(record.key()).to.equal(valid_key); - expect(record.error()).to.equal(g_lib.ERR_PERM_DENIED); - const pattern = /^Permission denied data is not managed by DataFed/; - expect(record.errorMessage()).to.match(pattern); - }); + it("unit_record: isManaged should initialize correctly, but without an allocation so should return false", () => { + const valid_key = "1122"; + const key_id = "d/" + valid_key; + const owner_id = "u/tom"; + const repo_id = "repo/datafed-banana-com"; + // Create nodes + recordRepoAndUserSetup(valid_key, owner_id, repo_id); - it("unit_record: isManaged should initialize correctly, but without an allocation so should return false", () => { - const valid_key = "1122"; - const key_id = "d/" + valid_key; - const owner_id = "u/tom"; - const repo_id = "repo/datafed-banana-com"; - // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); - // Create edges - g_db.loc.save({ - _from: key_id, - _to: repo_id, - uid: owner_id, + const record = new Record(valid_key); + expect(record.isManaged()).to.be.false; + expect(record.exists()).to.be.true; + expect(record.key()).to.equal(valid_key); + expect(record.error()).to.be.null; }); - const record = new Record(valid_key); - expect(record.isManaged()).to.be.false; - expect(record.exists()).to.be.true; - expect(record.key()).to.equal(valid_key); - expect(record.error()).to.be.null; - }); + it("unit_record: isManaged should initialize correctly, and with allocation show that record is managed.", () => { + const valid_key = "1123"; + const key_id = "d/" + valid_key; + const owner_id = "u/drake"; + const repo_id = "repo/datafed-best-com"; + // Create nodes + recordRepoAndUserSetup(valid_key, owner_id, repo_id); - it("unit_record: isManaged should initialize correctly, and with allocation show that record is managed.", () => { - const valid_key = "1123"; - const key_id = "d/" + valid_key; - const owner_id = "u/drake"; - const repo_id = "repo/datafed-best-com"; - // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); + g_db.alloc.save({ + _from: owner_id, + _to: repo_id, + }); - // Create edges - g_db.loc.save({ - _from: key_id, - _to: repo_id, - uid: owner_id, - }); - g_db.alloc.save({ - _from: owner_id, - _to: repo_id, + const record = new Record(valid_key); + expect(record.isManaged()).to.be.true; + expect(record.exists()).to.be.true; + expect(record.key()).to.equal(valid_key); + expect(record.error()).to.be.null; }); - const record = new Record(valid_key); - expect(record.isManaged()).to.be.true; - expect(record.exists()).to.be.true; - expect(record.key()).to.equal(valid_key); - expect(record.error()).to.be.null; - }); + it("unit_record: isPathConsistent should return false because it is not managed.", () => { + const valid_key = "1124"; + const key_id = "d/" + valid_key; + const owner_id = "u/carl"; + const repo_id = "repo/datafed-at-super"; + // Create nodes + recordRepoAndUserSetup(valid_key, owner_id, repo_id); - it("unit_record: isPathConsistent should return false because it is not managed.", () => { - const valid_key = "1124"; - const key_id = "d/" + valid_key; - const owner_id = "u/carl"; - const repo_id = "repo/datafed-at-super"; - // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); - // Create edges - g_db.loc.save({ - _from: key_id, - _to: repo_id, - uid: owner_id, + const record = new Record(valid_key); + expect(record.isPathConsistent("file/path/" + valid_key)).to.be.false; }); - const record = new Record(valid_key); - expect(record.isPathConsistent("file/path/" + valid_key)).to.be.false; - }); + it("unit_record: isPathConsistent should return false paths are not consistent.", () => { + const valid_key = "1125"; + const key_id = "d/" + valid_key; + const owner_id = "u/red"; + const repo_id = "repo/datafed-fine-com"; + // Create nodes + recordRepoAndUserSetup(valid_key, owner_id, repo_id); - it("unit_record: isPathConsistent should return false paths are not consistent.", () => { - const valid_key = "1125"; - const key_id = "d/" + valid_key; - const owner_id = "u/red"; - const repo_id = "repo/datafed-fine-com"; - // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); - - // Create edges - g_db.loc.save({ - _from: key_id, - _to: repo_id, - uid: owner_id, - }); - g_db.alloc.save({ - _from: owner_id, - _to: repo_id, - path: "/correct/file/path", + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); + g_db.alloc.save({ + _from: owner_id, + _to: repo_id, + path: "/correct/file/path", + }); + + const record = new Record(valid_key); + expect(record.isPathConsistent("/incorrect/file/path/" + 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); }); - const record = new Record(valid_key); - expect(record.isPathConsistent("/incorrect/file/path/" + 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); - }); + 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 repo_id = "repo/datafed-cool-com"; + // Create nodes + recordRepoAndUserSetup(valid_key, owner_id, repo_id); - 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 repo_id = "repo/datafed-cool-com"; - // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); - - // Create edges - g_db.loc.save({ - _from: key_id, - _to: repo_id, - uid: owner_id, - }); - g_db.alloc.save({ - _from: owner_id, - _to: repo_id, - path: "/correct/file/path", + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + }); + g_db.alloc.save({ + _from: owner_id, + _to: 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.error()).to.be.null; + expect(record.errorMessage()).to.be.null; }); - const record = new Record(valid_key); - expect(record.isPathConsistent("/correct/file/path/" + 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.", () => { + const valid_key = "1127"; + const key_id = "d/" + valid_key; + const owner_id = "u/john"; + const repo_id = "repo/orange-at-com"; + const new_repo_id = "repo/watermelon-at-org"; - it("unit_record: isPathConsistent should return true paths are inconsistent, but new path in new alloc is valid.", () => { - const valid_key = "1127"; - const key_id = "d/" + valid_key; - const owner_id = "u/john"; - const repo_id = "repo/orange-at-com"; - const new_repo_id = "repo/watermelon-at-org"; + // Create nodes + recordRepoAndUserSetup(valid_key, owner_id, repo_id); - // Create nodes - recordRepoAndUserSetup(valid_key, owner_id, repo_id); + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + new_repo: new_repo_id, + }); + 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", + }); - // Create edges - g_db.loc.save({ - _from: key_id, - _to: repo_id, - uid: owner_id, - new_repo: new_repo_id, - }); - 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.error()).to.be.null; + expect(record.errorMessage()).to.be.null; }); - const record = new Record(valid_key); - expect(record.isPathConsistent("/correct/file/path/" + 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.", () => { + 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); - it("unit_record: isPathConsistent should return false paths are inconsistent in new and old alloc.", () => { - 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); + // Create edges + g_db.loc.save({ + _from: key_id, + _to: repo_id, + uid: owner_id, + new_repo: new_repo_id, + }); + 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", + }); - // Create edges - g_db.loc.save({ - _from: key_id, - _to: repo_id, - uid: owner_id, - new_repo: new_repo_id, + const record = new Record(valid_key); + expect(record.isPathConsistent("/correct/file/path/" + 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); }); - 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.error()).to.equal(g_lib.ERR_PERM_DENIED); - const pattern = /^Record path is not consistent/; - expect(record.errorMessage()).to.match(pattern); - }); });