Skip to content

Fix/role based endpoint rules#216

Merged
annikaholmqvist94 merged 19 commits into
mainfrom
fix/role-based-endpoint-rules
Apr 21, 2026
Merged

Fix/role based endpoint rules#216
annikaholmqvist94 merged 19 commits into
mainfrom
fix/role-based-endpoint-rules

Conversation

@annikaholmqvist94

@annikaholmqvist94 annikaholmqvist94 commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Sammanställning vad vi exakt ändrat för krav per roll

OWNER strikt tillstramat

  • Kan inte längre skapa medicinska ärenden (POST /api/medical-records) blockeras både av URL-regel (hasAnyRole("VET","ADMIN")) och av MedicalRecordPolicy.canCreate (kastar ForbiddenException).
  • Kan inte längre uppdatera medicinska ärenden (PUT /api/medical-records/{id}) blockeras i både URL-lager och MedicalRecordPolicy.canUpdate.
  • Kan inte ladda upp bilagor (POST /api/attachments/**) kaskadeffekt via AttachmentPolicy.canUpload som kallar canUpdate. Dessutom URL-lagret.
  • Kan inte radera bilagor (DELETE /api/attachments/**) samma kaskad + URL-lager.
  • Oförändrat OWNER kan fortsatt:
    • Se egna ärenden (GET /api/medical-records/{id}, /my-records, /owner/{egenId}, /pet/{petId}).
    • Se bilagor på egna ärenden (GET /api/attachments/record/**, /{id}/download).
    • Kommentera, redigera och radera egna kommentarer på egna ärenden.
    • Se aktivitetsloggar för egna ärenden.
    • Skapa, se, uppdatera och radera egna djur (/api/pets/**).

VET rollen tydligare bekräftad i URL-lagret

  • URL-lagret kräver nu ROLE_VET eller ROLE_ADMIN för att skapa/uppdatera ärenden, stänga, tilldela, byta status, hämta klinikvyer, ladda upp/radera bilagor. Tidigare filtrerades detta bara i
    policy.
  • Policy-lagret oförändrat för VET: fortsatt klinik-begränsning (bara egna klinikens ärenden).
  • Praktiskt innebär det: en VET som manipulerar sin JWT kan inte längre hoppa till en helt annan roll-only endpoint de två lagren måste falla tillsammans.

ADMIN oförändrat

  • Har fortsatt full åtkomst. URL-reglerna vi lade till tillåter ROLE_ADMIN överallt där VET tillåts, och /api/users/**, POST /api/vets, POST/PUT/DELETE /api/clinics är ADMIN-only (tidigare bara
    authenticated(), respektive @PreAuthorize/AdminPolicy).
  • @PreAuthorize("hasRole('ADMIN')") på UserController.searchByEmail är oförändrat och fungerar parallellt med URL-regeln.

Publika endpoints oförändrat

  • POST/GET /api/auth/** (login, registrera nytt konto) fortsatt permitAll().
  • GET /api/clinics och GET /api/clinics/** fortsatt permitAll().

Tester som berördes

  • Ny: MedicalRecordPolicyTest med 21 tester som verifierar OWNER-blockering på canCreate, canUpdate, canClose, canUpdateStatus, canAssignVet, canViewClinic, samt positiva fall för VET (egen
    klinik) och ADMIN.

Summary by CodeRabbit

  • New Features

    • Comments now include a type (owner message or clinical note) and are saved with that classification.
  • Improvements

    • Pet owners no longer see veterinary clinical notes; vets and admins can.
    • Comment listings and counts respect per-comment visibility.
    • Comment creation auto-labels comments by creator role.
    • Attachment deletion now enforces uploader ownership.
    • Endpoint access tightened with clearer role-based restrictions.
  • Tests

    • New and updated tests cover comment visibility, policies, and related behaviors.

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Security authorization rules were changed to an explicit method+path role matrix; comment entities gained a typed CommentType and visibility rules; comment create/get/count flows were updated to set/filter by type; attachment delete policy was adjusted; schema and tests were added/updated to cover these behaviors.

Changes

Cohort / File(s) Summary
Security Authorization
src/main/java/org/example/vet1177/security/SecurityConfig.java, src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java
Replaced permissive path rules with explicit method+path role checks (ADMIN-only endpoints, VET/ADMIN medical-record actions). Tests updated to populate authorities and adjust admin-policy verification.
Comment Type System
src/main/java/org/example/vet1177/entities/CommentType.java, src/main/java/org/example/vet1177/entities/Comment.java, src/main/java/org/example/vet1177/dto/response/comment/CommentResponse.java
Added CommentType enum; new type field on Comment (DB-mapped, default OWNER_MESSAGE) and included type in CommentResponse.
Comment Visibility & Service
src/main/java/org/example/vet1177/policy/CommentPolicy.java, src/main/java/org/example/vet1177/services/CommentService.java
Added isVisibleTo(User, Comment) to enforce OWNER cannot see VET_CLINICAL_NOTE; CommentService.create sets type by role; getByRecord/countByRecord now filter by visibility instead of using raw repo counts.
Attachment Authorization
src/main/java/org/example/vet1177/policy/AttachmentPolicy.java
Merged VET and OWNER cases in canDelete to require uploaded-by ownership check; removed previous OWNER-always-deny branch.
Database Schema
src/main/resources/schema.sql
Added comment_type VARCHAR(32) NOT NULL DEFAULT 'OWNER_MESSAGE' column and an ALTER TABLE migration step.
Policy & Service Tests
src/test/java/org/example/vet1177/policy/MedicalRecordPolicyTest.java, src/test/java/org/example/vet1177/policy/CommentPolicyTest.java, src/test/java/org/example/vet1177/services/CommentServiceTest.java
Added MedicalRecordPolicyTest; expanded CommentPolicyTest for visibility; updated CommentServiceTest to assert type assignment and visibility-based counting/filtering.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CommentService
    participant CommentPolicy
    participant Database

    Note over Client,Database: Create comment flow
    Client->>CommentService: create(commentDto, currentUser)
    CommentService->>CommentService: determine type from currentUser.role
    CommentService->>Database: save Comment(with type)
    Database-->>CommentService: saved Comment

    Note over Client,Database: Read comments with visibility filtering
    Client->>CommentService: getByRecord(recordId, currentUser)
    CommentService->>Database: findByMedicalRecordIdOrderByCreatedAtAsc(recordId)
    Database-->>CommentService: List<Comment>
    loop each comment
        CommentService->>CommentPolicy: isVisibleTo(currentUser, comment)
        alt visible
            CommentPolicy-->>CommentService: true
        else filtered
            CommentPolicy-->>CommentService: false
        end
    end
    CommentService-->>Client: filtered List<Comment>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Issue #210: Modifies the same SecurityConfig.authorizeHttpRequests() block to add explicit role-based endpoint rules (admin and VET/ADMIN restrictions).

Possibly related PRs

  • PR #140: Overlaps changes to CommentService (create/getByRecord/countByRecord) and comment visibility logic.
  • PR #106: Touches AttachmentPolicy.canDelete logic that this PR also refactors.
  • PR #123: Adds/tests Comment entity and DTOs related to the new CommentType and response changes.

Suggested reviewers

  • johanbriger
  • lindaeskilsson
  • TatjanaTrajkovic

Poem

🐰
I hop and sort each comment kind,
Owner notes I leave behind,
Vets see clinical lines I write,
Admins may view both day and night,
Hoppy commits — all set and tight 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix/role based endpoint rules' clearly and concisely describes the main change: implementing role-based authorization rules at the endpoint/URL layer for different user roles (OWNER, VET, ADMIN).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/role-based-endpoint-rules

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/main/java/org/example/vet1177/policy/MedicalRecordPolicy.java (1)

39-67: ⚠️ Potential issue | 🟠 Major

Authorize the role before returning state-specific business errors.

Lines 40, 55, and 94 run final-state checks before the OWNER denial branches, so an OWNER acting on a closed record gets BusinessRuleException instead of the intended ForbiddenException. Move the business-rule checks after role/scope authorization so unauthorized users do not receive record-state information.

🛡️ Proposed fix
     public void canUpdate(User user, MedicalRecord record) {
-        if (record.getStatus().isFinal())
-            throw new BusinessRuleException("Stängda ärenden kan inte uppdateras");
-
         switch (user.getRole()) {
             case OWNER ->
                     throw new ForbiddenException("Ägare får inte uppdatera ärenden");
             case VET -> {
                 if (!sameClinic(user, record))
                     throw new ForbiddenException("Du har inte tillgång till ärenden på en annan klinik");
             }
             case ADMIN -> {}
         }
+
+        if (record.getStatus().isFinal())
+            throw new BusinessRuleException("Stängda ärenden kan inte uppdateras");
     }
 
     public void canUpdateStatus(User user, MedicalRecord record, RecordStatus newStatus) {
-        if (record.getStatus().isFinal())
-            throw new BusinessRuleException("Stängda ärenden kan inte uppdateras");
-
         switch (user.getRole()) {
             case OWNER ->
                     throw new ForbiddenException("Ägare får inte ändra status på ärenden");
             case VET -> {
                 if (!sameClinic(user, record))
                     throw new ForbiddenException("Du har inte tillgång till ärenden på en annan klinik");
             }
             case ADMIN -> {}
         }
+
+        if (record.getStatus().isFinal())
+            throw new BusinessRuleException("Stängda ärenden kan inte uppdateras");
     }
 
     public void canClose(User user, MedicalRecord record) {
-        if (record.getStatus().isFinal())
-            throw new BusinessRuleException("Ärendet är redan stängt");
-
         switch (user.getRole()) {
             case OWNER ->
                     throw new ForbiddenException("Ägare får inte stänga ärenden");
             case VET -> {
                 if (!sameClinic(user, record))
                     throw new ForbiddenException("Du har inte tillgång till ärenden på en annan klinik");
             }
             case ADMIN -> {}
         }
+
+        if (record.getStatus().isFinal())
+            throw new BusinessRuleException("Ärendet är redan stängt");
     }

Also applies to: 93-105

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

In `@src/main/java/org/example/vet1177/policy/MedicalRecordPolicy.java` around
lines 39 - 67, The final-state check (record.getStatus().isFinal()) should not
run before authorization because OWNERs (and unauthorized users) would see
BusinessRuleException instead of ForbiddenException; update canUpdate and
canUpdateStatus so you first enforce role/scope checks (handle OWNER -> throw
ForbiddenException, VET -> verify sameClinic(user, record) and throw
ForbiddenException if not, ADMIN allowed) and only after the caller is
authorized perform the final-state check and throw BusinessRuleException; refer
to the methods canUpdate and canUpdateStatus and symbols OWNER, VET, ADMIN,
sameClinic(...) when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/main/java/org/example/vet1177/policy/MedicalRecordPolicy.java`:
- Around line 39-67: The final-state check (record.getStatus().isFinal()) should
not run before authorization because OWNERs (and unauthorized users) would see
BusinessRuleException instead of ForbiddenException; update canUpdate and
canUpdateStatus so you first enforce role/scope checks (handle OWNER -> throw
ForbiddenException, VET -> verify sameClinic(user, record) and throw
ForbiddenException if not, ADMIN allowed) and only after the caller is
authorized perform the final-state check and throw BusinessRuleException; refer
to the methods canUpdate and canUpdateStatus and symbols OWNER, VET, ADMIN,
sameClinic(...) when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6c7f37e-ecdc-4a3b-b5f8-0a04ea247f0d

📥 Commits

Reviewing files that changed from the base of the PR and between caec9c7 and bb87001.

📒 Files selected for processing (4)
  • src/main/java/org/example/vet1177/policy/MedicalRecordPolicy.java
  • src/main/java/org/example/vet1177/security/SecurityConfig.java
  • src/test/java/org/example/vet1177/integration/vet/VetIntegrationTest.java
  • src/test/java/org/example/vet1177/policy/MedicalRecordPolicyTest.java

@annikaholmqvist94

Copy link
Copy Markdown
Contributor Author

Roller och vad de får

  • OWNER: skapa/uppdatera egna ärenden, ladda upp bilagor, ta bort egna bilagor, se egna ärenden och bilagor, skriva och läsa kommentarer av typ OWNER_MESSAGE.
  • VET: allt inom egen klinik — skapa/uppdatera ärenden, stänga, tilldela vet, byta status, hantera bilagor, skriva VET_CLINICAL_NOTE.
  • ADMIN: allt.

URL-regler (grovmaskigt rollfilter i SecurityConfig)

  • permitAll: /api/auth/, GET /api/clinics, GET /api/clinics/
  • ADMIN: /api/users/**, POST /api/vets, POST/PUT/DELETE /api/clinics
  • VET/ADMIN: PUT /api/medical-records/*/close|assign-vet|status, GET /api/medical-records/clinic/**
  • authenticated() (policy finjusterar): allt annat — inklusive POST /api/medical-records, PUT /api/medical-records/{id}, POST/GET/DELETE /api/attachments/, /api/comments/, /api/pets/,
    /api/activity-logs/

403 = fel roll. 401 = ogiltig/utgången token. Er nuvarande response-interceptor hanterar bara 401 — lägg gärna till en 403-branch som visar en toast ("Du saknar behörighet").

Nya kommentartyper (viktigt för CaseDetail.jsx)

CommentResponse innehåller nu type: "OWNER_MESSAGE" | "VET_CLINICAL_NOTE".

  • OWNER skickar kommentar → servern sätter OWNER_MESSAGE.
  • VET/ADMIN skickar kommentar → servern sätter VET_CLINICAL_NOTE.
  • OWNER ser aldrig VET_CLINICAL_NOTE (filtreras i getByRecord + countByRecord).

Frontend-saker att åtgärda mot regler

  1. DELETE /api/medical-records/{id} finns inte. medicalRecordService.deleteRecord + rollback-logik i CreateCase.jsx returnerar alltid 404/405. Ta bort rollbacken — bilaga är en separat
    transaktion, ärendet är inte korrupt om en bilaga misslyckas.
  2. App.jsx fetchInitialData kallar getMyRecords() oavsett roll. Endpointen kastar 403 för allt utom OWNER → spam i console. Guarda med if (currentUser.role === 'ROLE_OWNER').
  3. GET /api/pets (nya endpoint:en i denna PR) returnerar 403 för VET eftersom PetPolicy.canViewOwnerPets kräver ADMIN eller OWNER-self. VET behöver antingen ett separat endpoint (GET
    /api/pets/clinic/{id} — finns inte än) eller så hoppar frontend över anropet för VET.
  4. Sidomenyn i Layout.jsx visar "Mina djur / Mina ärenden" för både OWNER och VET. Texten är OWNER-språk och endpoints går 403 för VET. Dela upp menyn per roll.
  5. VetDashboard.jsx är tom. Antingen implementera eller dölj menyvalet + vet-dashboard-routen.
  6. Register.jsx skickar role: 'OWNER' — backend ignorerar fältet helt (AuthService.register hårdkodar Role.OWNER). Ingen säkerhetsrisk, men städa bort för tydlighet.
  7. CaseDetail "Ladda upp" / "Radera bilaga": OWNER kan nu ladda upp och radera egna uppladdningar. Radera-knappen bör ändå döljas för bilagor som andra har laddat upp (kolla uploadedBy ===
    currentUserId) — annars får OWNER 403 om de klickar på en bilaga VET laddat upp.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/main/java/org/example/vet1177/policy/AttachmentPolicy.java (1)

54-64: ⚠️ Potential issue | 🟠 Major

Keep OWNER deletion denial explicit.

Line 60 reintroduces OWNER into the allowed self-delete branch, which conflicts with the PR objective that OWNER cannot delete attachments. If MedicalRecordPolicy.canUpdate currently blocks OWNER, this branch is unreachable; if that policy changes later, this becomes a privilege regression. Since AttachmentController.deleteAttachment() delegates straight to policy and the provided SecurityConfig snippet only requires authentication, AttachmentPolicy.canDelete should state the OWNER rule directly.

🔒 Proposed authorization fix
         switch (user.getRole()) {
             case ADMIN -> {
                 // Admin har fulla rättigheter att radera.
             }
-            case VET, OWNER -> {
-                // VET och OWNER får endast radera bilagor de själva har laddat upp.
+            case VET -> {
+                // VET får endast radera bilagor de själva har laddat upp.
                 if (attachment.getUploadedBy() == null ||
                         !attachment.getUploadedBy().getId().equals(user.getId())) {
                     throw new ForbiddenException("Du kan endast radera bilagor du själv har laddat upp.");
                 }
             }
+            case OWNER -> throw new ForbiddenException("Djurägare får inte radera bilagor.");
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/vet1177/policy/AttachmentPolicy.java` around lines
54 - 64, AttachmentPolicy.canDelete currently groups OWNER into the self-delete
branch which reintroduces deletion rights for owners; change the switch in
AttachmentPolicy.canDelete so that OWNER is not included in the allowed branch
(use case VET -> { ... } only) and add an explicit case OWNER -> throw new
ForbiddenException("Du kan inte radera bilagor som ägs av posten" or the
project-standard owner-deny message) so OWNER denial is explicit; keep the
existing call to MedicalRecordPolicy.canUpdate(user,
attachment.getMedicalRecord()) and ensure AttachmentController.deleteAttachment
and SecurityConfig behavior remain unchanged.
🧹 Nitpick comments (4)
src/test/java/org/example/vet1177/policy/MedicalRecordPolicyTest.java (1)

82-84: Minor: duplicated otherOwner setup and inconsistent message assertions.

Three tests (Line 82-84, 118-120, 161-163) re-construct the same otherOwner. Lift it into setUp() to shrink noise. Additionally, the canView/canClose/canUpdateStatus/canAssignVet/canViewClinic tests only assert the exception type, whereas the canCreate/canUpdate tests also assert the Swedish message. Consider asserting hasMessage(...) consistently so regressions in user-facing error strings are caught uniformly.

Also applies to: 118-120, 161-163

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

In `@src/test/java/org/example/vet1177/policy/MedicalRecordPolicyTest.java` around
lines 82 - 84, Multiple tests recreate the same User instance otherOwner; move
that construction (new User(...) and setPrivateField(otherOwner, "id", ...))
into the test class setUp() method and reuse the field otherOwner in tests
(e.g., canCreate_ownerOfOtherPet_shouldThrowForbidden,
canView_ownerOfOtherPet_shouldThrowForbidden,
canUpdate_ownerOfOtherPet_shouldThrowForbidden) to reduce duplication; also make
exception assertions consistent by adding hasMessage("Du saknar åtkomst") (or
the exact Swedish message used in canCreate/canUpdate) to the existing
assertThrows/exception checks in
canView/canClose/canUpdateStatus/canAssignVet/canViewClinic tests so all
forbidden tests verify both exception type and message.
src/main/resources/schema.sql (1)

66-71: Constrain comment_type to valid enum values.

The column is non-null, but direct SQL/imports can still write values that do not map to CommentType, causing read-time failures in JPA. Add a check constraint alongside the default.

Suggested schema hardening
-                                       comment_type    VARCHAR(32) NOT NULL DEFAULT 'OWNER_MESSAGE',
+                                       comment_type    VARCHAR(32) NOT NULL DEFAULT 'OWNER_MESSAGE'
+                                           CHECK (comment_type IN ('OWNER_MESSAGE', 'VET_CLINICAL_NOTE')),

For existing deployments:

DO $$
BEGIN
    ALTER TABLE comment
        ADD CONSTRAINT chk_comment_comment_type
        CHECK (comment_type IN ('OWNER_MESSAGE', 'VET_CLINICAL_NOTE'));
EXCEPTION
    WHEN duplicate_object THEN NULL;
END $$;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/schema.sql` around lines 66 - 71, Add a CHECK constraint
to the comment table to restrict comment_type to valid enum values (e.g.,
'OWNER_MESSAGE', 'VET_CLINICAL_NOTE') to prevent invalid values being written by
direct SQL/imports; update the migration/schema by adding a constraint named
e.g. chk_comment_comment_type on table comment that enforces comment_type IN
(…), and for existing deployments wrap the addition in a safe block (DO $$ ...
EXCEPTION WHEN duplicate_object THEN NULL; END $$) or use an IF NOT EXISTS
pattern so the ALTER TABLE doesn't fail if the constraint already exists.
src/main/java/org/example/vet1177/services/CommentService.java (1)

125-130: Avoid loading every comment just to count visible comments.

countByRecord(...) now materializes the full ordered comment list and entity graph before counting. With the current visibility rule, this can stay as a DB count.

Proposed direction
-        return commentRepository.findByMedicalRecordIdOrderByCreatedAtAsc(recordId)
-                .stream()
-                .filter(c -> commentPolicy.isVisibleTo(currentUser, c))
-                .count();
+        if (currentUser.getRole() == Role.OWNER) {
+            return commentRepository.countByMedicalRecordIdAndType(recordId, CommentType.OWNER_MESSAGE);
+        }
+        return commentRepository.countByMedicalRecordId(recordId);

Add the repository method:

long countByMedicalRecordIdAndType(UUID recordId, CommentType type);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/vet1177/services/CommentService.java` around lines
125 - 130, The current code materializes all comments via
commentRepository.findByMedicalRecordIdOrderByCreatedAtAsc(...) then filters
with commentPolicy.isVisibleTo(...) to get a count; instead add a repository
method long countByMedicalRecordIdAndType(UUID recordId, CommentType type) and
use it so counting happens in the DB. Keep the existing
commentPolicy.canView(currentUser, record) call, determine the CommentType that
corresponds to the visibility rule used by commentPolicy.isVisibleTo(...) (the
same type used in the filter), and replace the stream/count block with a single
call to commentRepository.countByMedicalRecordIdAndType(recordId, thatType).
src/main/java/org/example/vet1177/entities/Comment.java (1)

26-28: Enforce the non-null type invariant in the setter.

The DB/JPA mapping says this is non-null, but setType(null) can still create an invalid in-memory entity before flush. Guard it at the entity boundary.

Proposed fix
 import jakarta.persistence.*;
 import java.time.Instant;
+import java.util.Objects;
 import java.util.UUID;
     public CommentType getType() { return type; }
-    public void setType(CommentType type) { this.type = type; }
+    public void setType(CommentType type) { this.type = Objects.requireNonNull(type, "type"); }

Also applies to: 60-61

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

In `@src/main/java/org/example/vet1177/entities/Comment.java` around lines 26 -
28, The entity's non-null invariant for the CommentType field is only enforced
by JPA annotations but not in-memory; update the Comment class's
setType(CommentType type) to validate input (e.g., Objects.requireNonNull or an
explicit null check) and throw an IllegalArgumentException with a clear message
if null is passed, ensuring the in-memory entity cannot be put into an invalid
state; apply the same null-guard pattern to any other enum/setter methods in
this class that are annotated non-null (e.g., other setters for non-null enum
fields).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/example/vet1177/policy/CommentPolicy.java`:
- Around line 70-72: The visibility check in CommentPolicy.isVisibleTo currently
lets OWNERs see any non-VET_CLINICAL_NOTE (including null/unrecognized types);
change the logic to "fail closed" for owners by first checking if user.getRole()
== Role.OWNER and, if so, only return true when comment.getType() ==
CommentType.OWNER_MESSAGE; otherwise preserve the non-owner behavior. Update
isVisibleTo in CommentPolicy to explicitly handle Role.OWNER first and compare
comment.getType() against CommentType.OWNER_MESSAGE.

In `@src/test/java/org/example/vet1177/policy/MedicalRecordPolicyTest.java`:
- Around line 76-149: Tests in MedicalRecordPolicyTest contradict the PR
objective that OWNERs must be blocked by the policy; update the two
owner-positive tests (canCreate_ownerOfPet_shouldNotThrow and
canUpdate_ownerOfRecord_shouldNotThrow) so they assert a ForbiddenException is
thrown when calling MedicalRecordPolicy.canCreate(owner, pet, clinic) and
MedicalRecordPolicy.canUpdate(owner, openRecord) respectively, using the same
localized forbidden messages the policy produces (e.g., the messages used
elsewhere for owner-forbidden cases) or the exact strings from
MedicalRecordPolicy; if you prefer the other route, instead update
MedicalRecordPolicy.canCreate / MedicalRecordPolicy.canUpdate to universally
throw ForbiddenException for Role.OWNER before the owner-equality checks so the
existing tests remain valid—choose one consistent approach and make the tests
and policy match (reference: MedicalRecordPolicy.canCreate,
MedicalRecordPolicy.canUpdate, and the test methods named above).

---

Outside diff comments:
In `@src/main/java/org/example/vet1177/policy/AttachmentPolicy.java`:
- Around line 54-64: AttachmentPolicy.canDelete currently groups OWNER into the
self-delete branch which reintroduces deletion rights for owners; change the
switch in AttachmentPolicy.canDelete so that OWNER is not included in the
allowed branch (use case VET -> { ... } only) and add an explicit case OWNER ->
throw new ForbiddenException("Du kan inte radera bilagor som ägs av posten" or
the project-standard owner-deny message) so OWNER denial is explicit; keep the
existing call to MedicalRecordPolicy.canUpdate(user,
attachment.getMedicalRecord()) and ensure AttachmentController.deleteAttachment
and SecurityConfig behavior remain unchanged.

---

Nitpick comments:
In `@src/main/java/org/example/vet1177/entities/Comment.java`:
- Around line 26-28: The entity's non-null invariant for the CommentType field
is only enforced by JPA annotations but not in-memory; update the Comment
class's setType(CommentType type) to validate input (e.g.,
Objects.requireNonNull or an explicit null check) and throw an
IllegalArgumentException with a clear message if null is passed, ensuring the
in-memory entity cannot be put into an invalid state; apply the same null-guard
pattern to any other enum/setter methods in this class that are annotated
non-null (e.g., other setters for non-null enum fields).

In `@src/main/java/org/example/vet1177/services/CommentService.java`:
- Around line 125-130: The current code materializes all comments via
commentRepository.findByMedicalRecordIdOrderByCreatedAtAsc(...) then filters
with commentPolicy.isVisibleTo(...) to get a count; instead add a repository
method long countByMedicalRecordIdAndType(UUID recordId, CommentType type) and
use it so counting happens in the DB. Keep the existing
commentPolicy.canView(currentUser, record) call, determine the CommentType that
corresponds to the visibility rule used by commentPolicy.isVisibleTo(...) (the
same type used in the filter), and replace the stream/count block with a single
call to commentRepository.countByMedicalRecordIdAndType(recordId, thatType).

In `@src/main/resources/schema.sql`:
- Around line 66-71: Add a CHECK constraint to the comment table to restrict
comment_type to valid enum values (e.g., 'OWNER_MESSAGE', 'VET_CLINICAL_NOTE')
to prevent invalid values being written by direct SQL/imports; update the
migration/schema by adding a constraint named e.g. chk_comment_comment_type on
table comment that enforces comment_type IN (…), and for existing deployments
wrap the addition in a safe block (DO $$ ... EXCEPTION WHEN duplicate_object
THEN NULL; END $$) or use an IF NOT EXISTS pattern so the ALTER TABLE doesn't
fail if the constraint already exists.

In `@src/test/java/org/example/vet1177/policy/MedicalRecordPolicyTest.java`:
- Around line 82-84: Multiple tests recreate the same User instance otherOwner;
move that construction (new User(...) and setPrivateField(otherOwner, "id",
...)) into the test class setUp() method and reuse the field otherOwner in tests
(e.g., canCreate_ownerOfOtherPet_shouldThrowForbidden,
canView_ownerOfOtherPet_shouldThrowForbidden,
canUpdate_ownerOfOtherPet_shouldThrowForbidden) to reduce duplication; also make
exception assertions consistent by adding hasMessage("Du saknar åtkomst") (or
the exact Swedish message used in canCreate/canUpdate) to the existing
assertThrows/exception checks in
canView/canClose/canUpdateStatus/canAssignVet/canViewClinic tests so all
forbidden tests verify both exception type and message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 13782a56-1d09-4138-b136-aab0e207ad12

📥 Commits

Reviewing files that changed from the base of the PR and between bb87001 and 005f95b.

📒 Files selected for processing (11)
  • src/main/java/org/example/vet1177/dto/response/comment/CommentResponse.java
  • src/main/java/org/example/vet1177/entities/Comment.java
  • src/main/java/org/example/vet1177/entities/CommentType.java
  • src/main/java/org/example/vet1177/policy/AttachmentPolicy.java
  • src/main/java/org/example/vet1177/policy/CommentPolicy.java
  • src/main/java/org/example/vet1177/security/SecurityConfig.java
  • src/main/java/org/example/vet1177/services/CommentService.java
  • src/main/resources/schema.sql
  • src/test/java/org/example/vet1177/policy/CommentPolicyTest.java
  • src/test/java/org/example/vet1177/policy/MedicalRecordPolicyTest.java
  • src/test/java/org/example/vet1177/services/CommentServiceTest.java
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/org/example/vet1177/entities/CommentType.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/example/vet1177/security/SecurityConfig.java

Comment thread src/main/java/org/example/vet1177/policy/CommentPolicy.java Outdated
Comment on lines +76 to +149
@Test
void canCreate_ownerOfPet_shouldNotThrow() {
assertThatNoException().isThrownBy(() -> policy.canCreate(owner, pet, clinic));
}

@Test
void canCreate_ownerOfOtherPet_shouldThrowForbidden() throws Exception {
User otherOwner = new User("Bertil Annan", "b@mail.se", "hash", Role.OWNER);
setPrivateField(otherOwner, "id", UUID.randomUUID());

assertThatThrownBy(() -> policy.canCreate(otherOwner, pet, clinic))
.isInstanceOf(ForbiddenException.class)
.hasMessage("Du kan inte skapa ärende för någon annans djur");
}

@Test
void canCreate_vetOnOwnClinic_shouldNotThrow() {
assertThatNoException().isThrownBy(() -> policy.canCreate(vet, pet, clinic));
}

@Test
void canCreate_vetOnOtherClinic_shouldThrowForbidden() {
assertThatThrownBy(() -> policy.canCreate(vetOtherClinic, pet, clinic))
.isInstanceOf(ForbiddenException.class)
.hasMessage("Du kan inte skapa ärende för en annan klinik");
}

@Test
void canCreate_admin_shouldNotThrow() {
assertThatNoException().isThrownBy(() -> policy.canCreate(admin, pet, clinic));
}

// -------------------------------------------------------------------------
// canUpdate — OWNER får uppdatera eget ärende (öppet)
// -------------------------------------------------------------------------

@Test
void canUpdate_ownerOfRecord_shouldNotThrow() {
assertThatNoException().isThrownBy(() -> policy.canUpdate(owner, openRecord));
}

@Test
void canUpdate_ownerOfOtherRecord_shouldThrowForbidden() throws Exception {
User otherOwner = new User("Bertil Annan", "b@mail.se", "hash", Role.OWNER);
setPrivateField(otherOwner, "id", UUID.randomUUID());

assertThatThrownBy(() -> policy.canUpdate(otherOwner, openRecord))
.isInstanceOf(ForbiddenException.class)
.hasMessage("Du har inte tillgång till detta ärende");
}

@Test
void canUpdate_vetOnOwnClinic_shouldNotThrow() {
assertThatNoException().isThrownBy(() -> policy.canUpdate(vet, openRecord));
}

@Test
void canUpdate_vetOnOtherClinic_shouldThrowForbidden() {
assertThatThrownBy(() -> policy.canUpdate(vetOtherClinic, openRecord))
.isInstanceOf(ForbiddenException.class)
.hasMessage("Du har inte tillgång till ärenden på en annan klinik");
}

@Test
void canUpdate_admin_shouldNotThrow() {
assertThatNoException().isThrownBy(() -> policy.canUpdate(admin, openRecord));
}

@Test
void canUpdate_closedRecord_shouldThrowBusinessRule() {
assertThatThrownBy(() -> policy.canUpdate(vet, closedRecord))
.isInstanceOf(BusinessRuleException.class)
.hasMessage("Stängda ärenden kan inte uppdateras");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

OWNER create/update tests contradict the stated PR objective.

The PR description explicitly states that OWNER should no longer be able to create or update medical records, and that MedicalRecordPolicy.canCreate / canUpdate should throw ForbiddenException for OWNER. However, these tests assert the opposite:

  • canCreate_ownerOfPet_shouldNotThrow (Line 77-79) — asserts OWNER can create for own pet.
  • canUpdate_ownerOfRecord_shouldNotThrow (Line 113-115) — asserts OWNER can update own open record.

Looking at the current MedicalRecordPolicy source, the OWNER branches in both methods still only throw when the user isn't the pet/record owner — they do not universally block OWNER as the PR description claims. So one of two things is true, and both are problematic:

  1. The policy code was not actually tightened as described in the PR — the claimed security restriction (OWNER blocked at the policy layer) is missing, and relying on URL rules alone is defense-in-depth that the PR explicitly promised in the policy layer.
  2. Or the tests encode pre-change expectations and will lock in the wrong behavior going forward.

Please reconcile MedicalRecordPolicy.canCreate / canUpdate with the PR objective and flip these assertions accordingly. Expected tests for the new behavior would look like:

🔧 Suggested expectation change (if policy is updated per PR description)
 `@Test`
-void canCreate_ownerOfPet_shouldNotThrow() {
-    assertThatNoException().isThrownBy(() -> policy.canCreate(owner, pet, clinic));
+void canCreate_owner_shouldAlwaysThrowForbidden() {
+    assertThatThrownBy(() -> policy.canCreate(owner, pet, clinic))
+            .isInstanceOf(ForbiddenException.class);
 }

 `@Test`
-void canUpdate_ownerOfRecord_shouldNotThrow() {
-    assertThatNoException().isThrownBy(() -> policy.canUpdate(owner, openRecord));
+void canUpdate_owner_shouldAlwaysThrowForbidden() {
+    assertThatThrownBy(() -> policy.canUpdate(owner, openRecord))
+            .isInstanceOf(ForbiddenException.class);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/vet1177/policy/MedicalRecordPolicyTest.java` around
lines 76 - 149, Tests in MedicalRecordPolicyTest contradict the PR objective
that OWNERs must be blocked by the policy; update the two owner-positive tests
(canCreate_ownerOfPet_shouldNotThrow and canUpdate_ownerOfRecord_shouldNotThrow)
so they assert a ForbiddenException is thrown when calling
MedicalRecordPolicy.canCreate(owner, pet, clinic) and
MedicalRecordPolicy.canUpdate(owner, openRecord) respectively, using the same
localized forbidden messages the policy produces (e.g., the messages used
elsewhere for owner-forbidden cases) or the exact strings from
MedicalRecordPolicy; if you prefer the other route, instead update
MedicalRecordPolicy.canCreate / MedicalRecordPolicy.canUpdate to universally
throw ForbiddenException for Role.OWNER before the owner-equality checks so the
existing tests remain valid—choose one consistent approach and make the tests
and policy match (reference: MedicalRecordPolicy.canCreate,
MedicalRecordPolicy.canUpdate, and the test methods named above).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/example/vet1177/security/SecurityConfig.java`:
- Around line 95-107: Update SecurityConfig to add URL-layer deny rules for
creating/updating medical records and attachment operations: add requestMatchers
to block POST /api/medical-records, PUT /api/medical-records/* (base record
update) and attachment upload/delete endpoints (e.g.,
/api/medical-records/*/attachments/**) so OWNER cannot reach them at the URL
layer; ensure the general PUT matcher for "/api/medical-records/*" is placed
after the specific PUT matchers ("/close", "/assign-vet", "/status") so they
take precedence; also update the surrounding comment text in SecurityConfig to
reflect that OWNER is blocked at the URL layer for these operations rather than
allowed to create/update or manage attachments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ce041763-99d9-4c00-9699-8ededeeda164

📥 Commits

Reviewing files that changed from the base of the PR and between 005f95b and 7d28269.

📒 Files selected for processing (3)
  • src/main/java/org/example/vet1177/policy/CommentPolicy.java
  • src/main/java/org/example/vet1177/security/SecurityConfig.java
  • src/test/java/org/example/vet1177/policy/CommentPolicyTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/example/vet1177/policy/CommentPolicy.java

Comment on lines +95 to +107
// ─── VET/ADMIN: ärende-status/tilldelning/stängning ───
.requestMatchers(HttpMethod.PUT, "/api/medical-records/*/close").hasAnyRole("VET", "ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/medical-records/*/assign-vet").hasAnyRole("VET", "ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/medical-records/*/status").hasAnyRole("VET", "ADMIN")

// ─── VET/ADMIN: klinik-vy ───
.requestMatchers(HttpMethod.GET, "/api/medical-records/clinic/**").hasAnyRole("VET", "ADMIN")

// ─── Alla inloggade + policy finjusterar ───
// Här ligger medvetet: skapa/uppdatera egna ärenden, se/skapa/radera egna bilagor,
// kommentarer, aktivitetsloggar, /api/pets/**. URL-lagret kan inte uttrycka ägarskap
// eller "samma klinik" — det gör policy. OWNER kan skapa/uppdatera egna ärenden;
// kliniska anteckningar döljs via CommentType-filter i CommentService.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify which medical-record and attachment endpoints actually exist in controllers,
# to confirm the URL patterns the new matchers should cover.
rg -nP --type=java -C2 '@(Post|Put|Delete|Get)Mapping' -g '**/controller/**' -g '**/MedicalRecord*.java' -g '**/Attachment*.java'

Repository: ithsjava25/project-backend-org-random-coders

Length of output: 21256


🏁 Script executed:

cat -n src/main/java/org/example/vet1177/security/SecurityConfig.java | sed -n '80,125p'

Repository: ithsjava25/project-backend-org-random-coders

Length of output: 3294


Missing URL-layer rules for medical-record create/update and attachment operations contradict the PR intent.

The PR description states OWNER is blocked at the URL layer for POST /api/medical-records, PUT /api/medical-records/{id} (base record update), and attachment upload/delete. None of these rules exist in the matcher list; all fall through to .anyRequest().authenticated(), so authorization is delegated entirely to the policy layer.

Additionally, the comment at lines 104–107 contradicts the stated PR goal. It says "OWNER kan skapa/uppdatera egna ärenden" and "se/skapa/radera egna bilagor" — implying policy-layer control — yet the PR description explicitly requires URL-layer rejection of OWNER for these operations.

These endpoints should be blocked at the URL layer for defense-in-depth:

🛡️ Add missing URL matchers
                         // ─── VET/ADMIN: ärende-status/tilldelning/stängning ───
                         .requestMatchers(HttpMethod.PUT, "/api/medical-records/*/close").hasAnyRole("VET", "ADMIN")
                         .requestMatchers(HttpMethod.PUT, "/api/medical-records/*/assign-vet").hasAnyRole("VET", "ADMIN")
                         .requestMatchers(HttpMethod.PUT, "/api/medical-records/*/status").hasAnyRole("VET", "ADMIN")
 
+                        // ─── VET/ADMIN: skapa/uppdatera medicinska ärenden ───
+                        .requestMatchers(HttpMethod.POST, "/api/medical-records").hasAnyRole("VET", "ADMIN")
+                        .requestMatchers(HttpMethod.PUT, "/api/medical-records/*").hasAnyRole("VET", "ADMIN")
+
+                        // ─── VET/ADMIN: bilagor (ladda upp/radera) ───
+                        .requestMatchers(HttpMethod.POST, "/api/attachments/record/**").hasAnyRole("VET", "ADMIN")
+                        .requestMatchers(HttpMethod.DELETE, "/api/attachments/**").hasAnyRole("VET", "ADMIN")
+
                         // ─── VET/ADMIN: klinik-vy ───
                         .requestMatchers(HttpMethod.GET, "/api/medical-records/clinic/**").hasAnyRole("VET", "ADMIN")
 
                         // ─── Alla inloggade + policy finjusterar ───
-                        // Här ligger medvetet: skapa/uppdatera egna ärenden, se/skapa/radera egna bilagor,
-                        // kommentarer, aktivitetsloggar, /api/pets/**. URL-lagret kan inte uttrycka ägarskap
-                        // eller "samma klinik" — det gör policy. OWNER kan skapa/uppdatera egna ärenden;
-                        // kliniska anteckningar döljs via CommentType-filter i CommentService.
+                        // Här ligger medvetet: se egna ärenden/bilagor, kommentarer, aktivitetsloggar,
+                        // /api/pets/**. URL-lagret kan inte uttrycka ägarskap eller "samma klinik" —
+                        // det gör policy. Kliniska anteckningar döljs via CommentType-filter i CommentService.
                         .anyRequest().authenticated()

Note: The PUT /api/medical-records/* rule must come after the specific sub-path matchers (*/close, */assign-vet, */status) so the more specific patterns match first.

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

In `@src/main/java/org/example/vet1177/security/SecurityConfig.java` around lines
95 - 107, Update SecurityConfig to add URL-layer deny rules for
creating/updating medical records and attachment operations: add requestMatchers
to block POST /api/medical-records, PUT /api/medical-records/* (base record
update) and attachment upload/delete endpoints (e.g.,
/api/medical-records/*/attachments/**) so OWNER cannot reach them at the URL
layer; ensure the general PUT matcher for "/api/medical-records/*" is placed
after the specific PUT matchers ("/close", "/assign-vet", "/status") so they
take precedence; also update the surrounding comment text in SecurityConfig to
reflect that OWNER is blocked at the URL layer for these operations rather than
allowed to create/update or manage attachments.

@annikaholmqvist94
annikaholmqvist94 merged commit 60f6cea into main Apr 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant