Skip to content

Feature/security#35

Merged
EdvinSandgren merged 6 commits into
mainfrom
feature/security
Apr 20, 2026
Merged

Feature/security#35
EdvinSandgren merged 6 commits into
mainfrom
feature/security

Conversation

@EdvinSandgren

@EdvinSandgren EdvinSandgren commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Adds the basis for the security layer, enabling the creation of users and restricting endpoints. By default endpoints are restricted to the SYSADMIN role, unless otherwise specified.

Summary by CodeRabbit

  • New Features

    • Username-based authentication and a single principal type for all user views
    • Role-based dashboards and path-specific access rules; form login with custom login page
  • Security Improvements

    • Passwords are now hashed/encoded and a stronger authentication provider is configured
    • Method-level security enabled
  • Bug Fixes

    • Corrected seeded admin/sysadmin credentials and added usernames to seeded users
  • Tests

    • Updated tests to include the new username field in test users

# Conflicts:
#	src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java
@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Replaces SecurityUser with UserPrincipal across auth flow; adds persistent username and username-based lookups; introduces UserPrincipal and PasswordEncoder-backed authentication; enables role- and path-based security with form login; seeds users with encoded passwords and updates tests to populate username.

Changes

Cohort / File(s) Summary
Principal Type Migration
src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java, src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java, src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java
Switched method parameters/imports from SecurityUser to UserPrincipal in controllers and service access-checks; updated constructor signatures where needed.
Security Configuration
src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java
Enabled web/method security, added constructor-injected UserDetailsService, role/path-based authorization rules, form login (/user/login), logout, HTTP Basic, AuthenticationProvider bean, and a delegating PasswordEncoder bean; adjusted headers/frame options.
UserDetails & Service Wiring
src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java, .../UserDetailsServiceImpl.java, .../SecurityUser.java
Added UserPrincipal implementing UserDetails; UserDetailsServiceImpl now loads by username and returns UserPrincipal; removed legacy SecurityUser.
User Model, Mapper & Repo
src/main/java/org/example/visacasemanagementsystem/user/entity/User.java, .../mapper/UserMapper.java, .../repository/UserRepository.java
Added persistent unique username field, made password a persisted column, mapper now sets username from email, and repository gains findByUsername(String).
Password Encoding & Data Seed
src/main/java/org/example/visacasemanagementsystem/config/DataInitializer.java
initData now accepts PasswordEncoder; seeded users have encoded passwords, usernames set to emails, and admin/sysadmin emails standardized.
Authorization Enum
src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java
Added asAuthority() to emit ROLE_-prefixed authority strings.
Misc / Tests
src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java, src/test/.../VisaServiceIntegrationTest.java, src/test/.../CommentServiceIntegrationTest.java
Minor formatting change; tests updated to set username alongside email when creating test users.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Browser as Browser
    participant SecurityConfig as SecurityConfig
    participant AuthProvider as AuthenticationProvider
    participant UserDetailsService as UserDetailsService
    participant UserRepository as UserRepository
    participant UserPrincipal as UserPrincipal
    participant Dashboard as Dashboard

    User->>Browser: Navigate to /dashboard
    Browser->>SecurityConfig: GET /dashboard

    rect rgba(200,150,100,0.5)
    note over SecurityConfig,AuthProvider: Authentication Required
    SecurityConfig->>SecurityConfig: Is authenticated?
    alt Not authenticated
        SecurityConfig->>Browser: Redirect to /user/login
        Browser->>User: Show login form
        User->>Browser: Submit credentials
        Browser->>SecurityConfig: POST /user/login
        SecurityConfig->>AuthProvider: authenticate(username,password)
        AuthProvider->>UserDetailsService: loadUserByUsername(username)
        UserDetailsService->>UserRepository: findByUsername(username)
        UserRepository-->>UserDetailsService: Optional<User>
        UserDetailsService->>UserPrincipal: new UserPrincipal(user)
        UserPrincipal-->>UserDetailsService: UserDetails
        UserDetailsService-->>AuthProvider: UserDetails
        AuthProvider->>AuthProvider: Verify password (PasswordEncoder)
        alt Valid
            AuthProvider-->>SecurityConfig: Auth success
            SecurityConfig->>Browser: Redirect to /dashboard
        else Invalid
            AuthProvider-->>Browser: Login failed
        end
    else Already authenticated
    end
    end

    rect rgba(100,150,200,0.5)
    note over SecurityConfig,Dashboard: Authorization Check
    SecurityConfig->>SecurityConfig: Check granted authorities
    alt Has required ROLE
        SecurityConfig->>Dashboard: Allow access
        Dashboard-->>Browser: Render content
    else Missing role
        SecurityConfig->>Browser: 403 Forbidden
    end
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Suggested Reviewers

  • Martin-E-Karlsson
  • dinbah18

Poem

🐰 Hop, hop, a principal in hand,

Usernames sprout across the land,
Encoded keys and roles on guard,
Logins hum and checks run hard,
A rabbit cheers — secure and grand! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feature/security' is vague and does not clearly summarize the main changes. It uses a generic convention-based label without conveying meaningful information about what security features or functionality were actually implemented. Replace with a more descriptive title that highlights the primary security changes, such as 'Add Spring Security configuration with role-based access control' or 'Implement user authentication and authorization layer'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 feature/security

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.

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 (2)
src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java (1)

25-44: ⚠️ Potential issue | 🟠 Major

Coupling username to email on update silently rotates login identity, and mapping raw passwords in the mapper is a risky boundary.

Two issues in this change:

  1. updateEntityFromDTO now sets username = dto.email() whenever a profile is edited. Since UserDetailsServiceImpl.loadUserByUsername resolves users via findByUsername, editing a user's email also changes their login identifier — invalidating any future logins with the old username and, depending on session strategy, potentially confusing already-authenticated sessions. If the intent is "email is the username", consider using email as the auth lookup directly (keep findByEmail for authentication) instead of denormalizing it into a separate username column, or decouple username from email so updates don't silently rebind login identity.

  2. toEntity(CreateUserDTO) now sets user.setPassword(dto.password()) in the mapper. Per prior learnings, UserService.createUser() is expected to encode via passwordEncoder.encode(...). If UserService now delegates full entity construction to this mapper and forgets to re-set the encoded password after toEntity(...), the plaintext set here will be what’s persisted. Safer pattern: omit password from the mapper entirely and have the service perform user.setPassword(passwordEncoder.encode(dto.password())) as the single source of truth.

🛡️ Proposed fix (option: don't map password in mapper)
     public User toEntity(CreateUserDTO dto){
         if (dto == null) return null;

         User user = new User();
         user.setFullName(dto.fullName());
         user.setEmail(dto.email());
         user.setUsername(dto.email());
-        user.setPassword(dto.password());
         user.setUserAuthorization(dto.userAuthorization());
         return user;
     }

     // Uppdatering (DTO --> Befintlig Entity)
     public void updateEntityFromDTO(UpdateUserDTO dto, User user) {
         if (dto == null || user == null) return;

         user.setFullName(dto.fullName());
         user.setEmail(dto.email());
-        user.setUsername(dto.email());
     }

And in UserService.createUser(...) ensure:

User user = userMapper.toEntity(dto);
user.setPassword(passwordEncoder.encode(dto.password()));
userRepository.save(user);

Based on learnings from PR #26 that UserService.createUser() is the intended place for passwordEncoder.encode(...).

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

In
`@src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java`
around lines 25 - 44, The mapper currently couples username to email and maps
plain-text passwords; change UserMapper so updateEntityFromDTO does NOT
overwrite user.setUsername(dto.email()) (leave username unchanged or only change
via an explicit username update flow) and remove setting password from
toEntity(CreateUserDTO) so it does not write raw passwords. Then ensure
UserService.createUser(...) calls userMapper.toEntity(dto) and explicitly sets
user.setPassword(passwordEncoder.encode(dto.password())) before saving; if
authentication should use email, adjust
UserDetailsServiceImpl.loadUserByUsername to lookup by email (findByEmail)
instead of findByUsername to avoid silently rotating login identity.
src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java (1)

60-67: ⚠️ Potential issue | 🟡 Minor

Set UserAuthorization before save(...), not after.

savedUser.setUserAuthorization(UserAuthorization.USER) runs after userRepository.save(user) and is never explicitly persisted — it only reaches the DB because the entity stays managed inside @Transactional and gets flushed on commit. That is brittle (e.g., if someone later moves the write out of the transactional boundary, or the method is called from a non‑transactional context, the role assignment silently disappears).

It also means whatever userMapper.toEntity(dto) wrote for authorization is first persisted and then overwritten — if that's intentional defense‑in‑depth against privilege escalation via the DTO, do it explicitly before save.

🐛 Proposed fix
-        user.setPassword(passwordEncoder.encode(dto.password()));
-        try {
-            User savedUser = userRepository.save(user);
-            savedUser.setUserAuthorization(UserAuthorization.USER);
-            return userMapper.toDTO(savedUser);
-        } catch (DataIntegrityViolationException e) {
+        user.setPassword(passwordEncoder.encode(dto.password()));
+        // Force role server-side; never trust the DTO for privilege assignment on signup.
+        user.setUserAuthorization(UserAuthorization.USER);
+        try {
+            User savedUser = userRepository.save(user);
+            return userMapper.toDTO(savedUser);
+        } catch (DataIntegrityViolationException e) {
             throw new IllegalArgumentException("A user with this email already exists", e);
         }

Based on learnings (PR #26), UserService.createUser is the intended injection point for password/role hardening; making the role assignment explicit and pre‑save rounds that out.

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

In
`@src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java`
around lines 60 - 67, In UserService.createUser, the UserAuthorization is set
after calling userRepository.save(user), relying on transactional EntityManager
flushing; move the hardening so the authorization is explicitly set on the
entity before persisting: after converting DTO to entity
(userMapper.toEntity(...)) and before calling userRepository.save(user) set
user.setUserAuthorization(UserAuthorization.USER) (or override any incoming
value), then call save and return userMapper.toDTO(savedUser); keep the existing
DataIntegrityViolationException handling intact.
🧹 Nitpick comments (7)
src/main/java/org/example/visacasemanagementsystem/user/entity/User.java (1)

25-29: Consider DB-level nullable = false on the new username and password columns.

@NotBlank is bean-validation only and runs during save lifecycle; it does not produce a NOT NULL DDL constraint. If validation is ever bypassed (native query, direct SQL, disabled validator), you could end up with null usernames/passwords — and UserDetailsServiceImpl / Spring Security will behave poorly with either. Mirror the existing style used on fullName:

🛡️ Proposed fix
-    `@NotBlank` `@Column`(unique = true)
-    private String username;
-
-    `@NotBlank` `@Column`
-    private String password;
+    `@NotBlank` `@Column`(nullable = false, unique = true)
+    private String username;
+
+    `@NotBlank` `@Column`(nullable = false)
+    private String password;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/visacasemanagementsystem/user/entity/User.java`
around lines 25 - 29, The new User entity fields username and password use
`@NotBlank` (bean validation) but lack DB-level NOT NULL; update the User class to
add nullable = false to the `@Column` annotations on the username and password
fields (mirror the existing fullName style) so the database schema enforces
non-null values and prevents null usernames/passwords from being
persisted/bypassing validation.
src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java (1)

6-11: Overriding toString() on the enum has wide-reaching side effects.

Every caller that prints or serializes a UserAuthorization — logs, Thymeleaf expressions like ${user.userAuthorization}, JSON serialization (Jackson uses toString() for enums only when WRITE_ENUMS_USING_TO_STRING is enabled, but many custom toString() calls in User.toString() and similar do use it), and any .toString() invoked on a UserAuthorization value — will now emit ROLE_USER/ROLE_ADMIN/ROLE_SYSADMIN instead of USER/ADMIN/SYSADMIN. This can silently break views, log output, and any other string-based consumers.

@Enumerated(EnumType.STRING) and Enum.valueOf(...) rely on name() (not toString()), so persistence still works — but the broader behavioral change is easy to forget.

Consider exposing a dedicated method instead and constructing the authority explicitly in UserPrincipal:

♻️ Proposed refactor
 public enum UserAuthorization {
     USER,
     ADMIN,
-    SYSADMIN;
-
-    `@Override`
-    public String toString() {
-        return "ROLE_" + this.name();
-    }
+    SYSADMIN;
+
+    public String asAuthority() {
+        return "ROLE_" + name();
+    }
 }

And in UserPrincipal.getAuthorities():

return Collections.singleton(new SimpleGrantedAuthority(user.getUserAuthorization().asAuthority()));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java`
around lines 6 - 11, Overriding toString() on the UserAuthorization enum
introduces wide-reaching breakage; remove the toString() override from
UserAuthorization and add a dedicated accessor (e.g., asAuthority() or
toRoleString()) that returns "ROLE_" + name(), then update the authority
construction in UserPrincipal.getAuthorities() (use new
SimpleGrantedAuthority(user.getUserAuthorization().asAuthority())) so all
logging/serialization still sees the original enum names while security gets the
ROLE_ prefixed string.
src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java (1)

41-45: defaultSuccessUrl("/dashboard", true) forces the user back to /dashboard even after they tried to access a deep link.

Passing true as the second arg means "always redirect to this URL on success", so the saved-request redirect flow (return the user to the page they originally attempted) is disabled. For a dashboard-heavy app this may be intentional, but if deep-linking is ever desired, switch to false (or omit) and let Spring's SavedRequestAwareAuthenticationSuccessHandler do the right thing.

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

In
`@src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java`
around lines 41 - 45, The current formLogin configuration in SecurityConfig uses
defaultSuccessUrl("/dashboard", true) which always redirects to /dashboard and
prevents returning users to saved deep links; change the call in the
SecurityConfig class (the formLogin(...) block where defaultSuccessUrl is
configured) to use defaultSuccessUrl("/dashboard", false) or remove the boolean
so SavedRequestAwareAuthenticationSuccessHandler can perform saved-request
redirects instead.
src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java (1)

104-141: Consolidate role‑check helpers and rename the validateSysAdmin overload.

Two issues in this cluster:

  1. validateSysAdmin(Long requesterId) (line 104) and validateSysAdmin(UserPrincipal principal) (line 123) share a name but have completely different semantics — one loads a user by id from the DB and checks the entity field, the other reads authorities off the authenticated principal. Overload resolution at the call site hides that divergence; give them distinct names (e.g., requireSysAdminById vs requireSysAdmin) so reviewers can tell which trust source is used.
  2. validateProfileAccess, validateSysAdmin(UserPrincipal), and validateAdmin all repeat the same principal.getAuthorities().stream().anyMatch(...) pattern with role strings. Extract a small hasRole(UserPrincipal, String) helper (or a constants class for ROLE_SYSADMIN/ROLE_ADMIN) to remove the duplication and keep role names in one place.
♻️ Sketch
+    private static boolean hasRole(UserPrincipal principal, String role) {
+        return principal.getAuthorities().stream()
+                .anyMatch(a -> Objects.equals(a.getAuthority(), role));
+    }
+
     public void validateProfileAccess(UserPrincipal principal, Long userId) {
         boolean isOwnProfile = principal.getUserId().equals(userId);
-        boolean isSysAdmin = principal.getAuthorities().stream()
-                .anyMatch(a -> Objects.equals(a.getAuthority(), "ROLE_SYSADMIN"));
-        if (!isOwnProfile && !isSysAdmin) {
+        if (!isOwnProfile && !hasRole(principal, "ROLE_SYSADMIN")) {
             throw new UnauthorizedException("You do not have permission to edit this profile.");
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java`
around lines 104 - 141, Rename the DB-backed validateSysAdmin(Long requesterId)
to requireSysAdminById and rename the principal-based
validateSysAdmin(UserPrincipal principal) to requireSysAdmin so callers clearly
indicate which trust source is used; then extract a single helper
hasRole(UserPrincipal principal, String role) (and optionally central
ROLE_SYSADMIN / ROLE_ADMIN constants) and refactor validateProfileAccess,
requireSysAdmin, and validateAdmin to call hasRole(...) instead of repeating
principal.getAuthorities().stream().anyMatch(...), updating all call sites
accordingly.
src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java (2)

42-60: Drop the redundant UserDetails default overrides.

isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, and isEnabled all just delegate to UserDetails.super.*. These overrides add noise and invite confusion about whether custom account‑state logic exists. Remove them until real behavior is needed (e.g., backed by enabled/locked fields on User).

♻️ Proposed cleanup
-    `@Override`
-    public boolean isAccountNonExpired() {
-        return UserDetails.super.isAccountNonExpired();
-    }
-
-    `@Override`
-    public boolean isAccountNonLocked() {
-        return UserDetails.super.isAccountNonLocked();
-    }
-
-    `@Override`
-    public boolean isCredentialsNonExpired() {
-        return UserDetails.super.isCredentialsNonExpired();
-    }
-
-    `@Override`
-    public boolean isEnabled() {
-        return UserDetails.super.isEnabled();
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java`
around lines 42 - 60, The four redundant overrides in
UserPrincipal—isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired,
and isEnabled—simply delegate to UserDetails.super and should be removed to
reduce noise; delete these methods from the UserPrincipal class (leaving other
UserDetails methods intact) and only reintroduce custom implementations later if
you implement real account-state fields (e.g., enabled/locked) on the User
model.

20-24: Avoid deriving authority strings from UserAuthorization.toString().

Using toString() as a business‑critical serialization format is fragile — toString() is conventionally for debugging/logging, and any future change (e.g., adding a display name, adjusting log formatting) will silently break authorization. Expose an explicit accessor on UserAuthorization (or build the string here) so the contract is intentional and searchable.

♻️ Proposed refactor
-    `@Override`
-    `@NullMarked`
-    public Collection<? extends GrantedAuthority> getAuthorities() {
-        return Collections.singleton(new SimpleGrantedAuthority(user.getUserAuthorization().toString()));
-    }
+    `@Override`
+    `@NullMarked`
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return Collections.singleton(
+                new SimpleGrantedAuthority("ROLE_" + user.getUserAuthorization().name()));
+    }

Then the custom UserAuthorization#toString() override can be dropped, leaving toString() to its default purpose.

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

In
`@src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java`
around lines 20 - 24, The getAuthorities() implementation in UserPrincipal
should not rely on UserAuthorization.toString(); instead add an explicit
accessor (e.g., UserAuthorization#getAuthority or `#getRoleName` or use an enum
`#name`()) on the UserAuthorization type and call that here; change
UserPrincipal.getAuthorities() to create the SimpleGrantedAuthority from that
explicit accessor (e.g., new
SimpleGrantedAuthority(user.getUserAuthorization().getAuthority())), and
update/introduce the accessor in the UserAuthorization class so the authority
string is a stable, documented contract (you can later remove any custom
toString() override).
src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java (1)

73-88: viewProfile re‑computes authorization flags already evaluated by validateProfileAccess.

validateProfileAccess at line 79 already performs the isOwnProfile || isSysAdmin check (throwing on failure), and lines 81‑86 recompute essentially the same predicate to set canEdit. That's two sources of truth for the same rule — if the policy ever changes (e.g., ADMIN can also edit), it'll have to be edited in both places.

Consider either returning the decision from UserService (e.g., boolean canEditProfile(UserPrincipal, Long)) or replacing the anyMatch literal with the same hasRole helper suggested in UserService.java, so "ROLE_SYSADMIN" isn't hard‑coded in two files.

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

In
`@src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java`
around lines 73 - 88, The viewProfile method currently duplicates authorization
logic from userService.validateProfileAccess; replace the local recomputation
with a single call to the service so there is one source of truth: remove the
isOwnProfile/isSysAdmin anyMatch logic and instead call a new or existing
service method (e.g., userService.canEditProfile(principal, userId)) or use the
UserService.hasRole helper to determine edit permission, then set
model.addAttribute("canEdit", userService.canEditProfile(principal, userId));
keep validateProfileAccess for throwing on unauthorized access but derive the
view flag from the service method to avoid duplicated hard‑coded
"ROLE_SYSADMIN".
🤖 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/visacasemanagementsystem/config/SecurityConfig.java`:
- Around line 31-45: Replace the overly-broad requestMatchers in SecurityConfig
so they target the exact endpoints in UserViewController: change
requestMatchers("/**/admin").hasRole("ADMIN") to
requestMatchers("/dashboard/admin").hasRole("ADMIN") and
requestMatchers("/**/applicant").hasRole("USER") to
requestMatchers("/dashboard/applicant").hasRole("USER"); keep the other matchers
(.requestMatchers("/user/signup"), .requestMatchers("/user/login"),
.requestMatchers("/dashboard")) and retain .anyRequest().hasRole("SYSADMIN") if
you want all other routes restricted to SYSADMIN. Also remove or comment out
httpBasic(withDefaults()) (or conditionally enable it for API clients) to avoid
HTTP Basic prompts in browsers when using formLogin(...). Ensure these changes
are made in the SecurityConfig class around the authorizeHttpRequests(...) and
formLogin(...) configuration blocks.

In
`@src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java`:
- Around line 36-40: The User entity's username field is missing a NOT NULL
constraint which contradicts the `@NullMarked` contract on
UserPrincipal.getUsername(); update the User entity declaration for the username
field to include nullable = false and a validation annotation (e.g., `@NotBlank`)
and keep unique = true so it reads with both nullable = false and unique = true;
then run or create a DB migration to add the NOT NULL constraint and backfill
any existing NULL usernames in the users table so runtime authentication
(UserPrincipal.getUsername) cannot encounter nulls.

---

Outside diff comments:
In
`@src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java`:
- Around line 25-44: The mapper currently couples username to email and maps
plain-text passwords; change UserMapper so updateEntityFromDTO does NOT
overwrite user.setUsername(dto.email()) (leave username unchanged or only change
via an explicit username update flow) and remove setting password from
toEntity(CreateUserDTO) so it does not write raw passwords. Then ensure
UserService.createUser(...) calls userMapper.toEntity(dto) and explicitly sets
user.setPassword(passwordEncoder.encode(dto.password())) before saving; if
authentication should use email, adjust
UserDetailsServiceImpl.loadUserByUsername to lookup by email (findByEmail)
instead of findByUsername to avoid silently rotating login identity.

In
`@src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java`:
- Around line 60-67: In UserService.createUser, the UserAuthorization is set
after calling userRepository.save(user), relying on transactional EntityManager
flushing; move the hardening so the authorization is explicitly set on the
entity before persisting: after converting DTO to entity
(userMapper.toEntity(...)) and before calling userRepository.save(user) set
user.setUserAuthorization(UserAuthorization.USER) (or override any incoming
value), then call save and return userMapper.toDTO(savedUser); keep the existing
DataIntegrityViolationException handling intact.

---

Nitpick comments:
In
`@src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java`:
- Around line 41-45: The current formLogin configuration in SecurityConfig uses
defaultSuccessUrl("/dashboard", true) which always redirects to /dashboard and
prevents returning users to saved deep links; change the call in the
SecurityConfig class (the formLogin(...) block where defaultSuccessUrl is
configured) to use defaultSuccessUrl("/dashboard", false) or remove the boolean
so SavedRequestAwareAuthenticationSuccessHandler can perform saved-request
redirects instead.

In
`@src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java`:
- Around line 73-88: The viewProfile method currently duplicates authorization
logic from userService.validateProfileAccess; replace the local recomputation
with a single call to the service so there is one source of truth: remove the
isOwnProfile/isSysAdmin anyMatch logic and instead call a new or existing
service method (e.g., userService.canEditProfile(principal, userId)) or use the
UserService.hasRole helper to determine edit permission, then set
model.addAttribute("canEdit", userService.canEditProfile(principal, userId));
keep validateProfileAccess for throwing on unauthorized access but derive the
view flag from the service method to avoid duplicated hard‑coded
"ROLE_SYSADMIN".

In `@src/main/java/org/example/visacasemanagementsystem/user/entity/User.java`:
- Around line 25-29: The new User entity fields username and password use
`@NotBlank` (bean validation) but lack DB-level NOT NULL; update the User class to
add nullable = false to the `@Column` annotations on the username and password
fields (mirror the existing fullName style) so the database schema enforces
non-null values and prevents null usernames/passwords from being
persisted/bypassing validation.

In
`@src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java`:
- Around line 42-60: The four redundant overrides in
UserPrincipal—isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired,
and isEnabled—simply delegate to UserDetails.super and should be removed to
reduce noise; delete these methods from the UserPrincipal class (leaving other
UserDetails methods intact) and only reintroduce custom implementations later if
you implement real account-state fields (e.g., enabled/locked) on the User
model.
- Around line 20-24: The getAuthorities() implementation in UserPrincipal should
not rely on UserAuthorization.toString(); instead add an explicit accessor
(e.g., UserAuthorization#getAuthority or `#getRoleName` or use an enum `#name`()) on
the UserAuthorization type and call that here; change
UserPrincipal.getAuthorities() to create the SimpleGrantedAuthority from that
explicit accessor (e.g., new
SimpleGrantedAuthority(user.getUserAuthorization().getAuthority())), and
update/introduce the accessor in the UserAuthorization class so the authority
string is a stable, documented contract (you can later remove any custom
toString() override).

In
`@src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java`:
- Around line 104-141: Rename the DB-backed validateSysAdmin(Long requesterId)
to requireSysAdminById and rename the principal-based
validateSysAdmin(UserPrincipal principal) to requireSysAdmin so callers clearly
indicate which trust source is used; then extract a single helper
hasRole(UserPrincipal principal, String role) (and optionally central
ROLE_SYSADMIN / ROLE_ADMIN constants) and refactor validateProfileAccess,
requireSysAdmin, and validateAdmin to call hasRole(...) instead of repeating
principal.getAuthorities().stream().anyMatch(...), updating all call sites
accordingly.

In
`@src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java`:
- Around line 6-11: Overriding toString() on the UserAuthorization enum
introduces wide-reaching breakage; remove the toString() override from
UserAuthorization and add a dedicated accessor (e.g., asAuthority() or
toRoleString()) that returns "ROLE_" + name(), then update the authority
construction in UserPrincipal.getAuthorities() (use new
SimpleGrantedAuthority(user.getUserAuthorization().asAuthority())) so all
logging/serialization still sees the original enum names while security gets the
ROLE_ prefixed string.
🪄 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: cd2fd140-fed8-4916-9b6f-fd2dbd2678b0

📥 Commits

Reviewing files that changed from the base of the PR and between 6d575bb and 090c124.

📒 Files selected for processing (13)
  • src/main/java/org/example/visacasemanagementsystem/ApplicationViewController.java
  • src/main/java/org/example/visacasemanagementsystem/VisaCaseManagementSystemApplication.java
  • src/main/java/org/example/visacasemanagementsystem/config/DataInitializer.java
  • src/main/java/org/example/visacasemanagementsystem/config/SecurityConfig.java
  • src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java
  • src/main/java/org/example/visacasemanagementsystem/user/controller/UserViewController.java
  • src/main/java/org/example/visacasemanagementsystem/user/entity/User.java
  • src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java
  • src/main/java/org/example/visacasemanagementsystem/user/repository/UserRepository.java
  • src/main/java/org/example/visacasemanagementsystem/user/security/SecurityUser.java
  • src/main/java/org/example/visacasemanagementsystem/user/security/UserDetailsServiceImpl.java
  • src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java
  • src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java
💤 Files with no reviewable changes (1)
  • src/main/java/org/example/visacasemanagementsystem/user/security/SecurityUser.java

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

🧹 Nitpick comments (3)
src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java (3)

31-34: @Nullable on getPassword() contradicts the entity contract.

User.password is declared @NotBlank @column(nullable = false), so getPassword() should never return null for a persisted user. Marking it @Nullable here will force callers (and any null-analysis tooling) to handle a case that cannot occur, and it's inconsistent with the @NullMarked treatment applied to getUsername() which has the same entity guarantees. Consider marking this @NullMarked too (or simply leaving it unannotated if the class/package is already null-marked).

🧹 Proposed change
-    `@Override`
-    public `@Nullable` String getPassword() {
-        return user.getPassword();
-    }
+    `@Override`
+    `@NullMarked`
+    public String getPassword() {
+        return user.getPassword();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java`
around lines 31 - 34, Remove the incorrect `@Nullable` annotation on
UserPrincipal.getPassword(), since User.getPassword() is annotated
`@NotBlank/`@Column(nullable = false) and thus cannot be null; update the method
in class UserPrincipal to either mark it as null-safe consistent with
getUsername() (e.g., use `@NullMarked/`@NonNull as per the project's nullability
convention) or leave it unannotated if the package/class is already null-marked
so callers and static analysis treat getPassword() as non-null.

20-24: Guard against a null userAuthorization.

getAuthorities() dereferences user.getUserAuthorization() unconditionally. While the column is declared @NotNull on the entity, a partially-populated User (e.g., constructed in tests, or loaded before the field is set) will trigger an NPE inside Spring Security's authentication pipeline, which surfaces as an opaque 500 rather than a clear auth failure. Consider returning an empty authority collection (or throwing a descriptive exception) when the authorization is missing.

🛡️ Proposed defensive check
-    public Collection<? extends GrantedAuthority> getAuthorities() {
-        return Collections.singleton(new SimpleGrantedAuthority(user.getUserAuthorization().asAuthority()));
-    }
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        var authorization = user.getUserAuthorization();
+        if (authorization == null) {
+            return Collections.emptyList();
+        }
+        return Collections.singleton(new SimpleGrantedAuthority(authorization.asAuthority()));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java`
around lines 20 - 24, getAuthorities() currently dereferences
user.getUserAuthorization() and can NPE; update UserPrincipal.getAuthorities()
to defensively check user.getUserAuthorization() for null and if null return
Collections.emptyList() (or alternatively throw a descriptive
IllegalStateException) instead of constructing a SimpleGrantedAuthority; ensure
the check references the existing user field and the SimpleGrantedAuthority
creation logic so you only create authorities when user.getUserAuthorization()
is non-null.

1-45: Move @NullMarked to the class level instead of annotating individual methods.

The current pattern of sprinkling @NullMarked on individual methods is verbose and inconsistent—getAuthorities() and getUsername() are annotated, but getFullName() and getUserId() are not, despite having identical nullness semantics. Per JSpecify guidance, @NullMarked is intended for class or package declarations, with @Nullable used to opt out specific members (in this case, getPassword()). This approach eliminates per-method noise and ensures uniform null-safety contracts across the class.

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

In
`@src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java`
around lines 1 - 45, Add `@NullMarked` to the UserPrincipal class declaration and
remove the per-method `@NullMarked` annotations on getAuthorities() and
getUsername(); keep the `@Nullable` on getPassword() to opt-out of the class-level
nullness and remove annotations from other methods like getFullName() and
getUserId() so the class-level nullness applies consistently for UserPrincipal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java`:
- Around line 31-34: Remove the incorrect `@Nullable` annotation on
UserPrincipal.getPassword(), since User.getPassword() is annotated
`@NotBlank/`@Column(nullable = false) and thus cannot be null; update the method
in class UserPrincipal to either mark it as null-safe consistent with
getUsername() (e.g., use `@NullMarked/`@NonNull as per the project's nullability
convention) or leave it unannotated if the package/class is already null-marked
so callers and static analysis treat getPassword() as non-null.
- Around line 20-24: getAuthorities() currently dereferences
user.getUserAuthorization() and can NPE; update UserPrincipal.getAuthorities()
to defensively check user.getUserAuthorization() for null and if null return
Collections.emptyList() (or alternatively throw a descriptive
IllegalStateException) instead of constructing a SimpleGrantedAuthority; ensure
the check references the existing user field and the SimpleGrantedAuthority
creation logic so you only create authorities when user.getUserAuthorization()
is non-null.
- Around line 1-45: Add `@NullMarked` to the UserPrincipal class declaration and
remove the per-method `@NullMarked` annotations on getAuthorities() and
getUsername(); keep the `@Nullable` on getPassword() to opt-out of the class-level
nullness and remove annotations from other methods like getFullName() and
getUserId() so the class-level nullness applies consistently for UserPrincipal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d637ba77-0393-417a-baf7-4778e937edd0

📥 Commits

Reviewing files that changed from the base of the PR and between 090c124 and 7ea9057.

📒 Files selected for processing (6)
  • src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java
  • src/main/java/org/example/visacasemanagementsystem/user/entity/User.java
  • src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java
  • src/main/java/org/example/visacasemanagementsystem/user/security/UserPrincipal.java
  • src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java
✅ Files skipped from review due to trivial changes (1)
  • src/test/java/org/example/visacasemanagementsystem/visa/VisaServiceIntegrationTest.java
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/java/org/example/visacasemanagementsystem/user/mapper/UserMapper.java
  • src/main/java/org/example/visacasemanagementsystem/user/entity/User.java
  • src/main/java/org/example/visacasemanagementsystem/user/UserAuthorization.java
  • src/main/java/org/example/visacasemanagementsystem/user/service/UserService.java

@Martin-E-Karlsson Martin-E-Karlsson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bra jobbat, jag tror inte något behöver göras angående den sista code rabbit issuen.

@eeebbaandersson
eeebbaandersson self-requested a review April 20, 2026 10:04

@eeebbaandersson eeebbaandersson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ser bra ut nu!

@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/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java (1)

68-71: ⚠️ Potential issue | 🟠 Major

Address authorId security gap now that Spring Security is integrated.

Line 70 creates a CreateCommentDTO with a client-supplied authorId. CommentService.createComment() (lines 52–53) still trusts dto.authorId() directly instead of resolving the author from the authenticated principal, leaving comment-author impersonation possible. Spring Security is now integrated across the codebase (@AuthenticationPrincipal is in use in other controllers); update CreateCommentDTO to remove authorId, refactor CommentService.createComment() to resolve the author from SecurityContextHolder or inject UserPrincipal, and update this test to use the authenticated principal pattern.

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

In
`@src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java`
around lines 68 - 71, Tests and service still accept a client-supplied authorId
which allows impersonation; remove authorId from CreateCommentDTO and update
CommentService.createComment(...) to resolve the author from the authenticated
principal (use SecurityContextHolder.getContext().getAuthentication() or inject
the UserPrincipal used across the app) instead of trusting dto.authorId(), then
update CommentServiceIntegrationTest to build CreateCommentDTO without authorId
and set up the test's security principal (or mock SecurityContext) so the
service resolves the expected author; adjust any callers/controllers accordingly
to stop passing authorId.
🤖 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/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java`:
- Around line 68-71: Tests and service still accept a client-supplied authorId
which allows impersonation; remove authorId from CreateCommentDTO and update
CommentService.createComment(...) to resolve the author from the authenticated
principal (use SecurityContextHolder.getContext().getAuthentication() or inject
the UserPrincipal used across the app) instead of trusting dto.authorId(), then
update CommentServiceIntegrationTest to build CreateCommentDTO without authorId
and set up the test's security principal (or mock SecurityContext) so the
service resolves the expected author; adjust any callers/controllers accordingly
to stop passing authorId.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19941a8e-0710-4202-b951-263f537c0023

📥 Commits

Reviewing files that changed from the base of the PR and between 7ea9057 and faa8a6e.

📒 Files selected for processing (1)
  • src/test/java/org/example/visacasemanagementsystem/comment/CommentServiceIntegrationTest.java

@EdvinSandgren
EdvinSandgren merged commit 46acde2 into main Apr 20, 2026
2 checks passed
@ithsjava25 ithsjava25 deleted a comment from coderabbitai Bot Apr 20, 2026
@EdvinSandgren EdvinSandgren linked an issue Apr 20, 2026 that may be closed by this pull request
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.

Add Spring Security Layer

3 participants