Skip to content

Commit

Permalink
Merge pull request #1224 from ORNL/1180-refactor-authz
Browse files Browse the repository at this point in the history
[DAPS, Foxx] - 1180 refactor Part 2 authz
  • Loading branch information
JoshuaSBrown authored Jan 17, 2025
2 parents 049a4f4 + c7cdae3 commit e7972b8
Show file tree
Hide file tree
Showing 12 changed files with 1,415 additions and 177 deletions.
6 changes: 5 additions & 1 deletion core/database/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,23 @@ if( ENABLE_FOXX_TESTS )
add_test(NAME foxx_teardown COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_teardown.sh")
add_test(NAME foxx_authz COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_authz")
add_test(NAME foxx_record COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_record")
add_test(NAME foxx_repo COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_repo")
add_test(NAME foxx_path COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_path")
add_test(NAME foxx_db_fixtures COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_fixture_setup.sh")
add_test(NAME foxx_version COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_version")
add_test(NAME foxx_support COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_support")
add_test(NAME foxx_user_router COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "user_router")
add_test(NAME foxx_user_router COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_user_router")
add_test(NAME foxx_authz_router COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_authz_router")

set_tests_properties(foxx_setup PROPERTIES FIXTURES_SETUP Foxx)
set_tests_properties(foxx_teardown PROPERTIES FIXTURES_CLEANUP Foxx)
set_tests_properties(foxx_db_fixtures PROPERTIES FIXTURES_SETUP FoxxDBFixtures FIXTURES_REQUIRED 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_authz_router PROPERTIES FIXTURES_REQUIRED Foxx)
set_tests_properties(foxx_record PROPERTIES FIXTURES_REQUIRED Foxx)
set_tests_properties(foxx_repo PROPERTIES FIXTURES_REQUIRED Foxx)
set_tests_properties(foxx_path PROPERTIES FIXTURES_REQUIRED Foxx)
set_tests_properties(foxx_user_router PROPERTIES FIXTURES_REQUIRED "Foxx;FoxxDBFixtures")
endif()
143 changes: 143 additions & 0 deletions core/database/foxx/api/authz.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

const g_db = require("@arangodb").db;
const path = require("path");
const Record = require("./record");
const pathModule = require("./posix_path");
const g_lib = require("./support");
const { Repo, PathType } = require("./repo");

module.exports = (function () {
let obj = {};
Expand Down Expand Up @@ -44,5 +47,145 @@ module.exports = (function () {
return false;
};

obj.readRecord = function (client, path) {
const permission = g_lib.PERM_RD_DATA;
const path_components = pathModule.splitPOSIXPath(path);
const data_key = path_components.at(-1);
let record = new Record(data_key);
if (!record.exists()) {
// Return not found error for non-existent records
console.log("AUTHZ act: read client: " + client._id + " path " + path + " NOT_FOUND");
throw [g_lib.ERR_NOT_FOUND, "Record not found: " + path];
}

// Special case - allow unknown client to read a publicly accessible record
// if record exists and if it is a public record
if (!client) {
if (!g_lib.hasPublicRead(record.id())) {
console.log("AUTHZ act: read" + " unknown client " + " path " + path + " FAILED");
throw [
g_lib.ERR_PERM_DENIED,
"Unknown client does not have read permissions on " + path,
];
}
} else if (!obj.isRecordActionAuthorized(client, data_key, permission)) {
console.log("AUTHZ act: read" + " client: " + client._id + " path " + path + " FAILED");
throw [
g_lib.ERR_PERM_DENIED,
"Client " + client._id + " does not have read permissions on " + path,
];
}

if (!record.isPathConsistent(path)) {
console.log("AUTHZ act: read client: " + client._id + " path " + path + " FAILED");
throw [record.error(), record.errorMessage()];
}
};

obj.none = function (client, path) {
const permission = g_lib.PERM_NONE;
};

obj.denied = function (client, path) {
throw g_lib.ERR_PERM_DENIED;
};

obj.createRecord = function (client, path) {
const permission = g_lib.PERM_WR_DATA;
const path_components = pathModule.splitPOSIXPath(path);
const data_key = path_components.at(-1);

if (!client) {
console.log(
"AUTHZ act: create" + " client: " + client._id + " path " + path + " FAILED",
);
throw [
g_lib.ERR_PERM_DENIED,
"Unknown client does not have create permissions on " + path,
];
} else if (!obj.isRecordActionAuthorized(client, data_key, permission)) {
console.log(
"AUTHZ act: create" + " client: " + client._id + " path " + path + " FAILED",
);
throw [
g_lib.ERR_PERM_DENIED,
"Client " + client._id + " does not have create permissions on " + path,
];
}

let record = new Record(data_key);
// This does not mean the record exsts in the repo it checks if an entry
// exists in the database.
if (!record.exists()) {
// If the record does not exist then the path would not be consistent.
console.log("AUTHZ act: create client: " + client._id + " path " + path + " FAILED");
throw [g_lib.ERR_PERM_DENIED, "Invalid record specified: " + path];
}

// This will tell us if the proposed path is consistent with what we expect
// GridFTP will fail if the posix file path does not exist.
if (!record.isPathConsistent(path)) {
console.log("AUTHZ act: create client: " + client._id + " path " + path + " FAILED");
throw [record.error(), record.errorMessage()];
}
};

obj.authz_strategy = {
read: {
[PathType.USER_PATH]: obj.none,
[PathType.USER_RECORD_PATH]: obj.readRecord,
[PathType.PROJECT_PATH]: obj.none,
[PathType.PROJECT_RECORD_PATH]: obj.readRecord,
[PathType.REPO_BASE_PATH]: obj.none,
[PathType.REPO_ROOT_PATH]: obj.none,
[PathType.REPO_PATH]: obj.none,
},
write: {
[PathType.USER_PATH]: obj.none,
[PathType.USER_RECORD_PATH]: obj.none,
[PathType.PROJECT_PATH]: obj.none,
[PathType.PROJECT_RECORD_PATH]: obj.none,
[PathType.REPO_BASE_PATH]: obj.none,
[PathType.REPO_ROOT_PATH]: obj.none,
[PathType.REPO_PATH]: obj.none,
},
create: {
[PathType.USER_PATH]: obj.none,
[PathType.USER_RECORD_PATH]: obj.createRecord,
[PathType.PROJECT_PATH]: obj.none,
[PathType.PROJECT_RECORD_PATH]: obj.createRecord,
[PathType.REPO_BASE_PATH]: obj.none,
[PathType.REPO_ROOT_PATH]: obj.none,
[PathType.REPO_PATH]: obj.none,
},
delete: {
[PathType.USER_PATH]: obj.denied,
[PathType.USER_RECORD_PATH]: obj.denied,
[PathType.PROJECT_PATH]: obj.denied,
[PathType.PROJECT_RECORD_PATH]: obj.denied,
[PathType.REPO_BASE_PATH]: obj.denied,
[PathType.REPO_ROOT_PATH]: obj.denied,
[PathType.REPO_PATH]: obj.denied,
},
chdir: {
[PathType.USER_PATH]: obj.none,
[PathType.USER_RECORD_PATH]: obj.none,
[PathType.PROJECT_PATH]: obj.none,
[PathType.PROJECT_RECORD_PATH]: obj.none,
[PathType.REPO_BASE_PATH]: obj.none,
[PathType.REPO_ROOT_PATH]: obj.none,
[PathType.REPO_PATH]: obj.none,
},
lookup: {
[PathType.USER_PATH]: obj.none,
[PathType.USER_RECORD_PATH]: obj.none,
[PathType.PROJECT_PATH]: obj.none,
[PathType.PROJECT_RECORD_PATH]: obj.none,
[PathType.REPO_BASE_PATH]: obj.none,
[PathType.REPO_ROOT_PATH]: obj.none,
[PathType.REPO_PATH]: obj.none,
},
};

return obj;
})();
147 changes: 44 additions & 103 deletions core/database/foxx/api/authz_router.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ const router = createRouter();
const joi = require("joi");
const g_db = require("@arangodb").db;
const g_lib = require("./support");
const pathModule = require("./posix_path"); // Replace with the actual file name
const Record = require("./record"); // Replace with the actual file name
const authzModule = require("./authz"); // Replace with the actual file name
const authzModule = require("./authz");
const { Repo, PathType } = require("./repo");

module.exports = router;

Expand All @@ -26,109 +25,36 @@ router
);

// Client will contain the following information
// {
// "_key" : "bob",
// "_id" : "u/bob",
// "name" : "bob junior ",
// "name_first" : "bob",
// "name_last" : "jones",
// "is_admin" : true,
// "max_coll" : 50,
// "max_proj" : 10,
// "max_sav_qry" : 20,
// :
// "email" : "[email protected]"
// }
const client = g_lib.getUserFromClientID_noexcept(req.queryParams.client);

// Will split a posix path into an array
// E.g.
// req.queryParams.file = "/usr/local/bin"
// const path_components = pathModule.splitPOSIXPath(req.queryParams.file);
//
// Path components will be
// ["usr", "local", "bin"]
const path_components = pathModule.splitPOSIXPath(req.queryParams.file);
const data_key = path_components.at(-1);
let record = new Record(data_key);

// Special case - allow unknown client to read a publicly accessible record
// if record exists and if it is a public record
// "_key" : "bob",
// "_id" : "u/bob",
// "name" : "bob junior ",
// "name_first" : "bob",
// "name_last" : "jones",
// "is_admin" : true,
// "max_coll" : 50,
// "max_proj" : 10,
// "max_sav_qry" : 20,
// :
// "email" : "[email protected]"
const client = g_lib.getUserFromClientID_noexcept(req.queryParams.client);
if (!client) {
if (record.exists()) {
if (req.queryParams.act != "read" || !g_lib.hasPublicRead(record.id())) {
console.log(
"AUTHZ act: " +
req.queryParams.act +
" client: " +
client._id +
" path " +
req.queryParams.file +
" FAILED",
);
throw g_lib.ERR_PERM_DENIED;
}
}
} else {
// Actions: read, write, create, delete, chdir, lookup
let req_perm = 0;
switch (req.queryParams.act) {
case "read":
req_perm = g_lib.PERM_RD_DATA;
break;
case "write":
break;
case "create":
req_perm = g_lib.PERM_WR_DATA;
break;
case "delete":
throw g_lib.ERR_PERM_DENIED;
case "chdir":
break;
case "lookup":
// For TESTING, allow these actions
return;
default:
throw [
g_lib.ERR_INVALID_PARAM,
"Invalid gridFTP action: ",
req.queryParams.act,
];
}

// This will tell us if the action on the record is authorized
// we still do not know if the path is correct.
if (record.exists()) {
if (!authzModule.isRecordActionAuthorized(client, data_key, req_perm)) {
console.log(
"AUTHZ act: " +
req.queryParams.act +
" client: " +
client._id +
" path " +
req.queryParams.file +
" FAILED",
);
throw g_lib.ERR_PERM_DENIED;
}
}
console.log(
"AUTHZ act: " +
req.queryParams.act +
" client: " +
+req.queryParams.client +
" path " +
req.queryParams.file +
" FAILED",
);
throw [g_lib.ERR_PERM_DENIED, "Unknown client: " + req.queryParams.client];
}
let repo = new Repo(req.queryParams.repo);
let path_type = repo.pathType(req.queryParams.file);

if (record.exists()) {
if (!record.isPathConsistent(req.queryParams.file)) {
console.log(
"AUTHZ act: " +
req.queryParams.act +
" client: " +
client._id +
" path " +
req.queryParams.file +
" FAILED",
);
throw [record.error(), record.errorMessage()];
}
} else {
// If the record does not exist then the path would noe be consistent.
// If the provided path is not within the repo throw an error
if (path_type === PathType.UNKNOWN) {
console.log(
"AUTHZ act: " +
req.queryParams.act +
Expand All @@ -138,7 +64,22 @@ router
req.queryParams.file +
" FAILED",
);
throw [g_lib.ERR_PERM_DENIED, "Invalid record specified: " + req.queryParams.file];
throw [
g_lib.ERR_PERM_DENIED,
"Unknown path, or path is not consistent with supported repository folder hierarchy: " +
req.queryParams.file,
];
}

// Determine permissions associated with path provided
// Actions: read, write, create, delete, chdir, lookup
if (Object.keys(authzModule.authz_strategy).includes(req.queryParams.act)) {
authzModule.authz_strategy[req.queryParams.act][path_type](
client,
req.queryParams.file,
);
} else {
throw [g_lib.ERR_INVALID_PARAM, "Invalid gridFTP action: ", req.queryParams.act];
}
console.log(
"AUTHZ act: " +
Expand Down
9 changes: 8 additions & 1 deletion core/database/foxx/api/posix_path.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,15 @@ module.exports = (function () {
* ['usr', 'local', 'bin', 'node']
*/
obj.splitPOSIXPath = function (a_posix_path) {
if (!a_posix_path || typeof a_posix_path !== "string") {
if (!a_posix_path) {
throw new Error("Invalid POSIX path");
} else if (typeof a_posix_path !== "string") {
throw new Error(
"Invalid POSIX path type: ",
typeof a_posix_path,
" path content: ",
a_posix_path,
);
}
// Split the path into components
// components: ['', 'usr', 'local', 'bin', 'node']
Expand Down
Loading

0 comments on commit e7972b8

Please sign in to comment.