refactor: casl factory - jobs#2805
Open
HayenNico wants to merge 10 commits into
Open
Conversation
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
JobAbility.buildAbility, the unauthenticated rules are mixed in with the authenticated path (the per-job config loop runs before the!usercheck); consider returning early for!userbefore the authenticated logic to make the control flow clearer and reduce the chance of future regressions when extending the method. - In
JobsControllerUtils, theisAlwaysFalseQuery/isEmptyObjecthelpers and the generic branching inreadAccessFiltermake sense for multiple query variants but are currently only used with a singleaccessibleBy(..., Action.JobRead)call; consider simplifying or extracting the helpers to a shared utility so the intent of the filtering logic stays obvious as the job abilities evolve.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `JobAbility.buildAbility`, the unauthenticated rules are mixed in with the authenticated path (the per-job config loop runs before the `!user` check); consider returning early for `!user` before the authenticated logic to make the control flow clearer and reduce the chance of future regressions when extending the method.
- In `JobsControllerUtils`, the `isAlwaysFalseQuery`/`isEmptyObject` helpers and the generic branching in `readAccessFilter` make sense for multiple query variants but are currently only used with a single `accessibleBy(..., Action.JobRead)` call; consider simplifying or extracting the helpers to a shared utility so the intent of the filtering logic stays obvious as the job abilities evolve.
## Individual Comments
### Comment 1
<location path="src/casl/abilities/jobs.ability.ts" line_range="74-81" />
<code_context>
+ const ifOwnerUser = { ownerUser: user.username };
+ const ifOwnerGroup = { ownerGroup: { $in: user.currentGroups } };
+
+ const createAuthorizationValues = [
+ ...Object.values(CreateJobAuth).filter(
+ (v) => String(v) !== "#jobAdmin",
+ ),
+ ...user.currentGroups.map((g) => "@" + g),
+ user.username,
+ ];
+ const updateAuthorizationValues = [
+ ...Object.values(UpdateJobAuth).filter(
+ (v) => String(v) !== "#jobAdmin",
</code_context>
<issue_to_address>
**🚨 issue (security):** Create/Update authorization lists now include dataset/job-scoped auth values, which may broaden permissions compared to the previous logic.
The prior `jobsInstanceAccessCan` logic distinguished dataset/job-scoped auth values (e.g. `#dataset*`, `#job*`) from generic instance-level permissions. Here, `createAuthorizationValues` / `updateAuthorizationValues` include all `CreateJobAuth` / `UpdateJobAuth` enums (minus `#jobAdmin`), so any matching value in `jobConfig.create.auth` / `.update.auth` now grants `JobCreate` / `JobUpdate`. This broadens permissions vs. the old behavior. If that’s not intended, you’ll need to reintroduce a filter that excludes dataset/job-scoped entries or otherwise preserve that distinction.
</issue_to_address>
### Comment 2
<location path="src/jobs/jobs.controller.utils.ts" line_range="276" />
<code_context>
- });
- }
-
- isAlwaysFalseQuery(q: Record<string, unknown>): boolean {
- const expr = q.$expr as { $eq?: unknown } | undefined;
- const eq = expr?.$eq;
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the helper functions and `readAccessFilter` to use clearer typing and a single-query flow while preserving behavior.
You can reduce complexity in this change without losing any functionality by simplifying the helpers and `readAccessFilter`.
### 1. Simplify `isEmptyObject` and improve typing
You don’t need the cast, and you can make the intent clearer with a type predicate:
```ts
isEmptyObject(q: unknown): q is Record<string, unknown> {
return !!q && typeof q === "object" && Object.keys(q).length === 0;
}
```
This avoids the misleading `as boolean` and gives you a useful type guard.
### 2. Loosen `isAlwaysFalseQuery` input type and simplify checks
`isAlwaysFalseQuery` doesn’t need the input to be a `Record<string, unknown>`, and you can keep the CASL-specific check while making the implementation more defensive:
```ts
isAlwaysFalseQuery(q: unknown): boolean {
if (!q || typeof q !== "object") return false;
const expr = (q as { $expr?: { $eq?: unknown } }).$expr;
const eq = expr?.$eq;
return Array.isArray(eq) && eq.length === 2 && eq.includes(0) && eq.includes(1);
}
```
This keeps the same semantics but avoids assuming `$expr` exists on the type.
### 3. Simplify `readAccessFilter` to a single-query flow
Right now you always build an array with a single query and then do `filter` / `some` / `length` checks as if you had multiple sources. Since only one query is ever produced, you can simplify to a single-object flow with identical behavior:
```ts
readAccessFilter(user: JWTUser) {
const abilities = this.caslAbilityFactory.jobAccess(user);
const q = accessibleBy(abilities, Action.JobRead).ofType(JobClass);
// No access at all: CASL's always-false expression
if (this.isAlwaysFalseQuery(q)) {
return { $expr: { $eq: [0, 1] } };
}
// Unrestricted access: empty filter
if (this.isEmptyObject(q)) {
return {};
}
// Restricted access: use CASL-generated filter as-is
return q;
}
```
This preserves the current behavior for the existing use case (single `accessibleBy` call), removes unnecessary branching and `$or` composition, and keeps the CASL-specific normalization in one place. If you later introduce multiple query sources, you can reintroduce combining logic at that time.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Subsection of PR #2748 for jobss. PR depends on #2759 to be merged first.
This unifies the jobsEndpointAccess and jobsAccess functions in CaslAbilityFactory, removes helper functions and instance-level Action elements and adjusts the affected controller to accommodate the change. The job-specific code is extracted into a separate module.
Functions in CaslAbilityFactory for the handling of jobs MongoDB queries were moved to the jobs.controller.utils.ts module.
Changes:
Tests included
Documentation
Summary by Sourcery
Refactor job authorization to centralize CASL job abilities in a dedicated JobAbility service and simplify job access checks across the CASL factory and jobs controller.
Enhancements: