Skip to content

Fix spoofing vulnerability (Resolves #15)#75

Merged
omatheusmesmo merged 40 commits into
omatheusmesmo:mainfrom
felipemelozx:fix/id-spoofing-vulnerability
Apr 18, 2026
Merged

Fix spoofing vulnerability (Resolves #15)#75
omatheusmesmo merged 40 commits into
omatheusmesmo:mainfrom
felipemelozx:fix/id-spoofing-vulnerability

Conversation

@felipemelozx
Copy link
Copy Markdown
Contributor

@felipemelozx felipemelozx commented Feb 22, 2026

Pull Request Description

This PR implements a complete fix for an ID Spoofing (IDOR) vulnerability that allowed authenticated users to access or modify resources belonging to other users by manipulating user IDs in request payloads.


Related Task

Resolves #15


What was done?

  • Updated the controllers to use @AuthenticationPrincipal, allowing the authenticated user to be automatically injected from the SecurityContextHolder.
  • Modified the User entity to implement UserDetails, enabling Spring Security to manage it directly after authentication through the JWT filter.
  • Added ownership validation in the service layer to ensure that the logged-in user is authorized to perform actions on the requested resources. Before executing any update, delete, or access operation, the system verifies whether the authenticated user matches the resource owner. If not, a ResourceOwnershipException is thrown and an HTTP 403 Forbidden response is returned.
  • Removed the idUser field from request DTOs to eliminate the spoofing vector.

Tests

  • I tested the changes locally and added both unit and integration tests.

How to Test

  1. Authenticate as User A and create a shopping list.
  2. Authenticate as User B.
  3. Attempt to:
    • Access User A's shopping list
    • Update it
    • Delete it
  4. Verify that the API returns HTTP 403 Forbidden.
  5. Confirm that User A can still access and manage their own resources normally.

Useful Links

Summary by CodeRabbit

  • New Features

    • Added user authentication with secure access control—users can only view and modify their own shopping lists.
    • Implemented sharing functionality allowing list owners to grant permissions to other users.
    • Enhanced list retrieval to display only accessible lists per user.
  • Documentation

    • Simplified getting started guide with separate backend and frontend setup instructions, replacing previous global environment configuration.
  • Tests

    • Added comprehensive integration tests validating security controls across shopping lists, list items, and permissions.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

🤖🎉 Thank you for your contribution! Your pull request has been submitted successfully. A maintainer from the team will review it as soon as possible. We appreciate your support in making this project better!

Copy link
Copy Markdown
Owner

@omatheusmesmo omatheusmesmo left a comment

Choose a reason for hiding this comment

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

Pull Request Description

This PR implements a complete fix for an ID Spoofing (IDOR) vulnerability that allowed authenticated users to access or modify resources belonging to other users by manipulating user IDs in request payloads.

Related Task

Resolves #15

What was done?

  • Updated the controllers to use @AuthenticationPrincipal, allowing the authenticated user to be automatically injected from the SecurityContextHolder.
  • Modified the User entity to implement UserDetails, enabling Spring Security to manage it directly after authentication through the JWT filter.
  • Added ownership validation in the service layer to ensure that the logged-in user is authorized to perform actions on the requested resources. Before executing any update, delete, or access operation, the system verifies whether the authenticated user matches the resource owner. If not, a ResourceOwnershipException is thrown and an HTTP 403 Forbidden response is returned.
  • Removed the idUser field from request DTOs to eliminate the spoofing vector.

Tests

  • I tested the changes locally and added both unit and integration tests.

How to Test

  1. Authenticate as User A and create a shopping list.

  2. Authenticate as User B.

  3. Attempt to:

    • Access User A's shopping list
    • Update it
    • Delete it
  4. Verify that the API returns HTTP 403 Forbidden.

  5. Confirm that User A can still access and manage their own resources normally.

Useful Links

Hello @felipemelozx, congratulations on your first PR, thanks for it! Please run mvn net.revelc.code.formatter:formatter-maven-plugin:2.29.0:format to resolve the style formatting issue.

Copy link
Copy Markdown
Owner

@omatheusmesmo omatheusmesmo left a comment

Choose a reason for hiding this comment

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

Code Review - PR #75 (Fix spoofing Vulnerability)

Hello, @felipemelozx! Great job on addressing the spoofing vulnerability (#15). Migrating to @AuthenticationPrincipal and cleaning up the DTOs are excellent steps toward a more secure and standard Spring Security implementation.

I have analyzed the changes and identified some points for refinement to ensure we maintain both performance and the core collaborative features of ShoppMate.


1. Collaborative Access vs. Data Redundancy

The current implementation in ShoppingListService restricts access exclusively to the owner. While this fixes the spoofing, it inadvertently disables the ListPermission system (the Many-to-Many relationship between Users and Lists).

  • The Issue: If two users need to share a list, they are now forced to create two identical list records in the database because the system no longer recognizes shared permissions. This leads to data redundancy and loss of synchronization.
  • The Fix: Access logic should allow both the owner OR any user with a valid entry in list_permissions. This preserves the Many-to-Many architecture designed for collaboration.

2. Performance: Unified Fetch & Verify

Currently, the Controllers perform two separate queries: one to verify ownership and another to fetch the data.

  • Optimization: Instead of a separate verifyOwnership call, we should implement a unified method that fetches the entity and checks permissions. By reusing the existing findListById(id), we keep the code DRY while ensuring only one query is made to fetch the list.
    • Example (Repository):
    // ListPermissionRepository.java
    boolean existsByShoppingListIdAndUserIdAndDeletedFalse(Long listId, Long userId);
    • Example (Service):
    public ShoppingList findAndVerifyAccess(Long listId, User user) {
        ShoppingList list = findListById(listId); // Reuses existing fetch & exception logic
        
        boolean isOwner = list.getOwner().getId().equals(user.getId());
        boolean hasPermission = permissionRepository.existsByShoppingListIdAndUserIdAndDeletedFalse(listId, user.getId());
    
        if (!isOwner && !hasPermission) {
            throw new ResourceOwnershipException("Access Denied: You do not have permission to access this list.");
        }
        return list;
    }

3. Fetch Type EAGER in Item.java

The change from LAZY to EAGER for Category and Unit in the Item entity is concerning.

  • N+1 Risk: This may trigger unnecessary joins or multiple queries when listing items, significantly impacting performance as the database grows.
  • Scope: This change appears unrelated to the spoofing fix. I recommend reverting to LAZY to keep the implementation focused and performant.

These refinements will ensure that ShoppMate remains secure without sacrificing its collaborative nature or database efficiency. Great foundation on the security layer!

@github-actions
Copy link
Copy Markdown
Contributor

🤖 Hi @felipemelozx, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions
Copy link
Copy Markdown
Contributor

🤖 I'm sorry @felipemelozx, but I was unable to process your request. Please see the logs for more details.

@omatheusmesmo
Copy link
Copy Markdown
Owner

@felipemelozx , there is some git conflicts, could you please fix them?

@felipemelozx
Copy link
Copy Markdown
Contributor Author

Got it, I'll fix the conflicts.

@github-actions
Copy link
Copy Markdown
Contributor

🤖 Hi @felipemelozx, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions
Copy link
Copy Markdown
Contributor

🤖 I'm sorry @felipemelozx, but I was unable to process your request. Please see the logs for more details.

@github-actions
Copy link
Copy Markdown
Contributor

🤖 Hi @felipemelozx, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions
Copy link
Copy Markdown
Contributor

🤖 I'm sorry @felipemelozx, but I was unable to process your request. Please see the logs for more details.

@omatheusmesmo
Copy link
Copy Markdown
Owner

Got it, I'll fix the conflicts.

@felipemelozx ,Something went wrong during the merge. See the CI logs for details.

Error:  Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project shoppmate: Compilation failure: Compilation failure: 
Error:  /home/runner/work/ShoppMate/ShoppMate/backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionService.java:[12,1] class, interface, enum, or record expected
Error:  /home/runner/work/ShoppMate/ShoppMate/backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionService.java:[15,1] class, interface, enum, or record expected
Error:  /home/runner/work/ShoppMate/ShoppMate/backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListItemService.java:[12,1] class, interface, enum, or record expected
Error:  /home/runner/work/ShoppMate/ShoppMate/backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListItemService.java:[16,1] class, interface, enum, or record expected
Error:  -> [Help 1]

@github-actions
Copy link
Copy Markdown
Contributor

🤖 Hi @felipemelozx, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements user-based access control throughout the shopping list API by extracting authenticated users from Spring Security's context rather than relying on request body IDs. Key changes include updating the User entity to implement UserDetails, injecting authenticated users via @AuthenticationPrincipal in controllers, propagating user context through service layers for ownership validation, adding user-scoped repository queries, removing idUser from DTOs, and introducing comprehensive integration tests for security enforcement.

Changes

Cohort / File(s) Summary
Documentation
README.md
Reworked Getting Started section with per-component (Backend/Frontend) run instructions, replacing global environment setup and Docker guidance with explicit backend and frontend startup commands.
User & Authentication
backend/src/main/java/com/omatheusmesmo/shoppmate/user/entity/User.java, backend/src/main/java/com/omatheusmesmo/shoppmate/auth/service/CustomUserDetailsService.java
User entity now implements Spring Security's UserDetails interface with authority and account state methods; CustomUserDetailsService simplified to delegate directly to repository, returning UserDetails-compatible User objects.
Shopping List Access Control
backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListController.java, backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ShoppingListService.java, backend/src/main/java/com/omatheusmesmo/shoppmate/list/repository/ShoppingListRepository.java, backend/src/main/java/com/omatheusmesmo/shoppmate/list/entity/ShoppingList.java
Controllers now inject @AuthenticationPrincipal User and enforce user-scoped operations; service layer validates ownership before access; repository adds user-scoped query methods (findByOwnerIdAndDeletedFalse, findAllAccessibleByUserId, findByIdAndUserId); ShoppingList owner field changed from OneToOne to ManyToOne relationship.
Shopping List DTOs & Mappers
backend/src/main/java/com/omatheusmesmo/shoppmate/list/dtos/ShoppingListRequestDTO.java, backend/src/main/java/com/omatheusmesmo/shoppmate/list/mapper/ListMapper.java
Removed idUser field from ShoppingListRequestDTO; ListMapper.toEntity now accepts User owner parameter instead of resolving internally.
List Item Access Control
backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ListItemController.java, backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListItemService.java, backend/src/main/java/com/omatheusmesmo/shoppmate/list/repository/ListItemRepository.java
Controllers inject authenticated user and validate listId in request path; service methods thread User parameter through with list-scoped access checks via findAndVerifyAccess; repository adds item-scoped query methods with list context filtering (findByIdAndShoppListIdAndDeletedFalseFetchShoppList, etc.).
List Item Mappers & Null Handling
backend/src/main/java/com/omatheusmesmo/shoppmate/list/mapper/ListItemMapper.java
Enhanced null safety for unitPrice and quantity with getSafeUnitPrice helper and defensive defaulting to ZERO in calculations.
List Permission Access Control
backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ListPermissionController.java, backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionService.java
Controllers inject authenticated user; service adds requester-based ownership validation for permission operations, throwing ResourceOwnershipException on unauthorized access; all permission methods now accept User parameter for access checks.
Item Repository Optimization
backend/src/main/java/com/omatheusmesmo/shoppmate/item/repository/ItemRepository.java, backend/src/main/java/com/omatheusmesmo/shoppmate/item/service/ItemService.java
Added findByIdWithRelations method using LEFT JOIN FETCH to eagerly load category and unit, reducing N+1 queries; ItemService updated to use new method.
Exception Handling
backend/src/main/java/com/omatheusmesmo/shoppmate/utils/exception/ResourceOwnershipException.java, backend/src/main/java/com/omatheusmesmo/shoppmate/utils/exception/GlobalExceptionHandler.java
New ResourceOwnershipException runtime exception for unauthorized access; GlobalExceptionHandler updated to catch and return HTTP 403 FORBIDDEN with "Access Denied" message.
Frontend Service Layer
frontend/src/app/shared/services/*.ts (category.service.ts, item.service.ts, list-permission.service.ts, shopping-list.service.ts, unit.service.ts, user.service.ts)
Added explicit no-argument constructors calling super() across all service classes extending BaseService.
Frontend Utilities & Mocks
frontend/src/app/shared/mocks/mock-services.ts, frontend/src/app/shared/components/unit/unit.component.ts
Removed getCurrentUserId mock method from MockAuthService; added type annotation for delete confirmation subscription.
Backend Unit Tests
backend/src/test/java/com/omatheusmesmo/shoppmate/list/service/ListItemServiceTest.java, backend/src/test/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionServiceTest.java, backend/src/test/java/com/omatheusmesmo/shoppmate/list/service/ShoppingListServiceTest.java, backend/src/test/java/com/omatheusmesmo/shoppmate/service/ItemServiceTest.java
Updated service tests to include User context in method calls; repository mocks switched to user-scoped variants (findByIdAndUserId, findByIdWithRelations); ownership validation paths added with ResourceOwnershipException assertions.
Backend Security Integration Tests
backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListControllerSecurityIntegrationTest.java, backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListItemControllerSecurityIntegrationTest.java, backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListPermissionControllerSecurityIntegrationTest.java
New comprehensive integration test suites (880+ lines total) validating security constraints: cross-user access denial, ownership enforcement on CRUD operations, token authorization, and edge cases across shopping lists, list items, and permissions.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Controller as Spring MVC<br/>Controller
    participant Security as Security<br/>Context
    participant Service as Service<br/>Layer
    participant Repository as JPA<br/>Repository
    participant Database as Database

    Client->>Controller: HTTP Request + JWT
    Controller->>Security: Extract Auth
    Security-->>Controller: User Principal
    Controller->>Service: invoke(resource_id, User)
    Service->>Service: Verify Ownership<br/>via findAndVerifyAccess()
    alt User Owns Resource
        Service->>Repository: fetch with user scope
        Repository->>Database: SELECT WHERE user_id = ?
        Database-->>Repository: Resource Data
        Repository-->>Service: Entity
        Service-->>Controller: Success Response
        Controller-->>Client: HTTP 200 + Data
    else User Lacks Access
        Service-->>Controller: ResourceOwnershipException
        Controller-->>Client: HTTP 403 Forbidden
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #98: Updates frontend DI patterns with explicit service constructors, directly related to the constructor additions across frontend services in this PR.
  • PR #102: Overlaps significantly on backend domain changes (shopping lists, list items, repositories, mappers, DTOs, UserDetails integration, and test updates).

Suggested reviewers

  • omatheusmesmo

Poem

🐰 Whiskers twitch with joy and cheer,
No more spoofing schemes to fear!
Users bounded by their own,
Each resource safely shown,
Security context guards the way,
Keep the baddies far at bay! 🔒

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Multiple out-of-scope changes detected: documentation/README updates unrelated to the security fix, explicit constructors added to multiple frontend services without security justification, and unnecessary enum persistence annotation. Remove README.md changes, frontend service constructors, and ListPermission.java enum annotation; keep only security-critical changes (controllers, services, entities, tests, DTOs, repositories).
Docstring Coverage ⚠️ Warning Docstring coverage is 0.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main objective: fixing an ID spoofing vulnerability. It is concise, specific, and directly related to the changeset.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #15: removes idUser from DTOs, uses @AuthenticationPrincipal for user injection, adds ownership validation with ResourceOwnershipException, and includes integration tests for cross-user access prevention.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

🤖 I'm sorry @felipemelozx, but I was unable to process your request. Please see the logs for more details.

@omatheusmesmo
Copy link
Copy Markdown
Owner

@felipemelozx , hi!

I made a huge change in the develop branch and there is more conflicts, could you please fix again?

Thanks.

@omatheusmesmo
Copy link
Copy Markdown
Owner

Hi @felipemelozx ! A better approach would be update your remote repository and then run the following commands inside your local feature branch.

git fetch origin
git rebase origin main

@felipemelozx
Copy link
Copy Markdown
Contributor Author

Hi @omatheusmesmo ! I ran the commands but nothing changed in my branch. Do you think it would be better to create a new branch from the latest develop and move the changes there? It might be easier to work from a clean branch.

@omatheusmesmo
Copy link
Copy Markdown
Owner

Hi @omatheusmesmo ! I ran the commands but nothing changed in my branch. Do you think it would be better to create a new branch from the latest develop and move the changes there? It might be easier to work from a clean branch.

@felipemelozx did you update your remote branch first?

@felipemelozx
Copy link
Copy Markdown
Contributor Author

Yes, I updated it.

@omatheusmesmo
Copy link
Copy Markdown
Owner

Yes, I updated it.

Oh, that's weird if 'git status' doesn't show commits to push.

You don't need to create a new branch on the remote. You can delete your local branch and recreate it from the updated main. Just be sure to stash your changes to this feature before deleting the branch. In the end, you can run 'git push -f' to update the remote branch.

Hope works fine.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/src/main/java/com/omatheusmesmo/shoppmate/auth/configs/RsaKeyConfig.java (1)

26-33: ⚠️ Potential issue | 🟠 Major

Restore resource validation and fix resource handling for JWT key loading

Lines 26 and 38 load keys from externally configured Resource values without validating existence, readability, or scheme. This allows unsafe key origins via environment variable override (e.g., JWT_PRIVATE_KEY_PATH=https://attacker.com/key.pem), weakening trust boundaries for signing/verification keys. Additionally, the InputStream created by getInputStream() is never explicitly closed, and new String(byte[]) lacks an explicit charset.

🔧 Proposed fix
@@
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.io.Resource;
+import java.nio.charset.StandardCharsets;
 
 import java.security.KeyFactory;
 import java.security.interfaces.RSAPrivateKey;
 import java.security.interfaces.RSAPublicKey;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.security.spec.X509EncodedKeySpec;
 import java.util.Base64;
@@
     `@Bean`
     public RSAPrivateKey privateKey() throws Exception {
+        validateResource(privateKeyResource, "jwt.private-key");
-        String key = new String(privateKeyResource.getInputStream().readAllBytes());
+        String key = new String(privateKeyResource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
         key = key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replaceAll("\\s", "");
@@
     `@Bean`
     public RSAPublicKey publicKey() throws Exception {
+        validateResource(publicKeyResource, "jwt.public-key");
-        String key = new String(publicKeyResource.getInputStream().readAllBytes());
+        String key = new String(publicKeyResource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
         key = key.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replaceAll("\\s", "");
@@
+    private void validateResource(Resource resource, String propertyName) throws Exception {
+        if (!resource.exists() || !resource.isReadable()) {
+            throw new IllegalStateException("Invalid key resource for " + propertyName);
+        }
+        URI uri = resource.getURI();
+        String scheme = uri.getScheme();
+        if (scheme != null && !scheme.equals("file") && !scheme.equals("classpath")) {
+            throw new IllegalStateException("Unsupported key resource scheme for " + propertyName + ": " + scheme);
+        }
+    }
 }

Also applies to: 38-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/auth/configs/RsaKeyConfig.java`
around lines 26 - 33, In RsaKeyConfig, validate and safely load the RSA keys:
before reading privateKeyResource (and likewise publicKeyResource), check
resource.exists() and resource.isReadable(), and reject or throw if the resource
URL has an untrusted remote scheme (e.g., only allow "file" or "classpath"
schemes) to prevent loading from attacker-controlled URLs; read the stream using
a try-with-resources (or explicitly close the InputStream) and construct the
String with an explicit charset (StandardCharsets.UTF_8) instead of new
String(byte[]), then proceed to Base64-decode and build the PKCS8EncodedKeySpec;
apply the same validation/stream-closing/charset fixes to the public key loading
path as well.
README.md (1)

74-100: ⚠️ Potential issue | 🟡 Minor

Duplicate and mislabeled sections in manual setup instructions.

There appear to be merge artifacts or copy-paste errors:

  1. Lines 74-79: Labeled "Frontend" but contain Java/Maven backend commands (should likely be removed or correctly labeled)
  2. Lines 92-100: Duplicate the npm instructions already present in lines 82-91
📝 Proposed fix to remove duplicates and fix mislabeling
-#### Frontend
-2. Ensure you have Java 17 installed.
-3. Run with:
-   ```bash
-   ./mvnw spring-boot:run
-   ```
-
 #### Frontend
 1. Navigate to `frontend/`.
 2. Install dependencies:
    ```bash
    npm install
    ```
 3. Run with:
    ```bash
    npm start
    ```
 4. Access at [http://localhost:4200](http://localhost:4200).
-2. Install dependencies:
-   ```bash
-   npm install
-   ```
-3. Run with:
-   ```bash
-   npm start
-   ```
-4. Access at [http://localhost:4200](http://localhost:4200).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 74 - 100, Remove the duplicate and mislabelled
sections in the README by deleting the Maven/backend snippet that is incorrectly
under the "Frontend" heading and removing the repeated npm install/start block;
ensure there is a single correctly labeled "Backend" or "Server" section
containing the Java 17 and ./mvnw spring-boot:run instructions and a single
"Frontend" section that lists Navigate to `frontend/`, npm install, npm start,
and the http://localhost:4200 URL so the README no longer contains duplicated
npm instructions or the misplaced Maven commands.
🧹 Nitpick comments (7)
.github/workflows/gemini-triage.yml (1)

62-62: Consider pinning run-gemini-cli to a specific commit SHA.

The action uses @v0 which is a floating tag that may change unexpectedly. While the # ratchet:exclude comment indicates this is intentional, floating tags can introduce breaking changes or security issues without notice.

As per coding guidelines: "Use specific action versions (commit SHA or tagged versions)." Consider pinning to a commit SHA once the action stabilizes, or document the rationale for using the floating tag.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/gemini-triage.yml at line 62, The workflow currently
references the floating tag 'google-github-actions/run-gemini-cli@v0' (with '#
ratchet:exclude') which can change; update the workflow to pin the action to a
specific commit SHA or stable tag (or add a short justification comment if you
must keep the floating tag) so CI behavior is deterministic; locate the uses
line referencing 'google-github-actions/run-gemini-cli@v0' and replace '@v0'
with a concrete '@<commit-sha>' or '@<tag>' (or add an explicit rationale near
the '# ratchet:exclude' comment).
backend/src/test/java/com/omatheusmesmo/shoppmate/service/ItemServiceTest.java (1)

109-127: Please make sure the real query is exercised outside Mockito.

These changes only swap the mocked repository method, so this suite still won't catch a broken findByIdWithRelations(...) implementation or regressions in the deleted = false behavior. If that coverage does not already exist elsewhere, add a repository/integration test for the new lookup.

Also applies to: 136-186

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/service/ItemServiceTest.java`
around lines 109 - 127, The test ItemServiceTest currently only mocks
itemRepository.findByIdWithRelations(...) so it doesn't exercise the real
repository query or the deleted = false filter; add an integration/repository
test that uses the actual ItemRepository (not Mockito) to persist an Item with
deleted=false and another with deleted=true, then call the real repository
method findByIdWithRelations(id) (or the JPA query backing it) to assert the
deleted=true item is not returned and deleted=false is returned; ensure the test
class boots Spring context or uses `@DataJpaTest` and saves entities via
ItemRepository before invoking findByIdWithRelations to verify the real query
behavior and prevent regressions.
backend/src/main/java/com/omatheusmesmo/shoppmate/item/service/ItemService.java (1)

55-57: Separate the read lookup from the write precheck.

This method is now doing double duty: read paths need the richer load, but removeItem()/editItem() only use it to confirm existence, and findItem(Item) still follows a different lookup path. Splitting those responsibilities would keep lookup semantics consistent and avoid extra work on mutation flows. As per coding guidelines, "You are a Senior Java Architect and Spring Boot expert. You focus on writing high-performance, secure, and maintainable microservices using modern Java 17 patterns."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/item/service/ItemService.java`
around lines 55 - 57, Split the responsibilities of ItemService.findById into a
rich-read method and a lightweight existence check: keep a new read method
(e.g., findByIdWithRelations or keep findById but ensure it uses
itemRepository.findByIdWithRelations(id) and throws NoSuchElementException) for
read paths that need full relations, and add a separate precheck method (e.g.,
ensureExistsById or existsByIdCheck) that calls a lightweight repository lookup
(itemRepository.existsById(id) or itemRepository.findById(id) without loading
relations) and throws the same NoSuchElementException for mutation flows; then
update mutation methods like removeItem and editItem to call the new precheck
method instead of the heavy findByIdWithRelations call so edits/removals don't
incur the extra relation loading.
backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListController.java (1)

22-22: Remove stale TODO comment.

This TODO has been addressed by the changes in this PR—getAllShoppingLists (line 39) now calls service.findAllByUser(user), which returns only lists owned by or shared with the authenticated user.

♻️ Proposed fix
-// TODO: return only lists owned by the user or that are shared with him
 `@RestController`
 `@RequestMapping`("/lists")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListController.java`
at line 22, Remove the stale TODO comment in ShoppingListController.java; the
functionality is already implemented as getAllShoppingLists now calls
service.findAllByUser(user) which returns only lists owned by or shared with the
authenticated user, so delete the "// TODO: return only lists owned by the user
or that are shared with him" line to avoid confusion.
backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionService.java (3)

22-30: Consider making injected dependencies final.

The coding guidelines specify constructor injection with final fields. These fields are assigned only in the constructor and should be marked final to ensure immutability and clearly express intent.

♻️ Proposed fix
-    private final ListPermissionRepository listPermissionRepository;
+    private final ListPermissionRepository listPermissionRepository;

-    private final ShoppingListService shoppingListService;
+    private final ShoppingListService shoppingListService;

-    private final UserService userService;
+    private final UserService userService;

-    private final AuditService auditService;
+    private final AuditService auditService;

-    private final ListPermissionMapper listPermissionMapper;
+    private final ListPermissionMapper listPermissionMapper;

Note: The fields are already final. No change needed here.

On closer inspection, the fields are already final. LGTM on dependency injection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionService.java`
around lines 22 - 30, The injected dependencies in ListPermissionService
(listPermissionRepository, shoppingListService, userService, auditService,
listPermissionMapper) are already declared final, so no code changes are needed;
simply confirm the constructor performs proper constructor injection and leave
the final fields as-is.

45-48: Extract ownership verification to a helper method.

The ownership check pattern is repeated across addListPermission, findListUserPermissionById, and findAllPermissionsByListId. Extracting this to a private helper would improve maintainability and provide a single place for potential null-safety improvements.

♻️ Proposed helper method
private void verifyOwnership(ShoppingList list, User user, String message) {
    if (!list.getOwner().getId().equals(user.getId())) {
        throw new ResourceOwnershipException(message);
    }
}

Usage example:

-if (!shoppingList.getOwner().getId().equals(requester.getId())) {
-    throw new ResourceOwnershipException(
-            "User does not have permission to grant access to this list. Only the list owner can grant permissions.");
-}
+verifyOwnership(shoppingList, requester, 
+        "User does not have permission to grant access to this list. Only the list owner can grant permissions.");

Also applies to: 78-80, 103-105

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionService.java`
around lines 45 - 48, Extract the repeated ownership check in
ListPermissionService into a private helper method verifyOwnership(ShoppingList
list, User user, String message) that throws ResourceOwnershipException when
list.getOwner().getId().equals(user.getId()) is false; replace the inline checks
in addListPermission, findListUserPermissionById, and findAllPermissionsByListId
with calls to verifyOwnership(shoppingList, requester, "<same message as
original>") to centralize null-safety and error messaging (ensure you call
getOwner() and getId() safely as needed inside the helper).

100-108: Consider simplifying by removing redundant access check.

findAndVerifyAccess (line 101) verifies the user has access (owner OR shared), but lines 103-105 then reject non-owners anyway. Since only owners can proceed, the initial access check is redundant—you could call a simple findListById instead.

However, if findAndVerifyAccess also verifies the list exists and isn't deleted, keeping it might be intentional for consistency.

♻️ Potential simplification
 public List<ListPermission> findAllPermissionsByListId(Long id, User user) {
-    ShoppingList list = shoppingListService.findAndVerifyAccess(id, user);
-
-    if (!list.getOwner().getId().equals(user.getId())) {
-        throw new ResourceOwnershipException("Only the list owner can view permissions");
-    }
+    ShoppingList list = shoppingListService.findListById(id);
+    verifyOwnership(list, user, "Only the list owner can view permissions");

     return listPermissionRepository.findByShoppingListIdAndDeletedFalse(id);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionService.java`
around lines 100 - 108, The method findAllPermissionsByListId currently calls
shoppingListService.findAndVerifyAccess and then enforces owner-only access
again, which is redundant; fix by either (A) replace the call to
findAndVerifyAccess with a simpler existence check such as
shoppingListService.findListById (or equivalent findById that verifies
existence/deleted) and keep the owner-only check and the
ResourceOwnershipException, or (B) keep the call to findAndVerifyAccess and
remove the subsequent owner-only check lines (the throw) so ownership isn’t
redundantly enforced. Update findAllPermissionsByListId in ListPermissionService
and adjust the call to shoppingListService accordingly (use findListById or
remove the extra check) so behavior is consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/gemini-dispatch.yml:
- Around line 125-165: The review and invoke jobs reference missing reusable
workflow files causing runtime failures; either add the two workflows named ./
.github/workflows/gemini-review.yml and ./.github/workflows/gemini-invoke.yml
implementing the expected inputs/permissions/outputs used by the review and
invoke jobs, or update the review and invoke job 'uses' values to point to
existing reusable workflows; locate the job definitions named review and invoke
in the gemini-dispatch.yml and ensure the referenced workflow filenames and
required input/secret contracts match the created workflows (also verify triage
uses ./ .github/workflows/gemini-triage.yml if intended).

In @.github/workflows/gemini-triage.yml:
- Around line 23-25: The selected_labels output is empty because
env.SELECTED_LABELS is never set; update the triage job to extract labels from
the Gemini CLI result (e.g., steps.gemini_analysis.outputs.summary or
steps.get_labels.outputs.*) and set a step output or env var that the job
outputs can reference; specifically, add a step after run-gemini-cli that parses
steps.gemini_analysis.outputs.summary into a step output like selected_labels
(or sets an env var SELECTED_LABELS) and then change outputs.selected_labels to
reference that step output (instead of env.SELECTED_LABELS) so the downstream
label job condition can evaluate correctly.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/auth/configs/RsaKeyConfig.java`:
- Around line 26-27: The PEM loading currently calls
privateKeyResource.getInputStream().readAllBytes() (and similarly
publicKeyResource.getInputStream()) without closing the stream and converts
bytes to String using the platform default charset; change both to use
try-with-resources to open the InputStream and readAllBytes inside the try
block, and convert bytes to String with StandardCharsets.US_ASCII (e.g. new
String(bytes, StandardCharsets.US_ASCII)) before stripping "-----BEGIN/END
PRIVATE/ PUBLIC KEY-----" and whitespace so streams are closed deterministically
and parsing uses an explicit charset; update the code paths that build the key
String (the variables computed from privateKeyResource and publicKeyResource)
accordingly.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/auth/service/CustomUserDetailsService.java`:
- Around line 20-21: The current orElseThrow in CustomUserDetailsService returns
a UsernameNotFoundException that echoes the submitted email; change the thrown
message to a generic, non-PII string (e.g., "User not found" or "Invalid
credentials") so the repository lookup in userRepository.findByEmail(email) no
longer leaks the email; update the exception construction in the method (the
lambda passed to orElseThrow) to use the generic message while keeping the same
exception type UsernameNotFoundException.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ListItemController.java`:
- Around line 37-40: The controller currently ignores the path-level listId for
single-item operations; update ListItemController handlers (getListItemById,
createListItem, updateListItem, deleteListItem) to accept `@PathVariable` Long
listId and pass it to service methods (e.g., change calls to
service.findListItemById(id, user) -> service.findListItemById(listId, id, user)
or add new service overloads that accept listId) and validate that
requestDTO.listId() (for POST/PUT) matches the path listId, returning a 400/403
on mismatch; also update the service implementations to enforce the parent
listId check and add a regression test that sends a request with mismatched path
listId vs body listId to ensure the controller rejects it.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ListPermissionController.java`:
- Around line 47-49: Ensure the controller enforces the nested resource by
accepting the path variable listId in ListPermissionController methods (e.g.,
addListPermission and the other permission mutation endpoints) and passing that
listId into the service (e.g., service.addListPermission(..., listId)); inside
the controller validate that requestDTO.idList() (or the target permission's
list id for PUT/DELETE) matches the path listId and reject mismatches with an
appropriate 4xx response, and update the corresponding service signatures to
accept the explicit listId so the service can also enforce/validate parent/child
consistency.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ShoppingListService.java`:
- Around line 74-76: The editList method currently uses the generic
findAndVerifyAccess(ShoppingList.getId(), currentLoggedUser) which allows
READ-only collaborators to perform mutations; change this to a mutation-specific
authorization check (e.g., findAndVerifyWriteAccess or verifyOwner) before
performing any save or delete operations in editList and the similar blocks at
lines 92-95; update or add a new helper (referenced by name in ListItemService
usage) that asserts WRITE or OWNER permissions and call that helper instead of
the generic read check so only users with write/owner rights can rename lists or
modify items.

In `@backend/src/main/java/com/omatheusmesmo/shoppmate/user/entity/User.java`:
- Around line 15-16: In User.java replace the use of
Collections.singletonList(...) with List.of(...) where authorities are created
(e.g., in the getAuthorities / getRoles logic that currently calls
Collections.singletonList(new SimpleGrantedAuthority(role))); update that
expression to List.of(new SimpleGrantedAuthority(role)) and ensure the import
for java.util.List is present; optionally, if you intend to use role-based
checks later, prepend "ROLE_" to the authority string when constructing
SimpleGrantedAuthority (e.g., "ROLE_" + role) so future hasRole(...) checks work
as expected.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/utils/exception/GlobalExceptionHandler.java`:
- Around line 105-109: The handler handleResourceOwnership currently logs and
returns ex.getMessage(), which may expose PII; change it to avoid returning or
logging raw exception details: update the log.warn call to a generic message
like "Resource ownership violation detected" (optionally log a non-sensitive
error id) and construct the ApiError with a generic message (e.g., "Access
Denied" or "You do not have permission to access this resource") without
including ex.getMessage(); keep ResourceOwnershipException for control flow but
do not propagate its message to logs or the ResponseEntity.

In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListPermissionControllerSecurityIntegrationTest.java`:
- Around line 149-156: The test duplicates the "forbidden" scenario by posting
to ownerList with tokenUserA; instead exercise the owner-grant path: in
ListPermissionControllerSecurityIntegrationTest update the request to
authenticate as the list owner (use the owner token variable, e.g., tokenOwner
or whatever represents the owner) when posting maliciousDTO for ownerList and
change the expectation from status().isForbidden() to the successful owner
outcome (e.g., status().isOk() and appropriate json assertions) so the
owner-can-grant-on-their-own-list path is covered.

In `@frontend/src/app/shared/components/list-item/list-item.component.html`:
- Around line 113-116: The template renders the same subtitle twice; remove the
duplicated interpolation block that repeats "{{ listItem.quantity }} {{
listItem.item.unit.symbol }} x {{ listItem.unitPrice | currency }} = {{
listItem.totalPrice | currency }} | Category: {{ listItem.item.category.name }}"
so only the original single instance remains in list-item.component.html
(referencing the listItem template variable within the ListItemComponent).
Delete the redundant block (the second occurrence) and verify screen-reader
output still conveys the same info or add an ARIA attribute on the remaining
element if needed to preserve accessibility.

In `@frontend/src/app/shared/services/shopping-list.service.ts`:
- Around line 12-15: Remove the duplicated import of ListItemRequestDTO and
ListItemResponseDTO in shopping-list.service.ts by deleting the redundant import
block so only the original import remains; ensure the remaining import statement
(the one that imports ListItemRequestDTO and ListItemResponseDTO) is kept and
properly grouped with other imports to avoid TypeScript/lint errors.

---

Outside diff comments:
In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/auth/configs/RsaKeyConfig.java`:
- Around line 26-33: In RsaKeyConfig, validate and safely load the RSA keys:
before reading privateKeyResource (and likewise publicKeyResource), check
resource.exists() and resource.isReadable(), and reject or throw if the resource
URL has an untrusted remote scheme (e.g., only allow "file" or "classpath"
schemes) to prevent loading from attacker-controlled URLs; read the stream using
a try-with-resources (or explicitly close the InputStream) and construct the
String with an explicit charset (StandardCharsets.UTF_8) instead of new
String(byte[]), then proceed to Base64-decode and build the PKCS8EncodedKeySpec;
apply the same validation/stream-closing/charset fixes to the public key loading
path as well.

In `@README.md`:
- Around line 74-100: Remove the duplicate and mislabelled sections in the
README by deleting the Maven/backend snippet that is incorrectly under the
"Frontend" heading and removing the repeated npm install/start block; ensure
there is a single correctly labeled "Backend" or "Server" section containing the
Java 17 and ./mvnw spring-boot:run instructions and a single "Frontend" section
that lists Navigate to `frontend/`, npm install, npm start, and the
http://localhost:4200 URL so the README no longer contains duplicated npm
instructions or the misplaced Maven commands.

---

Nitpick comments:
In @.github/workflows/gemini-triage.yml:
- Line 62: The workflow currently references the floating tag
'google-github-actions/run-gemini-cli@v0' (with '# ratchet:exclude') which can
change; update the workflow to pin the action to a specific commit SHA or stable
tag (or add a short justification comment if you must keep the floating tag) so
CI behavior is deterministic; locate the uses line referencing
'google-github-actions/run-gemini-cli@v0' and replace '@v0' with a concrete
'@<commit-sha>' or '@<tag>' (or add an explicit rationale near the '#
ratchet:exclude' comment).

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/item/service/ItemService.java`:
- Around line 55-57: Split the responsibilities of ItemService.findById into a
rich-read method and a lightweight existence check: keep a new read method
(e.g., findByIdWithRelations or keep findById but ensure it uses
itemRepository.findByIdWithRelations(id) and throws NoSuchElementException) for
read paths that need full relations, and add a separate precheck method (e.g.,
ensureExistsById or existsByIdCheck) that calls a lightweight repository lookup
(itemRepository.existsById(id) or itemRepository.findById(id) without loading
relations) and throws the same NoSuchElementException for mutation flows; then
update mutation methods like removeItem and editItem to call the new precheck
method instead of the heavy findByIdWithRelations call so edits/removals don't
incur the extra relation loading.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListController.java`:
- Line 22: Remove the stale TODO comment in ShoppingListController.java; the
functionality is already implemented as getAllShoppingLists now calls
service.findAllByUser(user) which returns only lists owned by or shared with the
authenticated user, so delete the "// TODO: return only lists owned by the user
or that are shared with him" line to avoid confusion.

In
`@backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionService.java`:
- Around line 22-30: The injected dependencies in ListPermissionService
(listPermissionRepository, shoppingListService, userService, auditService,
listPermissionMapper) are already declared final, so no code changes are needed;
simply confirm the constructor performs proper constructor injection and leave
the final fields as-is.
- Around line 45-48: Extract the repeated ownership check in
ListPermissionService into a private helper method verifyOwnership(ShoppingList
list, User user, String message) that throws ResourceOwnershipException when
list.getOwner().getId().equals(user.getId()) is false; replace the inline checks
in addListPermission, findListUserPermissionById, and findAllPermissionsByListId
with calls to verifyOwnership(shoppingList, requester, "<same message as
original>") to centralize null-safety and error messaging (ensure you call
getOwner() and getId() safely as needed inside the helper).
- Around line 100-108: The method findAllPermissionsByListId currently calls
shoppingListService.findAndVerifyAccess and then enforces owner-only access
again, which is redundant; fix by either (A) replace the call to
findAndVerifyAccess with a simpler existence check such as
shoppingListService.findListById (or equivalent findById that verifies
existence/deleted) and keep the owner-only check and the
ResourceOwnershipException, or (B) keep the call to findAndVerifyAccess and
remove the subsequent owner-only check lines (the throw) so ownership isn’t
redundantly enforced. Update findAllPermissionsByListId in ListPermissionService
and adjust the call to shoppingListService accordingly (use findListById or
remove the extra check) so behavior is consistent.

In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/service/ItemServiceTest.java`:
- Around line 109-127: The test ItemServiceTest currently only mocks
itemRepository.findByIdWithRelations(...) so it doesn't exercise the real
repository query or the deleted = false filter; add an integration/repository
test that uses the actual ItemRepository (not Mockito) to persist an Item with
deleted=false and another with deleted=true, then call the real repository
method findByIdWithRelations(id) (or the JPA query backing it) to assert the
deleted=true item is not returned and deleted=false is returned; ensure the test
class boots Spring context or uses `@DataJpaTest` and saves entities via
ItemRepository before invoking findByIdWithRelations to verify the real query
behavior and prevent regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e817ed5f-9043-40d2-8db7-7fbd3a03d7fb

📥 Commits

Reviewing files that changed from the base of the PR and between 7d17aab and 7912013.

📒 Files selected for processing (36)
  • .github/workflows/gemini-dispatch.yml
  • .github/workflows/gemini-triage.yml
  • README.md
  • backend/src/main/java/com/omatheusmesmo/shoppmate/auth/configs/RsaKeyConfig.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/auth/service/CustomUserDetailsService.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/item/repository/ItemRepository.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/item/service/ItemService.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ListItemController.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ListPermissionController.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListController.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/dtos/ShoppingListRequestDTO.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/entity/ShoppingList.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/mapper/ListItemMapper.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/mapper/ListMapper.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/repository/ShoppingListRepository.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListItemService.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionService.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/service/ShoppingListService.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/user/entity/User.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/utils/exception/GlobalExceptionHandler.java
  • backend/src/main/java/com/omatheusmesmo/shoppmate/utils/exception/ResourceOwnershipException.java
  • backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListItemControllerSecurityIntegrationTest.java
  • backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListPermissionControllerSecurityIntegrationTest.java
  • backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListControllerSecurityIntegrationTest.java
  • backend/src/test/java/com/omatheusmesmo/shoppmate/list/service/ListItemServiceTest.java
  • backend/src/test/java/com/omatheusmesmo/shoppmate/list/service/ListPermissionServiceTest.java
  • backend/src/test/java/com/omatheusmesmo/shoppmate/service/ItemServiceTest.java
  • frontend/src/app/shared/components/list-item/list-item.component.html
  • frontend/src/app/shared/components/unit/unit.component.ts
  • frontend/src/app/shared/mocks/mock-services.ts
  • frontend/src/app/shared/services/category.service.ts
  • frontend/src/app/shared/services/item.service.ts
  • frontend/src/app/shared/services/list-permission.service.ts
  • frontend/src/app/shared/services/shopping-list.service.ts
  • frontend/src/app/shared/services/unit.service.ts
  • frontend/src/app/shared/services/user.service.ts
💤 Files with no reviewable changes (2)
  • frontend/src/app/shared/mocks/mock-services.ts
  • frontend/src/app/shared/components/unit/unit.component.ts

Comment thread .github/workflows/gemini-dispatch.yml Outdated
Comment thread .github/workflows/gemini-triage.yml Outdated
Comment thread backend/src/main/java/com/omatheusmesmo/shoppmate/auth/configs/RsaKeyConfig.java Outdated
Comment thread frontend/src/app/shared/components/list-item/list-item.component.html Outdated
Comment thread frontend/src/app/shared/services/shopping-list.service.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
backend/src/test/java/com/omatheusmesmo/shoppmate/list/service/ShoppingListServiceTest.java (2)

95-100: Rename test to match asserted behavior.

Line 98 asserts ResourceOwnershipException, but the method name says WhenNotFound. Rename for clarity to avoid future confusion in failure triage.

Proposed rename
-    void testRemoveListThrowsExceptionWhenNotFound() {
+    void testRemoveListThrowsExceptionWhenUserDoesNotOwnResource() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/list/service/ShoppingListServiceTest.java`
around lines 95 - 100, Rename the test method
testRemoveListThrowsExceptionWhenNotFound to a name that reflects the asserted
exception and scenario (e.g.,
testRemoveListThrowsResourceOwnershipExceptionWhenNotOwnedByUser) so it matches
the assertion of ResourceOwnershipException; update the method declaration to
the new name and keep the body invoking shoppingListService.removeList(999L,
testUser) with the same repository mock (findByIdAndUserId returning
Optional.empty()) to preserve behavior.

112-117: Add explicit edit ownership-denied test case.

This segment validates only the success path for editList. Add a negative ownership test to lock in the 403/ownership contract for updates too.

Proposed additional test
+    `@Test`
+    void testEditListThrowsExceptionWhenUserDoesNotOwnResource() {
+        when(shoppingListRepository.findByIdAndUserId(1L, testUser.getId()))
+                .thenReturn(Optional.empty());
+
+        assertThrows(com.omatheusmesmo.shoppmate.utils.exception.ResourceOwnershipException.class,
+                () -> shoppingListService.editList(testList, testUser));
+
+        verify(shoppingListRepository, never()).save(any(ShoppingList.class));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/list/service/ShoppingListServiceTest.java`
around lines 112 - 117, Add a new negative test alongside testEditListSuccess
that verifies ownership is enforced: create a test method (e.g.,
testEditListOwnershipDenied) that tries to call
shoppingListService.editList(testList, testUser) but mock
shoppingListRepository.findByIdAndUserId(1L, testUser.getId()) to return
Optional.empty() (or otherwise indicate the list does not belong to that user)
and assert that editList throws the expected ownership/403 exception (e.g.,
AccessDeniedException or the service's ResponseStatusException for 403) instead
of succeeding; use the same testList and testUser setup and mirror the success
test structure but assert the exception is thrown.
backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListControllerSecurityIntegrationTest.java (1)

36-61: Refactor DI to constructor injection with immutable fields.

Please switch @Autowired fields to constructor-injected final dependencies to match repository standards.

As per coding guidelines, "Dependency Injection: Always use constructor injection with final fields."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListControllerSecurityIntegrationTest.java`
around lines 36 - 61, Replace field injection in
ShoppingListControllerSecurityIntegrationTest by making the dependencies private
final fields (mockMvc, objectMapper, shoppingListRepository, userRepository,
passwordEncoder, jwtService, listMapper, shoppingListService,
listPermissionRepository) and injecting them via a constructor; remove the
`@Autowired` annotations from the fields, add a single constructor that accepts
these types and assigns them to the final fields (you can annotate the
constructor with `@Autowired` or omit the annotation if there is only one
constructor), and update any usages to reference the final fields.
backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListItemControllerSecurityIntegrationTest.java (1)

45-85: Use constructor injection with final fields in this test class.

Current field injection works, but it violates the project DI rule and makes dependencies mutable.

Refactor sketch
 class ListItemControllerSecurityIntegrationTest {
-    `@Autowired`
-    private MockMvc mockMvc;
+    private final MockMvc mockMvc;
+    private final ObjectMapper objectMapper;
+    // ...other final dependencies

+    `@Autowired`
+    ListItemControllerSecurityIntegrationTest(
+            MockMvc mockMvc,
+            ObjectMapper objectMapper,
+            ListItemRepository listItemRepository,
+            ShoppingListRepository shoppingListRepository,
+            ShoppingListService shoppingListService,
+            ListItemService listItemService,
+            UserRepository userRepository,
+            ItemRepository itemRepository,
+            CategoryRepository categoryRepository,
+            UnitRepository unitRepository,
+            PasswordEncoder passwordEncoder,
+            JwtService jwtService,
+            ListMapper listMapper,
+            ListPermissionRepository listPermissionRepository) {
+        this.mockMvc = mockMvc;
+        this.objectMapper = objectMapper;
+        // ...assign all fields
+    }

As per coding guidelines, "Dependency Injection: Always use constructor injection with final fields."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListItemControllerSecurityIntegrationTest.java`
around lines 45 - 85, The test class ListItemControllerSecurityIntegrationTest
currently uses mutable field injection for many beans (mockMvc, objectMapper,
listItemRepository, shoppingListRepository, shoppingListService,
listItemService, userRepository, itemRepository, categoryRepository,
unitRepository, passwordEncoder, jwtService, listMapper,
listPermissionRepository); replace these with constructor injection by declaring
each as private final, adding a single `@Autowired` constructor that accepts all
those types as parameters and assigns them to the final fields, and remove the
individual `@Autowired` annotations from the fields so dependencies are immutable
and comply with the project's DI rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListItemControllerSecurityIntegrationTest.java`:
- Around line 30-43: Add the explicit test profile annotation to the test class:
annotate ListItemControllerSecurityIntegrationTest with `@ActiveProfiles`("test")
placed above the class declaration and keep the existing import for
ActiveProfiles; remove any truly unused imports in the file (or other test
classes) to avoid warnings. Ensure the annotation is applied to the class named
ListItemControllerSecurityIntegrationTest so the test uses
application-test.properties and follows the suite convention.

In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListControllerSecurityIntegrationTest.java`:
- Around line 23-34: The test class
ShoppingListControllerSecurityIntegrationTest imports ActiveProfiles but doesn't
apply it, so add the `@ActiveProfiles`("test") annotation to the
ShoppingListControllerSecurityIntegrationTest class declaration to ensure the
test profile (application-test.properties, H2 config, test logging) is loaded
when running integration tests; locate the class
ShoppingListControllerSecurityIntegrationTest and annotate it with
`@ActiveProfiles`("test").

---

Nitpick comments:
In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListItemControllerSecurityIntegrationTest.java`:
- Around line 45-85: The test class ListItemControllerSecurityIntegrationTest
currently uses mutable field injection for many beans (mockMvc, objectMapper,
listItemRepository, shoppingListRepository, shoppingListService,
listItemService, userRepository, itemRepository, categoryRepository,
unitRepository, passwordEncoder, jwtService, listMapper,
listPermissionRepository); replace these with constructor injection by declaring
each as private final, adding a single `@Autowired` constructor that accepts all
those types as parameters and assigns them to the final fields, and remove the
individual `@Autowired` annotations from the fields so dependencies are immutable
and comply with the project's DI rule.

In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListControllerSecurityIntegrationTest.java`:
- Around line 36-61: Replace field injection in
ShoppingListControllerSecurityIntegrationTest by making the dependencies private
final fields (mockMvc, objectMapper, shoppingListRepository, userRepository,
passwordEncoder, jwtService, listMapper, shoppingListService,
listPermissionRepository) and injecting them via a constructor; remove the
`@Autowired` annotations from the fields, add a single constructor that accepts
these types and assigns them to the final fields (you can annotate the
constructor with `@Autowired` or omit the annotation if there is only one
constructor), and update any usages to reference the final fields.

In
`@backend/src/test/java/com/omatheusmesmo/shoppmate/list/service/ShoppingListServiceTest.java`:
- Around line 95-100: Rename the test method
testRemoveListThrowsExceptionWhenNotFound to a name that reflects the asserted
exception and scenario (e.g.,
testRemoveListThrowsResourceOwnershipExceptionWhenNotOwnedByUser) so it matches
the assertion of ResourceOwnershipException; update the method declaration to
the new name and keep the body invoking shoppingListService.removeList(999L,
testUser) with the same repository mock (findByIdAndUserId returning
Optional.empty()) to preserve behavior.
- Around line 112-117: Add a new negative test alongside testEditListSuccess
that verifies ownership is enforced: create a test method (e.g.,
testEditListOwnershipDenied) that tries to call
shoppingListService.editList(testList, testUser) but mock
shoppingListRepository.findByIdAndUserId(1L, testUser.getId()) to return
Optional.empty() (or otherwise indicate the list does not belong to that user)
and assert that editList throws the expected ownership/403 exception (e.g.,
AccessDeniedException or the service's ResponseStatusException for 403) instead
of succeeding; use the same testList and testUser setup and mirror the success
test structure but assert the exception is thrown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 05671a8e-7055-47e7-9ddc-85c37b9c7422

📥 Commits

Reviewing files that changed from the base of the PR and between 7912013 and 2b1f1b7.

📒 Files selected for processing (7)
  • backend/src/main/java/com/omatheusmesmo/shoppmate/list/entity/ListPermission.java
  • backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListItemControllerSecurityIntegrationTest.java
  • backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListPermissionControllerSecurityIntegrationTest.java
  • backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ShoppingListControllerSecurityIntegrationTest.java
  • backend/src/test/java/com/omatheusmesmo/shoppmate/list/service/ShoppingListServiceTest.java
  • frontend/src/app/shared/components/unit/unit.component.ts
  • frontend/src/app/shared/services/shopping-list.service.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/src/test/java/com/omatheusmesmo/shoppmate/list/controller/ListPermissionControllerSecurityIntegrationTest.java
  • frontend/src/app/shared/services/shopping-list.service.ts
  • frontend/src/app/shared/components/unit/unit.component.ts

@omatheusmesmo
Copy link
Copy Markdown
Owner

Hello @felipemelozx ,you can just ignore coderabbit if the file isn't related to your feature.

ASAP I'll take a look at your pr.

@felipemelozx felipemelozx force-pushed the fix/id-spoofing-vulnerability branch from 2c14753 to 3d5a061 Compare April 4, 2026 16:00
- ListItemServiceTest: use verifyOwnership instead of findAndVerifyAccess
- ShoppingListServiceTest: fix mock for removeList test
- ItemServiceTest: use findByIdWithRelations instead of findById
- Integration tests: fix expected status codes
Copy link
Copy Markdown
Owner

@omatheusmesmo omatheusmesmo left a comment

Choose a reason for hiding this comment

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

@felipemelozx Thanks for working on this — the IDOR fix looks solid, all 127 tests pass and the ownership verification logic is correct. ✅

Before merging, could you please clean up the branch:

  • Resolve merge conflicts with the latest develop
  • Remove IDE config files — backend/.settings/ and backend/.factorypath should not be committed. Please add them to .gitignore
  • Restore README.md — it was deleted but is not related to this fix

Once that is done, I will merge this right away. 🚀

@omatheusmesmo omatheusmesmo changed the base branch from develop to main April 18, 2026 15:54
@omatheusmesmo
Copy link
Copy Markdown
Owner

Hey @felipemelozx ,

Almost there, just check the latest backend CI errors.

Thanks,

@felipemelozx
Copy link
Copy Markdown
Contributor Author

Problem

Controller unit tests were failing because @WithMockUser creates Spring Security's default User object, but our controllers expect a custom User entity via @AuthenticationPrincipal.

Solution

Created a custom annotation following Spring Security best practices to properly inject our custom User entity in tests.

Changes

New Files

  • @WithMockCustomUser - Custom annotation for test authentication
  • WithMockCustomUserSecurityContextFactory - Creates SecurityContext with our User entity

Updated Tests

  • ListPermissionControllerTest.java
  • ListItemControllerTest.java
  • ShoppingListControllerTest.java

@omatheusmesmo omatheusmesmo merged commit bf5bb4b into omatheusmesmo:main Apr 18, 2026
6 checks passed
@omatheusmesmo
Copy link
Copy Markdown
Owner

Great job @felipemelozx , thanks for your patience!

@felipemelozx felipemelozx deleted the fix/id-spoofing-vulnerability branch April 19, 2026 12:40
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.

Security: Extract user identity from Security Context

2 participants