Repository navigation
fix: add access filter and status guard to v3 publisheddata operations - #2886
Conversation
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The authorization check for non-global accessors requires
PublishedDataStatus.PRIVATE, butgetAccessBasedFiltersstill 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-scopedfilterfor the initialfindOnebut then callupdatewith{ 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(insidegetAccessBasedFiltersand 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
The original PR was only to fix the issue with PATCH endpoint. |
|
Adding e2e tests written using Claude for the new behavior |
Junjiequan
left a comment
There was a problem hiding this comment.
This needs to be reviewed more
|
Sorry for the sudden request changes, but I have couple of questions before we merge this PR |
Of course! Thank you for reviewing |
|
My concern is about the missing authorization(
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, 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 |
|
Good points! Carlo and I were also wondering why ownerGroup is absent from the published data schema.
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, 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 |
|
Since this change only affects v3 endpoints and addresses a serious security issue, I'd suggest we proceed @nitrosx ? |
|
If it is a security issue, we should merge. |
…mporary ownership model
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:
Enhancements:
Tests: