Skip to content

fix: add access filter and status guard to v3 publisheddata operations - #2886

Merged
omkar-ethz merged 8 commits into
SciCatProject:masterfrom
knQzx:publisheddata-update-guard
Aug 20, 2026
Merged

omkar-ethz merged 8 commits into
SciCatProject:masterfrom
knQzx:publisheddata-update-guard

Conversation

@knQzx

@knQzx knQzx commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

The v3 publisheddata update had no policy check beyond authentication, so any authenticated user could patch any published data record. This adds an access scoped lookup and a status guard so users can only change their own private records.

Summary by Sourcery

Enforce ownership and lifecycle-state protections across v3 PublishedData operations.

Bug Fixes:

  • Restrict v3 PublishedData reads and mutations to records users are authorized to access, preventing cross-user access to private records.
  • Prevent updates to registered or amended records while allowing authorized updates to private records.

Enhancements:

  • Apply consistent access scoping to PublishedData listing, counting, retrieval, registration, and resynchronization operations, with administrators retaining broader access.

Tests:

  • Add end-to-end coverage for ownership-based access, administrator privileges, private-record visibility, mutation restrictions, and status-based update guards.

@knQzx
knQzx requested a review from a team as a code owner August 12, 2026 16:22

@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:

  • The authorization check for non-global accessors requires PublishedDataStatus.PRIVATE, but getAccessBasedFilters still allows statuses REGISTERED, PUBLIC, and AMENDED, which means users can resolve records they are then forbidden to update; consider aligning the filter with the status guard or separating lookup vs. update concerns more clearly.
  • In update, you build an access-scoped filter for the initial findOne but then call update with { doi: id } instead of the same filter, which could bypass the access constraints; consider reusing the access-based filter for the update operation to ensure consistent enforcement.
  • The CASL ability is instantiated twice in update (inside getAccessBasedFilters and again in the method); consider computing it once and passing it into the helper to avoid redundant work and keep the access logic in one place.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The authorization check for non-global accessors requires `PublishedDataStatus.PRIVATE`, but `getAccessBasedFilters` still allows statuses REGISTERED, PUBLIC, and AMENDED, which means users can resolve records they are then forbidden to update; consider aligning the filter with the status guard or separating lookup vs. update concerns more clearly.
- In `update`, you build an access-scoped `filter` for the initial `findOne` but then call `update` with `{ doi: id }` instead of the same filter, which could bypass the access constraints; consider reusing the access-based filter for the update operation to ensure consistent enforcement.
- The CASL ability is instantiated twice in `update` (inside `getAccessBasedFilters` and again in the method); consider computing it once and passing it into the helper to avoid redundant work and keep the access logic in one place.

## Individual Comments

### Comment 1
<location path="src/published-data/published-data.controller.ts" line_range="359-363" />
<code_context>
+      request.user as JWTUser,
+    );
+    if (ability.cannot(Action.AccessAny, PublishedData)) {
+      filter.$or = [
+        { createdBy: (request.user as JWTUser)?.username },
+        { status: PublishedDataStatus.REGISTERED },
+        { status: PublishedDataStatus.PUBLIC },
+        { status: PublishedDataStatus.AMENDED },
+      ];
+      return filter;
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The `$or` filter in `getAccessBasedFilters` may be broader than needed for updates.

For callers without `AccessAny`, this filter still returns REGISTERED, PUBLIC, and AMENDED records, even though the update logic only permits modifying PRIVATE records. This can expose records that cannot be updated and yields `BAD_REQUEST` instead of `NotFound`. For update-specific endpoints, consider restricting this filter to PRIVATE records created by the user or introducing a mutation-focused variant of `getAccessBasedFilters`.

Suggested implementation:

```typescript
  getAccessBasedFilters(
    request: Request,
    doi: string,
    options?: { forMutation?: boolean },
  ) {
    const filter: FilterQuery<PublishedData> = {
      doi,
    };
    const ability = this.caslAbilityFactory.publishedDataInstanceAccess(
      request.user as JWTUser,
    );

    if (ability.cannot(Action.AccessAny, PublishedData)) {
      const username = (request.user as JWTUser)?.username;

      if (options?.forMutation) {
        // For mutation endpoints, only expose records that the user is allowed to modify
        filter.$and = [
          { createdBy: username },
          { status: PublishedDataStatus.PRIVATE },
        ];
      } else {
        // For read-only endpoints, retain broader visibility rules
        filter.$or = [
          { createdBy: username },
          { status: PublishedDataStatus.REGISTERED },
          { status: PublishedDataStatus.PUBLIC },
          { status: PublishedDataStatus.AMENDED },
        ];
      }

      return filter;
    }

    return filter;
  }

```

1. Update all mutation/update endpoints (e.g. PUT/PATCH handlers for `PublishedData`) that call `getAccessBasedFilters` to pass `{ forMutation: true }` as the third argument so they only see PRIVATE records they can actually modify.
   - Example: `this.getAccessBasedFilters(request, doi, { forMutation: true })`.
2. Ensure `PublishedDataStatus.PRIVATE` exists in the enum and is imported into this controller file if the enum is not already fully imported.
3. If there are any non-update endpoints relying on the previous `$or` behavior, verify they continue to call `getAccessBasedFilters` without the `options` parameter to retain the broader visibility (REGISTERED, PUBLIC, AMENDED, plus own records).
</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.

@omkar-ethz omkar-ethz changed the title fix: add access filter and status guard to v3 publisheddata update fix: add access filter and status guard to v3 publisheddata operations Aug 18, 2026
@omkar-ethz

Copy link
Copy Markdown
Member

The original PR was only to fix the issue with PATCH endpoint.
This also fixes the issue for all other endpoints.

@omkar-ethz

omkar-ethz commented Aug 18, 2026 •

Copy link
Copy Markdown
Member

Adding e2e tests written using Claude for the new behavior

@minottic minottic 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.

thanks!

@Junjiequan Junjiequan 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.

This needs to be reviewed more

@Junjiequan

Copy link
Copy Markdown
Member

Sorry for the sudden request changes, but I have couple of questions before we merge this PR

@omkar-ethz

Copy link
Copy Markdown
Member

Sorry for the sudden request changes, but I have couple of questions before we merge this PR

Of course! Thank you for reviewing

@Junjiequan

Junjiequan commented Aug 18, 2026 •

Copy link
Copy Markdown
Member

My concern is about the missing authorization(ownableClass) on the publishedData schema.

getMutationAccessFilters allows only the createdBy user to update, register, or resync published data. But the pattern used across other SciCat modules is that ownerGroup users can update the entity. Filtering by createdBy means only that specific user can update it, which seems inconsistent to me.

IMO (after a short discussion with Max) a more consistent solution is to extend OwnableClass in the PublishedData schema, then add a migration script to set ownerGroup and accessGroups on existing published data. That needs a bit more discussion though, PublishedData didn't have those 2 fields before, so we'd need to agree on what to assign

Current approach fixes missing authorization issues temporarily, but I don't think this is fix for everyone, especially considering scicat auth pattern is group based onwership

@omkar-ethz

omkar-ethz commented Aug 18, 2026 •

Copy link
Copy Markdown
Member

Good points! Carlo and I were also wondering why ownerGroup is absent from the published data schema.

getMutationAccessFilters allows only the createdBy user to update, register, or resync published data. But the pattern used across other SciCat modules is that ownerGroup users can update the entity. Filtering by createdBy means only that specific user can update it, which seems inconsistent to me.

Agree, i just borrowed this logic from the v4 endpoint. Carlo also mentioned that this breaks the "four eyes" review of a private published data record that used to be possible before - but then, the frontend already uses v4 publisheddata endpoints, so this is already not supported currently.

IMO, as the v3 endpoints are deprecated, and not used in frontend, but only used in landing-page (and https://github.com/SciCatProject/oai-provider-service ?), I think it's better to have this more restrictive behavior as a temporary fix. As currently, even unauthenticated user any authenticated user can read READ private PublishedData, and any authenticated user can PATCH someone else's private PublishedData via the v3 endpoints

But also happy to hold off the PR until the long term solution is discussed. We might just deploy this as a hotfix at PSI in the meantime

@Junjiequan

Copy link
Copy Markdown
Member

Since this change only affects v3 endpoints and addresses a serious security issue, I'd suggest we proceed @nitrosx ?

@nitrosx

nitrosx commented Aug 19, 2026

Copy link
Copy Markdown
Member

If it is a security issue, we should merge.
I would like you to add somewhere in documentation a reference to this hot fix and mention that it is a temporary fix, it deviates from the supported pattern, it will be supported until we redesign the new ownership of published data.

@nitrosx nitrosx mentioned this pull request Aug 19, 2026
4 tasks done
@Junjiequan
Junjiequan dismissed their stale review August 19, 2026 07:15

remove blocker

@omkar-ethz

Copy link
Copy Markdown
Member

@nitrosx i added the comment to a (newly created) authorization section for published data. It is with respect to CASL before the refactor, so if we merge this PR first, we should update the docs in #2812

@omkar-ethz
omkar-ethz merged commit 53cc12b into SciCatProject:master Aug 20, 2026
15 checks passed
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.

5 participants