-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
1199 feature add support for scoped lookup #1225
base: 1180-refactor-authz
Are you sure you want to change the base?
1199 feature add support for scoped lookup #1225
Conversation
Reviewer's Guide by SourceryThis pull request replaces the existing authorization module with a new scoped lookup implementation for stricter authorization enforcement. It introduces a new Sequence diagram for authorization lookup flowsequenceDiagram
participant Client
participant AuthzRouter
participant Repo
participant Project
Client->>AuthzRouter: Request access to path
AuthzRouter->>Repo: hasAccess(client_id)
alt Direct allocation
Repo-->>AuthzRouter: true
else Project allocation
Repo->>Project: hasAccess(client_id)
Project-->>Repo: true/false
Repo-->>AuthzRouter: true/false
end
AuthzRouter-->>Client: Authorization result
Class diagram showing the new authorization structureclassDiagram
class Repo {
-repo_id: string
+isAdmin(client_id: string): boolean
+hasAccess(client_id: string): boolean
+getProjectIds(): string[]
}
class Project {
-project_id: string
-project: object
+exists(): boolean
+hasAccess(client_id: string): boolean
+isMember(client_id: string): boolean
+isAdmin(client_id: string): boolean
+getRepositoryIds(): Set
+hasAllocationOnRepo(repo_id: string): boolean
}
class User {
-user_id: string
-user: object
+exists(): boolean
+getGroupIds(): string[]
+getProjectIds(): string[]
+getRepos(): Set
}
Repo -- Project : allocations
Project -- User : membership
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @JoshuaSBrown - I've reviewed your changes and they look great!
Here's what I looked at during the review
- 🟡 General issues: 3 issues found
- 🟢 Security: all looks good
- 🟡 Testing: 3 issues found
- 🟢 Complexity: all looks good
- 🟢 Documentation: all looks good
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
}) | ||
.toArray(); | ||
|
||
const repo_ids = [...new Set(allocs.map((obj) => obj._to))]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): The 'allocs' variable is undefined in getRepos method - should be 'user_allocs'.
Also 'group_ids' is used in the query but getProjectIds() was called instead of getGroupIds().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sourcery has a good catch! The resulting object should also be a set, not an array!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're absolutely right, thanks for catching that! I've missed the incorrect variable name and the fact that the result should be a set. My apologies for the oversight. I've updated my suggested fix to reflect both of these points. Please double-check it to make sure it aligns with your intentions.
return false; | ||
} | ||
const repo_ids = this.getRepositoryIds(); | ||
return repo_ids.has(a_repo_id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): getRepositoryIds() returns an array but Set.has() is called on it.
This will always return false since arrays don't have a has() method. Consider converting repo_ids to a Set or use includes() instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sourcery left a good callout
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're welcome! I'm glad I could help. Since you agree with the assessment, I suggest changing the getRepositoryIds
function to return a Set directly. This will be more efficient than creating an array and then converting it to a Set in hasAllocationOnRepo
. Something like this:
getRepositoryIds() {
const allocs = this.getAllocations();
return new Set(allocs.map((obj) => obj._to));
}
This way, the hasAllocationOnRepo
function will work correctly with the has()
method. Let me know if you have any other questions.
const repo = new Repo(repo_id); | ||
expect(repo.isAdmin(user_id)).to.be.true; | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): Missing test case for system admin with no repo admin access.
Add a test case where a system admin doesn't have specific admin access to the repo, and verify isAdmin returns false.
const repo = new Repo(repo_id); | |
expect(repo.isAdmin(user_id)).to.be.true; | |
}); | |
const repo = new Repo(repo_id); | |
expect(repo.isAdmin(user_id)).to.be.true; | |
}); | |
it("should return false for system admin without repo admin access", () => { | |
const repo_id = "r/test"; | |
g_db.r.save({ | |
_id: repo_id, | |
_key: "test", | |
admins: [] // Empty admins list | |
}); | |
const user_id = "u/systemadmin"; | |
g_db.u.save({ | |
_id: user_id, | |
_key: "systemadmin", | |
is_admin: true // System admin but not repo admin | |
}); | |
const repo = new Repo(repo_id); | |
expect(repo.isAdmin(user_id)).to.be.false; | |
}); | |
// Direct allocation from user to repo | ||
g_db.alloc.save({ _from: user_id, _to: repo_id }); | ||
|
||
// Test repo | ||
const repo = new Repo(repo_id); | ||
expect(repo.exists()).to.be.true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): Edge case: User has direct allocation AND is a project member.
Add a test where the user has both a direct allocation and is a member of a project allocated to the repo. Verify that hasAccess still returns true.
// Direct allocation from user to repo | |
g_db.alloc.save({ _from: user_id, _to: repo_id }); | |
// Test repo | |
const repo = new Repo(repo_id); | |
expect(repo.exists()).to.be.true; | |
// Direct allocation from user to repo | |
g_db.alloc.save({ _from: user_id, _to: repo_id }); | |
// Create a project and add user as member | |
const project_key = "test_project"; | |
const project_id = "p/" + project_key; | |
g_db.p.save({ _id: project_id, _key: project_key }, { waitForSync: true }); | |
g_db.member.save({ _from: project_id, _to: user_id }); | |
// Allocate project to repo | |
g_db.alloc.save({ _from: project_id, _to: repo_id }); | |
// Test repo | |
const repo = new Repo(repo_id); | |
expect(repo.exists()).to.be.true; |
expect(repo.hasAccess(user_id)).to.be.false; // should return false, no allocation exists | ||
}); | ||
|
||
it("unit_repo: testing hasAccess for a user with allocation to a different repo", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): Edge case: User has allocation to a different repo AND is a system admin.
Add a test where the user has an allocation to a different repo but is also a system admin. Verify the expected behavior of hasAccess in this scenario.
Suggested implementation:
it("unit_repo: testing hasAccess for a user with allocation to a different repo", () => {
// Create two repositories
const repo_key_1 = "other_repo";
const repo_id_1 = "repo/" + repo_key_1;
const repo_key_2 = "different_repo";
const repo_id_2 = "repo/" + repo_key_2;
g_db.repo.save(
{ _id: repo_id_1, _key: repo_key_1, path: "/mnt/other" },
{ waitForSync: true },
);
g_db.repo.save(
{ _id: repo_id_2, _key: repo_key_2, path: "/mnt/different" },
{ waitForSync: true },
);
// Create user with allocation to repo_1 only
const user_key = "alice";
const user_id = "u/" + user_key;
g_db.u.save({ _id: user_id, _key: user_key }, { waitForSync: true });
// Create allocation for repo_1
g_db.allocation.save(
{ _id: "allocation/test", _key: "test", repo: repo_id_1, u: user_id },
{ waitForSync: true },
);
// Test access
const repo2 = new Repo(repo_id_2);
expect(repo2.hasAccess(user_id)).to.be.false;
});
it("unit_repo: testing hasAccess for a system admin with allocation to a different repo", () => {
// Create two repositories
const repo_key_1 = "other_repo";
g_db.repo.save(
{ _id: repo_id_2, _key: repo_key_2, path: "/mnt/different" },
{ waitForSync: true },
);
// Create system admin user with allocation to repo_1 only
const user_key = "admin_bob";
const user_id = "u/" + user_key;
g_db.u.save({ _id: user_id, _key: user_key, system_admin: true }, { waitForSync: true });
// Create allocation for repo_1
g_db.allocation.save(
{ _id: "allocation/admin_test", _key: "admin_test", repo: repo_id_1, u: user_id },
{ waitForSync: true },
);
// Test access - should be true because user is system admin, regardless of allocation
const repo2 = new Repo(repo_id_2);
expect(repo2.hasAccess(user_id)).to.be.true;
const allocs = g_db.alloc | ||
.byExample({ | ||
_from: this.#project_id, | ||
}) | ||
.toArray(); | ||
return allocs; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (code-quality): Inline variable that is immediately returned (inline-immediately-returned-variable
)
const allocs = g_db.alloc | |
.byExample({ | |
_from: this.#project_id, | |
}) | |
.toArray(); | |
return allocs; | |
return g_db.alloc | |
.byExample({ | |
_from: this.#project_id, | |
}) | |
.toArray(); | |
Explanation
Something that we often see in people's code is assigning to a result variableand then immediately returning it.
Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.
Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.
"use strict"; | ||
|
||
const chai = require("chai"); | ||
const expect = chai.expect; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (code-quality): Prefer object destructuring when accessing and using properties. (use-object-destructuring
)
const expect = chai.expect; | |
const {expect} = chai; |
Explanation
Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.From the Airbnb Javascript Style Guide
7b2f123
to
43ccca1
Compare
const g_lib = require("../api/support"); | ||
const arangodb = require("@arangodb"); | ||
|
||
describe("Project Class", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
describe("Project Class", () => { | |
describe("Project", () => { |
@@ -2,7 +2,7 @@ | |||
|
|||
const chai = require("chai"); | |||
const expect = chai.expect; | |||
const pathModule = require("../api/posix_path"); // Replace with the actual file name | |||
const pathModule = require("../api/utils/posix_path"); // Replace with the actual file name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const pathModule = require("../api/utils/posix_path"); // Replace with the actual file name | |
const pathModule = require("../api/utils/posix_path"); |
@@ -1,7 +1,7 @@ | |||
"use strict"; | |||
const chai = require("chai"); | |||
const expect = chai.expect; | |||
const authzModule = require("../api/authz"); | |||
const authzModule = require("../api/controllers/authz"); // Replace with the actual file name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const authzModule = require("../api/controllers/authz"); // Replace with the actual file name | |
const authzModule = require("../api/controllers/authz"); |
const g_db = require("@arangodb").db; | ||
const g_lib = require("../support"); | ||
|
||
class User { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a user model
https://docs.arangodb.com/3.12/concepts/data-models/
My question is what's handling the user route interactions?
const g_db = require("@arangodb").db; | ||
const g_lib = require("../support"); | ||
|
||
class Project { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my questions about controllers/user.js
const { Repo, PathType } = require("./repo"); | ||
const { Project } = require("./project"); | ||
|
||
module.exports = (function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like some kind of module
or service
layer logic. Is this directly handling some API routes request? Also how would this be a controller?
" SUCCESS", | ||
); | ||
} else { | ||
console.log( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be console.error?
console.log( | |
console.error( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good correction!
@@ -61,7 +61,7 @@ class Repo { | |||
const collection = g_db._collection("repo"); | |||
|
|||
// This function is designed to check if the provided key exists in the | |||
// database as a record. Searches are only made in the 'd' collection | |||
// database. Searches are only made in the 'repo' collection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes me think this is a model
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a few questions and comments.
**/ | ||
obj.readRecord = function (client, path, a_repo) { | ||
const permission = g_lib.PERM_RD_DATA; | ||
// Will split a posix path into an array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment on splitPOSIXPath
seems redundant since we have a JSDoc on it.
obj.createRecord = function (client, path, a_repo) { | ||
const permission = g_lib.PERM_WR_DATA; | ||
|
||
// Will split a posix path into an array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also redundant
* /mnt/large/data/project - REPO_PATH | ||
* /mnt/large/data/user - REPO_PATH | ||
*/ | ||
obj.lookupRepo = function (client, path, a_repo) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this differ from lookupRepoRoot
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There isn't a difference in this case. I'll just consolidate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more comments on this
return false; | ||
} | ||
const repo_ids = this.getRepositoryIds(); | ||
return repo_ids.has(a_repo_id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sourcery left a good callout
**/ | ||
getRepositoryIds() { | ||
const allocs = this.getAllocations(); | ||
return [...new Set(allocs.map((obj) => obj._to))]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe this is as simple as return new Set([...allocs.map((obj) => obj._to)]);
} | ||
|
||
/** | ||
* Get all the repos the user has access too. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spelling too
-> to
}) | ||
.toArray(); | ||
|
||
const repo_ids = [...new Set(allocs.map((obj) => obj._to))]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sourcery has a good catch! The resulting object should also be a set, not an array!
// The maximum number of edges connecting a user to a repo is 3 | ||
// user <- g -> p -> repo | ||
// We only need to verify one path exists | ||
let qry = "FOR v, e, p IN 1..3 ANY @user_id admin, member, owner, alloc"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are admin, member, owner, alloc
the edges being searched here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved contingent upon some small bugs being addressed.
[DAPS, Foxx] - 1180 refactor Part 2 authz
PR Description
Replaces authz, in foxx with scoped lookup enforcing stricter authorization.
Tasks
Summary by Sourcery
Implement scoped lookup for stricter authorization in Foxx, replacing the previous authz implementation.
New Features:
Tests: