Skip to content

refactor: casl factory - datasets - #2760

Merged
nitrosx merged 20 commits into
masterfrom
refactor-casl-factory-datasets
Aug 7, 2026
Merged

nitrosx merged 20 commits into
masterfrom
refactor-casl-factory-datasets

Conversation

@HayenNico

@HayenNico HayenNico commented May 26, 2026 •

Copy link
Copy Markdown
Member

Description

Subsection of PR #2748 for datasets. PR depends on #2759 to be merged first.

This unifies the datasetEndpointAccess and datasetInstanceAccess functions in CaslAbilityFactory, removes instance-level Action elements and adjusts all affected controllers to accommodate the change. The dataset-specific code is extracted into a separate module

Fixes

  • The endpoints /proposals/:id/datasets and /samples/:id/datasets would not apply correct filters due to a mismatch between the used casl ability and checked Action elements

Changes:

  • Replace CaslAbilityFactory.datasetsInstanceAccess and CaslAbilityFactory.datasetsEndpointAccess with one function CaslAbilityFactory.datasetAccess
  • Code for CaslAbilityFactory.datasetAccess is factored out into new module DatasetAbility
  • Remove all instance-level dataset Action elements
  • Adjust endpoint and instance auth logic in dataset controllers
  • Remove unused readOwner checks, treat readPublic as always true (datasets-access.service)
  • Adjust auth logic in samples, proposals, users and origdatablocks controllers where dataset abilities are used

Tests included

  • Included for each change/fix?
  • Passing?

Documentation

  • swagger documentation updated (required for API changes)
  • official documentation updated

official documentation info

Summary by Sourcery

Consolidate dataset authorization into a single CASL ability and align controllers and access services with the simplified permission model.

Bug Fixes:

  • Ensure /proposals/:id/datasets, /samples/:id/datasets, and other dataset-related endpoints apply correct filters based on unified dataset permissions for authenticated and unauthenticated users.

Enhancements:

  • Replace separate dataset endpoint and instance access factories with a single datasetAccess ability using unified actions and conditions.
  • Simplify dataset-related controllers, V4 controllers, and access services to rely on generic Dataset* actions instead of owner/access/public-specific variants.
  • Remove numerous instance-level dataset Action enum values in favor of a smaller, clearer action set, adding AccessAny for elevated roles.
  • Treat public dataset reads consistently by defaulting unauthenticated access to published datasets and tightening v4 behavior to require public endpoints for anonymous users.
  • Update Swagger-guarded policies and related controllers (datasets, samples, proposals, origdatablocks, users) to use the new datasetAccess checks.

Tests:

  • Adjust dataset controller specs to mock the new datasetAccess factory method and keep authorization tests passing.

@HayenNico
HayenNico requested a review from a team as a code owner May 26, 2026 17:51

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In several places you now rely on ability.can(Action.DatasetRead, DatasetClass) (or other dataset actions) with the class as subject, but the new datasetAccess rules are all condition-based; CASL ignores conditions when checking against a type, so these checks effectively become unconditional and may grant broader access than intended—consider switching those checks to use an instance or reintroducing explicit owner/access/published predicates where you need them.
  • The new unauthenticated behaviour in DatasetsAccessService.addDatasetAccess / addDatasetAccessToPipeline appears to no longer enforce isPublished: true for lookups (because canView now becomes true due to the generic DatasetRead rule on the class), which could expose non‑public datasets via lookups; it would be safer to explicitly add the isPublished filter for unauthenticated users instead of relying on the generic ability.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In several places you now rely on `ability.can(Action.DatasetRead, DatasetClass)` (or other dataset actions) with the class as subject, but the new `datasetAccess` rules are all condition-based; CASL ignores conditions when checking against a type, so these checks effectively become unconditional and may grant broader access than intended—consider switching those checks to use an instance or reintroducing explicit owner/access/published predicates where you need them.
- The new unauthenticated behaviour in `DatasetsAccessService.addDatasetAccess` / `addDatasetAccessToPipeline` appears to no longer enforce `isPublished: true` for lookups (because `canView` now becomes true due to the generic `DatasetRead` rule on the class), which could expose non‑public datasets via lookups; it would be safer to explicitly add the `isPublished` filter for unauthenticated users instead of relying on the generic ability.

## Individual Comments

### Comment 1
<location path="src/datasets/datasets-access.service.ts" line_range="121-126" />
<code_context>
-      Action.DatasetReadManyAccess,
-      DatasetClass,
-    );
+    const ability = this.caslAbilityFactory.datasetAccess(currentUser);
+    const canViewAny = ability.can(Action.AccessAny, DatasetClass);
+    const canView = ability.can(Action.DatasetRead, DatasetClass);

     if (!canViewAny) {
-      if (canViewAccess) {
+      if (canView) {
         fieldValue.$lookup.pipeline?.unshift({
           $match: {
</code_context>
<issue_to_address>
**issue (bug_risk):** Potential runtime error in `addDatasetAccess` when `currentUser` is undefined.

In `addDatasetAccess`, `currentUser` is still dereferenced when it may be `undefined`:

```ts
const currentUser = this.request.user as JWTUser;
const ability = this.caslAbilityFactory.datasetAccess(currentUser);
const canViewAny = ability.can(Action.AccessAny, DatasetClass);
const canView = ability.can(Action.DatasetRead, DatasetClass);

if (!canViewAny) {
  if (canView) {
    fieldValue.$lookup.pipeline?.unshift({
      $match: {
        $or: [
          { ownerGroup: { $in: currentUser.currentGroups } },
          { accessGroups: { $in: currentUser.currentGroups } },
          { sharedWith: { $in: [currentUser.email] } },
          { isPublished: true },
        ],
      },
    });
  }
}
```

With the new `datasetAccess` behavior, unauthenticated users can have `canView === true` for `DatasetRead` on `DatasetClass`, so this branch will run with `currentUser === undefined`, causing a runtime error. Previously, the branch depended on `canViewAccess`, which was `false` for unauthenticated users.

Please gate this branch on the presence of a user (e.g. `if (currentUser && canView)`) or handle the unauthenticated case separately, similar to `getAccessDetailsForLookup`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/datasets/datasets-access.service.ts Resolved
@HayenNico
HayenNico force-pushed the refactor-casl-factory-datasets branch from 6fa3dd6 to cc8fc38 Compare June 3, 2026 20:30
@HayenNico HayenNico mentioned this pull request Jun 8, 2026
4 tasks done
@nitrosx

nitrosx commented Jun 9, 2026

Copy link
Copy Markdown
Member

Overall review:

Code Review: refactor-casl-factory-datasets Branch

Assessment

The branch refactors CASL authorization for datasets by:

  • Extracting dataset-specific logic from casl-ability.factory.ts (665 lines removed) into a new datasets.ability.ts (202 lines)
  • Simplifying the action model: removed 50+ granular dataset actions (e.g., DatasetReadManyPublic, DatasetReadOneOwner, DatasetAttachmentReadPublic) in favor of 13 coarse-grained actions (e.g., DatasetRead, DatasetCreate)
  • Consolidating type definitions into casl-subjects.ts
  • Updating 15+ files across controllers and services to use the new datasetAccess() method and simplified action names
  • Reducing datasets-access.service.ts from 219 to 146 lines by removing redundant permission checks

Improvement needed

  • Breaking change risk: The simplified action model (DatasetRead vs DatasetReadManyPublic) changes the permission granularity. Controllers now use ability.can(group, datasetInstance) where group is the coarse action. This may break existing client expectations or fine-grained access control.
  • Test coverage: Only 1 test file (DatasetV4.js) was updated. Other dataset-related tests may need updates to reflect the new permission model.
  • Type safety: The Conditions type still uses CASL's MongoQuery which lacks $or support, causing TypeScript errors. Needs type extension.
  • Consistency: Similar refactoring could be applied to other entities (proposals, samples, etc.) for uniformity.

Verdict

Necessary with caveats. The refactoring improves maintainability by reducing duplication and separating concerns. However, it trades fine-grained permissions for simplicity. Verify that:

  1. All existing use cases are covered by the new coarse-grained actions
  2. The permission logic in datasets.ability.ts correctly implements all access rules
  3. All affected tests pass
  4. Type issues with $or in MongoQuery are resolved

If these are confirmed, the changes are a net improvement. If not, revert or extend the action model to preserve necessary granularity.

Generated with Mistral AI

@nitrosx

nitrosx commented Jun 9, 2026 •

Copy link
Copy Markdown
Member

Code Review: refactor-casl-factory-datasets Branch

Comparison against master branch


1. Logic Correctness: VERIFIED ✅

The refactoring maintains equivalent permission logic through a coarse-grained action model:

Old Model (Fine-grained) New Model (Coarse-grained) Equivalence
DatasetReadManyPublic + DatasetReadOnePublic DatasetRead with {isPublished: true} ✅ Same
DatasetReadOneOwner + DatasetReadOneAccess DatasetRead with {ownerGroup: {$in: groups}} + {accessGroups: {$in: groups}} ✅ Same
DatasetCreateOwnerNoPid DatasetCreate with {ownerGroup: {$in: groups}, pid: { $eq: "" }} ✅ Same
DatasetCreateOwnerWithPid DatasetCreate with {ownerGroup: {$in: groups}} ✅ Same
DatasetReadAny AccessAny ✅ Same (new generic action)

CASL behavior: Multiple can() calls for the same action create OR conditions. The new code uses:

can(Action.DatasetRead, DatasetClass, ifOwner);
can(Action.DatasetRead, DatasetClass, ifAccess);
can(Action.DatasetRead, DatasetClass, ifPublished);

This is functionally equivalent to the old model's separate actions.


2. Edge Cases: ALL HANDLED ✅

Edge Case Old Behavior New Behavior Status
Unauthenticated user Could read published datasets can(DatasetRead, ifPublished) defined before early return ✅ Handled
User in multiple groups Permissions accumulated across groups Separate if() blocks (not else if()) accumulate permissions ✅ Handled
Admin + other groups Full access + group-specific permissions Both admin and other group blocks execute ✅ Handled
Empty PID check DatasetCreateOwnerNoPid required pid: "" New code preserves {...ifOwner, pid: { $eq: "" }} ✅ Handled
null/undefined user Explicit checks in controllers if (!user) in ability + controller guards ✅ Handled

3. What the Changed Code Does

Extracted Components:

  • src/casl/abilities/datasets.ability.ts (new, 202 lines): Centralizes all dataset permission logic
  • src/casl/types/casl-subjects.ts (new, 48 lines): Type definitions for CASL subjects and abilities
  • datasetAccess() in CaslAbilityFactory: Delegates to DatasetAbility.buildAbility()

Simplified Action Model:

  • Removed 50+ dataset-specific action variants (e.g., DatasetReadManyPublic, DatasetReadOneOwner)
  • Consolidated to 13 coarse-grained actions (e.g., DatasetRead, DatasetCreate, DatasetAttachmentRead)
  • Introduced AccessAny as a generic super-permission

Controller Changes:

Replaced verbose permission checks:

// OLD
ability.can(Action.DatasetReadAny, DatasetClass) ||
ability.can(Action.DatasetReadOneOwner, datasetInstance) ||
ability.can(Action.DatasetReadOneAccess, datasetInstance) ||
ability.can(Action.DatasetReadOnePublic, datasetInstance)

// NEW
ability.can(Action.DatasetRead, datasetInstance)
  • Unified filter building logic across v3 and v4 controllers

Access Service Changes:

  • Simplified getRelationViewAccess() to use new action model
  • Preserved MongoDB aggregation pipeline logic for access filtering

4. Do the Changes Make Sense?

YES, with architectural benefits:

Aspect Improvement
Maintainability 1208 lines removed, 495 added = 713 net reduction
Separation of Concerns Dataset logic isolated in dedicated class
Consistency Unified permission checking across controllers
Type Safety Centralized type definitions
Testability Smaller, focused ability class easier to test

Trade-offs:

  • Less granular: Cannot distinguish between "read public", "read as owner", "read as access group member" at the action level
  • Higher coupling: Controllers now depend on the ability builder's internal logic for condition evaluation
  • Migration effort: Other entities (proposals, samples, etc.) still use old model

Verdict: The trade-off is worthwhile. The old model's granularity was not being fully utilized (controllers already combined multiple checks with OR logic), and the new model achieves the same runtime behavior with significantly cleaner code.


5. Unreachable Code: NONE FOUND ✅

Analyzed files:

  • datasets.ability.ts: All code paths reachable. The if (!user) early return comes after setting up unauthenticated rules, which is correct (unauthenticated users can read published datasets)
  • datasets-access.service.ts: Line 87 if (access) is always true, but this is not unreachable—it's just a redundant check since getRelationViewAccess() always returns an object. No functional impact.
  • datasets.ability.ts group checks: Uses separate if() (not else if()) intentionally to accumulate permissions for users in multiple groups. This is correct behavior.

Potential issue identified:

  • Line 87 in datasets-access.service.ts: if (access) is redundant but harmless. Could be removed for clarity.

Final Assessment

Category Status Notes
Logic Correctness ✅ PASS Equivalent runtime behavior
Edge Cases ✅ PASS All handled correctly
Code Quality ✅ IMPROVED Better separation of concerns
Unreachable Code ✅ NONE Minor redundant check in access service
Breaking Changes ⚠️ LOW RISK Internal only; API behavior preserved

Recommendation: MERGE — The refactoring is well-executed, maintains correctness, and significantly improves code maintainability.

Generated with Mistral AI

@nitrosx nitrosx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work.
Small code refactoring in dataset abilities regarding the order of the permissions.

I do not understand the need of the need for AccessAny. Could you please elaborate on it?
I do apologize for all the comments about AccessAny on all the files.

Comment thread src/casl/abilities/datasets.ability.ts Resolved
Comment thread src/samples/samples.controller.ts Resolved
Comment thread src/proposals/proposals.controller.ts Resolved
Comment thread src/datasets/datasets.v4.controller.ts Resolved
Comment thread src/datasets/datasets.v4.controller.ts Resolved
Comment thread src/datasets/datasets.controller.ts Resolved
Comment thread src/datasets/datasets-access.service.ts Resolved
@nitrosx

nitrosx commented Jun 9, 2026

Copy link
Copy Markdown
Member

Security Review: refactor-casl-factory-datasets Branch

Comparison against master branch


1. Injection Vulnerabilities: ❌ NONE

Analysis:

  • All MongoDB conditions use CASL's type-safe MongoQuery with values from validated JWT tokens
  • User data (currentGroups, email) is sourced from already-authenticated JWT (not raw user input)
  • No dynamic query construction from user-supplied strings
  • No use of eval, $where, or other MongoDB injection vectors

Example of safe usage:

// datasets.ability.ts
const ifOwner = { ownerGroup: { $in: user?.currentGroups } };
can(Action.DatasetRead, DatasetClass, ifOwner);

→ currentGroups comes from validated JWT, not user input.


2. Sensitive User Data Exposure: ❌ NONE

Analysis:

  • No new endpoints introduced
  • No data serialization changes — only authorization logic refactored
  • Same data returned to clients; only permission checks changed
  • Public/private data visibility controlled by isPublished, ownerGroup, accessGroups — unchanged

Verdict: The refactoring does not expose any additional user data.


3. Insecure API Usage: ❌ NONE

Analysis:

  • No new external API calls introduced
  • No hardcoded credentials, tokens, or secrets
  • No changes to configuration for external services
  • All changes are internal to the authorization layer

Verdict: No insecure API usage patterns introduced.


4. Authentication Bypass: ❌ NONE

Deep Analysis:

Attack Vector Old Code New Code Risk
Unauthenticated read Allowed public datasets via DatasetReadOnePublic Allowed via can(DatasetRead,ifPublished) ✅ Same
Admin escalation DatasetReadAny granted to admin AccessAny + all actions granted to admin ✅ Same
Group-based access Separate actions (DatasetReadOneOwner, DatasetReadOneAccess) Single action with OR conditions ✅ Equivalent
Permission accumulation Multiple can() calls with OR Multiple can() calls with OR ✅ Same

Critical Check: CASL Semantics

  • Multiple can(Action.DatasetRead, ...) calls create OR conditions
  • cannot() rules have higher priority than can() (preserved in new code)
  • The new coarse-grained model is functionally equivalent to the old fine-grained model

V4 Controller Note:
The new code explicitly blocks unauthenticated users in filter methods:

if (!user) {
  // In API v4 unauthorized users must use the public endpoints
  throw new ForbiddenException("Unauthorized access");
}

This is intentional (per comment) and represents a security improvement, not a bypass.


Final Verdict: ALL SECURE ✅

Security Concern Status Notes
Injection Vulnerabilities ❌ NONE Type-safe queries, validated JWT sources
Sensitive Data Exposure ❌ NONE No new endpoints or serialization changes
Insecure API Usage ❌ NONE No external API changes
Authentication Bypass ❌ NONE Equivalent permission logic; no gaps

Conclusion: The refactoring maintains the same security posture while improving code clarity. No security vulnerabilities introduced.

Generated with Mistral Ai

@nitrosx

nitrosx commented Jun 13, 2026

Copy link
Copy Markdown
Member

@HayenNico Thank you for the explanation regarding Action.AccessAny.
That is a great idea.

Last question: Given that we use the following convention in naming: type of entry and action, which results in DatasetRead. I am wondering if we should rename AccessAny to follow the same convention: AnyRead or AllRead?

If you think that this needs a longer discussion, I am ok to merge the Pr as it is, and make a following PR with the additional changes. Keep in mind that there is the PR #2721 which I will refactor it keeping following the changes introduced by this PR.

@nitrosx
nitrosx self-requested a review June 13, 2026 11:32
@HayenNico

HayenNico commented Jun 15, 2026 •

Copy link
Copy Markdown
Member Author

@nitrosx I'm okay with renaming the AccessAny action to something more sensible (I prefer ReadAny, personally), but since it already existed (used for PublishedData and Attachments v4 at least) I would put that separate from this PR.
Maybe the correct place would be the base branch in #2759 that all the casl refactor PRs depend on anyway?

Regarding PR #2721 and dev documentation: I was waiting to write that until #2759 is reviewed and merged and this PR is rebased on master. I think it would be best if you go ahead with #2721 and then we can refactor that document in this branch to mirror the new changes

Base automatically changed from refactor-casl-factory-cleanup to master June 26, 2026 09:56
@nitrosx

nitrosx commented Jul 3, 2026

Copy link
Copy Markdown
Member

@HayenNico let's keep AccessAny as it is for now.
We can tackle the naming while we complete #2721.

@HayenNico
HayenNico requested a review from a team July 22, 2026 12:45
@nitrosx

nitrosx commented Aug 5, 2026

Copy link
Copy Markdown
Member

Code Review Report: Branch refactor-casl-factory-datasets


Overview

  1. Verify logic correctness: The refactoring consolidates dataset authorization into a single DatasetAbility class with unified action definitions. The logic appears correct and maintains the same permission semantics.

  2. Check if all edge cases are handled: The new implementation handles unauthenticated users, authenticated users, and all special permission groups (CREATE_DATASET_GROUPS, CREATE_DATASET_WITH_PID_GROUPS, CREATE_DATASET_PRIVILEGED_GROUPS, UPDATE_DATASET_LIFECYCLE_GROUPS, ADMIN_GROUPS, DELETE_GROUPS). Edge cases for undefined accessGroups are handled via optional chaining.

  3. Summarize what the code touched by the changes does: The changes refactor the CASL authorization system for datasets, consolidating 40+ dataset-specific actions into 17 main actions and moving all dataset authorization logic into a dedicated DatasetAbility service class. The permission matrix and behavior remain the same, but the implementation is cleaner and more maintainable.

  4. Assess whether the changes make sense: The refactoring is well-executed and makes sense. It reduces code duplication, simplifies the action enum, and creates a single source of truth for dataset authorization. The changes align with the documented authorization model.

  5. Identify any unreachable code: No unreachable code identified. The old datasetEndpointAccess and datasetInstanceAccess methods were completely removed, which is appropriate.

Responsible commits: Main refactoring in 3901fed8, e59abfcc, 50259abd, b9bafaa6, cc8fc383, f8decd48, bbafa84b; documentation added in d735094c; cleanup in 3697e2e5, e1db9f5a.


Code Changes

Major Changes

File Change Type Description
src/casl/abilities/datasets.ability.ts New New dedicated service for dataset authorization logic
docs/developer-guide/authorization/datasets.md New Comprehensive documentation of authorization model
src/casl/action.enum.ts Modified Removed 44 old dataset-specific actions, kept 17 main actions
src/casl/casl-ability.factory.ts Modified Removed datasetEndpointAccess and datasetInstanceAccess, added datasetAccess method
src/casl/casl.module.ts Modified Added DatasetAbility to providers
src/datasets/datasets-access.service.ts Modified Updated to use new action names and datasetAccess
src/datasets/datasets.controller.ts Modified Updated all authorization checks to use simplified actions
src/datasets/datasets.v4.controller.ts Modified Updated authorization checks for API v4
src/origdatablocks/origdatablocks.v4.controller.ts Modified Updated dataset authorization checks
src/proposals/proposals.controller.ts Modified Updated dataset access checks
src/samples/samples.controller.ts Modified Updated dataset access checks
src/users/users.controller.ts Modified Updated dataset creation authorization

Assessment

Necessary: Yes. The old system had:

  • 40+ dataset-specific action types causing complexity
  • Duplicate authorization logic between endpoint and instance access
  • Inconsistent action naming conventions
  • Hard-to-maintain permission matrices

Improvement Needed:

  • The DatasetAbility class could benefit from unit tests (currently only integration tests exist)
  • Some controller methods could be further simplified (minor redundancy in filter building)
  • Consider adding validation for accessGroups configuration to ensure required fields exist

Verdict: Excellent refactoring. The changes significantly improve code maintainability while preserving all functionality. The new structure is cleaner, better documented, and follows SOLID principles better.


Security Review

  1. Potential injection vulnerabilities: None identified. The authorization logic uses CASL's built-in MongoDB query condition building which is safe from injection. All user input is properly validated through the authorization framework.

  2. Sensitive user data exposure: None identified. The changes only affect authorization logic, not data retrieval or exposure patterns. The permission system properly restricts access based on group membership and publication status.

  3. Insecure API usage: None identified. The CASL library is used correctly with proper ability definitions and condition checks.

  4. Authentication bypass: None identified. The refactoring maintains the same security posture:

    • Unauthenticated users can only read public datasets
    • Authenticated users need proper group membership
    • All write operations require appropriate permissions
    • Special groups (admin, delete) have explicit checks

Responsible commits: All security-relevant changes are in the core refactoring commits (3901fed8, e59abfcc, 50259abd).


Test Coverage

Current Coverage

  • Unit tests: src/casl/casl-ability.factory.spec.ts - Updated to work with new structure, but only tests factory instantiation and one specific permission
  • Integration tests: Controller tests updated to use new method names
  • New file: src/casl/abilities/datasets.ability.ts - No dedicated unit tests

Assessment

Adequate for existing functionality: Yes, the integration tests cover the main use cases through the controllers.

Improvement Needed:

  • High priority: Add unit tests for DatasetAbility class covering:
    • Unauthenticated user scenarios
    • Authenticated user with various group memberships
    • Each permission group (CREATE_DATASET_GROUPS, ADMIN_GROUPS, etc.)
    • Edge cases (empty accessGroups config, undefined user, etc.)
  • Medium priority: Add tests for the permission matrix to ensure it matches the documentation

Test Suggestions

// Example test structure for DatasetAbility
describe('DatasetAbility', () => {
  describe('Unauthenticated user', () => {
    it('should allow read on public datasets');
    it('should deny write operations');
  });

  describe('Authenticated user', () => {
    it('should allow read on owned datasets');
    it('should allow read on access group datasets');
    it('should allow read on public datasets');
  });

  describe('CREATE_DATASET_GROUPS', () => {
    it('should allow create with empty pid and matching ownerGroup');
    it('should deny create with non-empty pid');
  });

  // ... tests for other permission groups
});

Responsible commits: Test updates in ce1ec06f, 3697e2e5, but no dedicated DatasetAbility tests added.


Security Examples

No relevant vulnerabilities found. The refactoring maintains proper security controls:

  • All dataset read operations require either:

    • isPublished: true for unauthenticated
    • Matching ownerGroup or accessGroups for authenticated users
    • Special group membership for elevated access
  • All write operations require:

    • Specific permission group membership
    • Owner/group matching conditions where applicable

Example of secure pattern from datasets.ability.ts:

// Unauthenticated - only public read access
can(Action.DatasetRead, DatasetClass, ifPublished);

// Authenticated - read access to owned/access-group/public datasets
can(Action.DatasetRead, DatasetClass, ifOwner);
can(Action.DatasetRead, DatasetClass, ifAccess);
can(Action.DatasetRead, DatasetClass, ifPublished);

// CREATE_DATASET_GROUPS - can create only with empty pid and matching owner
can(Action.DatasetCreate, DatasetClass, { ...ifOwner, pid: { $eq: "" } });

Testing for Security Use Cases

Test Cases to Verify Security

  1. Unauthenticated user access:

    • Test: GET /datasets without authentication
    • Expected: Only datasets with isPublished: true are returned
    • Code: datasets.controller.ts:1421-1428 (filter building for unauthenticated)
  2. Authenticated user without special groups:

    • Test: GET /datasets with authenticated user not in any special group
    • Expected: Only datasets where user's currentGroups match ownerGroup or accessGroups, plus public datasets
    • Code: datasets.ability.ts:57-71 (authenticated read permissions)
  3. CREATE_DATASET_GROUPS user creating dataset:

    • Test: POST /datasets with user in CREATE_DATASET_GROUPS, providing explicit pid
    • Expected: 403 Forbidden (pid must be empty for this group)
    • Code: datasets.ability.ts:84-87 (create with pid restriction)
  4. CREATE_DATASET_WITH_PID_GROUPS user creating dataset:

    • Test: POST /datasets with user in CREATE_DATASET_WITH_PID_GROUPS, providing explicit pid
    • Expected: 201 Created (pid allowed for this group)
    • Code: datasets.ability.ts:111 (create without pid restriction)
  5. DELETE_GROUPS user:

    • Test: DELETE /datasets/:id with user in DELETE_GROUPS
    • Expected: 200 OK (any dataset can be deleted)
    • Code: datasets.ability.ts:182 (unrestricted delete)
  6. ADMIN_GROUPS user:

    • Test: PUT /datasets/:id with user in ADMIN_GROUPS, dataset not owned by user
    • Expected: 200 OK (admin can update any dataset)
    • Code: datasets.ability.ts:155-173 (unrestricted access)
  7. UPDATE_DATASET_LIFECYCLE_GROUPS user:

    • Test: PATCH /datasets/:id/datasetlifecycle with user in UPDATE_DATASET_LIFECYCLE_GROUPS
    • Expected: 200 OK for any dataset, but only datasetLifecycle field can be updated
    • Code: datasets.ability.ts:188-200 (lifecycle update permissions)
    • Note: The endpoint itself should enforce field-level restrictions

Verification Commands

# Run existing tests
npm test -- src/casl/casl-ability.factory.spec.ts

# Check for any authorization-related test failures
npm test -- --grep "dataset"

# Manual testing with different user roles
# Use Postman or similar to test each endpoint with various user contexts

Relevant code locations:

  • Authorization logic: src/casl/abilities/datasets.ability.ts
  • Controller authorization checks: All dataset controller files, lines using caslAbilityFactory.datasetAccess()
  • Filter building: src/datasets/datasets.controller.ts:1421-1468 and similar in v4 controller

Summary

Key Findings

Aspect Status Details
Logic Correctness Pass Refactoring maintains identical permission semantics
Edge Cases Pass All user types and permission groups handled
Code Quality Excellent Significant improvement in maintainability
Security Secure No vulnerabilities introduced; same security posture maintained
Test Coverage Partial Integration tests exist, but DatasetAbility lacks unit tests
Documentation Excellent Comprehensive developer documentation added

Consolidated Assessment

The refactor-casl-factory-datasets branch introduces a well-executed, security-preserving refactoring of the dataset authorization system. The changes:

  1. Consolidate 40+ action types into 17 clear, semantic actions
  2. Centralize dataset authorization logic in a dedicated service
  3. Document the complete authorization model for developers
  4. Maintain all existing security controls and permission semantics
  5. Improve code maintainability and reduce duplication

Primary Risk: None identified. The refactoring is purely structural and does not change the authorization behavior.

Primary Recommendation: Add comprehensive unit tests for the new DatasetAbility class to ensure the permission matrix works as documented.

Overall Verdict: Approve for merge. The changes are beneficial, well-implemented, and maintain security. The only outstanding item is adding unit tests for the new authorization service, which should be done before or shortly after merging.

Generated with Mistral Ai

@nitrosx
nitrosx merged commit 45153cb into master Aug 7, 2026
18 of 20 checks passed
@nitrosx
nitrosx deleted the refactor-casl-factory-datasets branch August 7, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants