Skip to content

AuthorityService doesn't include role names as granted authorities, breaking @PreAuthorize("hasRole(...)") #293

@devondragon

Description

@devondragon

Bug Report

Summary

AuthorityService.getAuthoritiesFromRoles() only maps privilege names to GrantedAuthority objects — it does not include the role names themselves (e.g., ROLE_ADMIN). This causes the framework's own @PreAuthorize("hasRole('ADMIN')") annotations (on methods like UserEmailService.initiateAdminPasswordReset()) to always deny access, even for users who have the ROLE_ADMIN role.

Root Cause

In AuthorityService.getAuthoritiesFromRoles(), the stream pipeline does:

roles.stream()
    .flatMap(role -> role.getPrivileges().stream())  // only privileges
    .map(Privilege::getName)
    .map(SimpleGrantedAuthority::new)
    .collect(toSet());

This produces authorities like ADMIN_PRIVILEGE, LOGIN_PRIVILEGE, etc. — but never ROLE_ADMIN or ROLE_USER.

Meanwhile, UserEmailService.initiateAdminPasswordReset() (all overloads) is annotated with @PreAuthorize("hasRole('ADMIN')"), which checks for ROLE_ADMIN as a granted authority. Since ROLE_ADMIN is never granted, the call always fails with AuthorizationDeniedException: Access Denied.

Impact

Any consumer using UserEmailService.initiateAdminPasswordReset() gets a 500 error. The only workaround is to add ROLE_ADMIN as an explicit privilege name for the ROLE_ADMIN role in the config:

user:
  roles:
    roles-and-privileges:
      "[ROLE_ADMIN]":
        - ROLE_ADMIN          # workaround: needed for framework's hasRole() checks
        - ADMIN_PRIVILEGE

Suggested Fix

AuthorityService.getAuthoritiesFromRoles() should include both role names and privilege names as granted authorities:

public Collection<? extends GrantedAuthority> getAuthoritiesFromRoles(Collection<Role> roles) {
    Set<GrantedAuthority> authorities = new HashSet<>();

    for (Role role : roles) {
        // Include the role itself (needed for hasRole() checks)
        authorities.add(new SimpleGrantedAuthority(role.getName()));

        // Include all privileges for the role
        for (Privilege privilege : role.getPrivileges()) {
            authorities.add(new SimpleGrantedAuthority(privilege.getName()));
        }
    }

    return authorities;
}

This is the standard Spring Security convention — UserDetailsService implementations typically include both role names and privilege names as granted authorities.

Environment

  • DS Spring User Framework version: 4.3.0
  • Spring Security version: 7.0.3
  • Discovered in: MagicMenu admin password reset feature (MM-207)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions