Skip to content

Commit d3b1599

Browse files
committed
fix(security): include role names as granted authorities
AuthorityService.getAuthoritiesFromRoles() only produced privilege-name authorities (e.g. ADMIN_PRIVILEGE), never role-name authorities (e.g. ROLE_ADMIN). Spring Security's hasRole() checks require ROLE_ADMIN to be present in the granted authority set -- the role hierarchy bean only expands from base role names, it does not conjure them. This caused @PreAuthorize("hasRole('ADMIN')") on UserEmailService.initiateAdminPasswordReset() to always deny access, producing a 500 for any consumer of that method. Fix: include both the role name and each privilege name when building the authority set, matching the standard Spring Security convention. Updated all AuthorityServiceTest assertions to reflect the new behaviour and added three targeted tests: - shouldIncludeRoleNamesAsAuthorities - shouldIncludeRoleNameEvenWhenRoleHasNoPrivileges - shouldDeduplicateWhenRoleNameMatchesPrivilegeName Fixes #293
1 parent e1dbea7 commit d3b1599

2 files changed

Lines changed: 81 additions & 36 deletions

File tree

src/main/java/com/digitalsanctuary/spring/user/service/AuthorityService.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package com.digitalsanctuary.spring.user.service;
22

33
import java.util.Collection;
4-
import java.util.stream.Collectors;
4+
import java.util.HashSet;
5+
import java.util.Set;
56
import org.springframework.security.core.GrantedAuthority;
67
import org.springframework.security.core.authority.SimpleGrantedAuthority;
78
import org.springframework.stereotype.Service;
@@ -39,21 +40,24 @@ public Collection<? extends GrantedAuthority> getAuthoritiesFromUser(User user)
3940
}
4041

4142
/**
43+
* Returns a collection of Spring Security's {@link GrantedAuthority} objects that includes both the role names and
44+
* the privilege names associated with the given collection of roles.
4245
*
43-
* Returns a collection of Spring Security's GrantedAuthority objects that corresponds to the privileges associated with the given collection of
44-
* roles.
46+
* <p>Including role names as authorities is required for Spring Security's {@code hasRole()} checks
47+
* (e.g., {@code @PreAuthorize("hasRole('ADMIN')")}) to work correctly.</p>
4548
*
46-
* @param roles a collection of roles whose privileges should be converted into Spring Security's GrantedAuthority objects
47-
* @return a collection of Spring Security's GrantedAuthority objects that corresponds to the privileges associated with the given collection of
48-
* roles
49+
* @param roles a collection of roles whose names and privileges should be converted into GrantedAuthority objects
50+
* @return a deduplicated set of GrantedAuthority objects containing both role names and privilege names
4951
*/
5052
public Collection<? extends GrantedAuthority> getAuthoritiesFromRoles(Collection<Role> roles) {
51-
// flatMap streams the roles, and maps each Role to its privileges (a Collection of Privilege objects).
52-
// The stream of Collection<Privilege> objects is then flattened into a single stream of Privilege objects.
53-
// Finally, each Privilege is mapped to its name as a String, wrapped in a SimpleGrantedAuthority object,
54-
// and collected into a Set of GrantedAuthority objects.
55-
return roles.stream().flatMap(role -> role.getPrivileges().stream()).map(Privilege::getName).map(SimpleGrantedAuthority::new)
56-
.collect(Collectors.toSet());
53+
Set<GrantedAuthority> authorities = new HashSet<>();
54+
for (Role role : roles) {
55+
authorities.add(new SimpleGrantedAuthority(role.getName()));
56+
for (Privilege privilege : role.getPrivileges()) {
57+
authorities.add(new SimpleGrantedAuthority(privilege.getName()));
58+
}
59+
}
60+
return authorities;
5761
}
5862

5963
}

src/test/java/com/digitalsanctuary/spring/user/service/AuthorityServiceTest.java

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,11 @@ void getAuthoritiesFromUser_userWithRoles_returnsCorrectAuthorities() {
110110
// When
111111
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromUser(testUser);
112112

113-
// Then
113+
// Then - includes both role names and privilege names
114114
assertThat(authorities)
115-
.hasSize(2)
115+
.hasSize(4)
116116
.extracting(GrantedAuthority::getAuthority)
117-
.containsExactlyInAnyOrder("READ_PRIVILEGE", "WRITE_PRIVILEGE");
117+
.containsExactlyInAnyOrder("ROLE_USER", "ROLE_ADMIN", "READ_PRIVILEGE", "WRITE_PRIVILEGE");
118118
}
119119

120120
// Tests for getAuthoritiesFromRoles
@@ -145,11 +145,11 @@ void getAuthoritiesFromRoles_singleRoleSinglePrivilege_returnsOneAuthority() {
145145
// When
146146
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromRoles(roles);
147147

148-
// Then
148+
// Then - includes role name and privilege name
149149
assertThat(authorities)
150-
.hasSize(1)
150+
.hasSize(2)
151151
.extracting(GrantedAuthority::getAuthority)
152-
.containsExactly("READ_PRIVILEGE");
152+
.containsExactlyInAnyOrder("ROLE_USER", "READ_PRIVILEGE");
153153
}
154154

155155
@Test
@@ -161,11 +161,11 @@ void getAuthoritiesFromRoles_singleRoleMultiplePrivileges_returnsMultipleAuthori
161161
// When
162162
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromRoles(roles);
163163

164-
// Then
164+
// Then - includes role name and both privilege names
165165
assertThat(authorities)
166-
.hasSize(2)
166+
.hasSize(3)
167167
.extracting(GrantedAuthority::getAuthority)
168-
.containsExactlyInAnyOrder("READ_PRIVILEGE", "WRITE_PRIVILEGE");
168+
.containsExactlyInAnyOrder("ROLE_ADMIN", "READ_PRIVILEGE", "WRITE_PRIVILEGE");
169169
}
170170

171171
@Test
@@ -177,16 +177,16 @@ void getAuthoritiesFromRoles_multipleRolesWithOverlappingPrivileges_deduplicates
177177
// When
178178
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromRoles(roles);
179179

180-
// Then - READ_PRIVILEGE appears in both roles but should only appear once
180+
// Then - READ_PRIVILEGE appears in both roles but should only appear once; role names included
181181
assertThat(authorities)
182-
.hasSize(2)
182+
.hasSize(4)
183183
.extracting(GrantedAuthority::getAuthority)
184-
.containsExactlyInAnyOrder("READ_PRIVILEGE", "WRITE_PRIVILEGE");
184+
.containsExactlyInAnyOrder("ROLE_USER", "ROLE_ADMIN", "READ_PRIVILEGE", "WRITE_PRIVILEGE");
185185
}
186186

187187
@Test
188-
@DisplayName("Should handle role with empty privileges")
189-
void getAuthoritiesFromRoles_roleWithEmptyPrivileges_returnsEmptyAuthorities() {
188+
@DisplayName("Should include role name even when role has no privileges")
189+
void getAuthoritiesFromRoles_roleWithEmptyPrivileges_returnsRoleNameOnly() {
190190
// Given
191191
Role emptyRole = RoleTestDataBuilder.aRole()
192192
.withName("ROLE_EMPTY")
@@ -197,8 +197,11 @@ void getAuthoritiesFromRoles_roleWithEmptyPrivileges_returnsEmptyAuthorities() {
197197
// When
198198
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromRoles(roles);
199199

200-
// Then
201-
assertThat(authorities).isEmpty();
200+
// Then - role name is still included even with no privileges
201+
assertThat(authorities)
202+
.hasSize(1)
203+
.extracting(GrantedAuthority::getAuthority)
204+
.containsExactly("ROLE_EMPTY");
202205
}
203206

204207
@Test
@@ -275,11 +278,11 @@ void getAuthoritiesFromRoles_mixedCasePrivileges_preservesCase() {
275278
// When
276279
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromRoles(roles);
277280

278-
// Then
281+
// Then - includes role name and all privilege names with case preserved
279282
assertThat(authorities)
280-
.hasSize(3)
283+
.hasSize(4)
281284
.extracting(GrantedAuthority::getAuthority)
282-
.containsExactlyInAnyOrder("read_privilege", "WRITE_PRIVILEGE", "Delete_Privilege");
285+
.containsExactlyInAnyOrder("ROLE_TEST", "read_privilege", "WRITE_PRIVILEGE", "Delete_Privilege");
283286
}
284287

285288
@Test
@@ -303,8 +306,8 @@ void getAuthoritiesFromRoles_largeNumberOfPrivileges_handlesEfficiently() {
303306
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromRoles(roles);
304307
long endTime = System.currentTimeMillis();
305308

306-
// Then
307-
assertThat(authorities).hasSize(1000);
309+
// Then - 1000 privileges + 1 role name
310+
assertThat(authorities).hasSize(1001);
308311
assertThat(endTime - startTime).isLessThan(100); // Should complete in less than 100ms
309312
}
310313

@@ -338,10 +341,48 @@ void getAuthoritiesFromRoles_complexOverlap_correctlyDeduplicates() {
338341
// When
339342
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromRoles(roles);
340343

341-
// Then
344+
// Then - 3 role names + 4 unique privilege names
345+
assertThat(authorities)
346+
.hasSize(7)
347+
.extracting(GrantedAuthority::getAuthority)
348+
.containsExactlyInAnyOrder("ROLE_1", "ROLE_2", "ROLE_3", "COMMON_PRIVILEGE", "UNIQUE_1", "UNIQUE_2", "UNIQUE_3");
349+
}
350+
351+
@Test
352+
@DisplayName("Should include role names as authorities alongside privileges")
353+
void shouldIncludeRoleNamesAsAuthorities() {
354+
// Given
355+
List<Role> roles = Arrays.asList(adminRole);
356+
357+
// When
358+
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromRoles(roles);
359+
360+
// Then - ROLE_ADMIN is present as an authority (needed for hasRole('ADMIN'))
361+
assertThat(authorities)
362+
.extracting(GrantedAuthority::getAuthority)
363+
.contains("ROLE_ADMIN");
364+
}
365+
366+
@Test
367+
@DisplayName("Should deduplicate when role name matches a privilege name")
368+
void shouldDeduplicateWhenRoleNameMatchesPrivilegeName() {
369+
// Given - a role whose name also appears as a privilege name
370+
Privilege roleNamePrivilege = new Privilege("ROLE_SPECIAL");
371+
roleNamePrivilege.setId(10L);
372+
373+
Role specialRole = RoleTestDataBuilder.aRole()
374+
.withName("ROLE_SPECIAL")
375+
.withPrivilege(roleNamePrivilege)
376+
.build();
377+
List<Role> roles = Arrays.asList(specialRole);
378+
379+
// When
380+
Collection<? extends GrantedAuthority> authorities = authorityService.getAuthoritiesFromRoles(roles);
381+
382+
// Then - ROLE_SPECIAL appears only once despite being both a role name and privilege name
342383
assertThat(authorities)
343-
.hasSize(4) // COMMON_PRIVILEGE, UNIQUE_1, UNIQUE_2, UNIQUE_3
384+
.hasSize(1)
344385
.extracting(GrantedAuthority::getAuthority)
345-
.containsExactlyInAnyOrder("COMMON_PRIVILEGE", "UNIQUE_1", "UNIQUE_2", "UNIQUE_3");
386+
.containsExactly("ROLE_SPECIAL");
346387
}
347388
}

0 commit comments

Comments
 (0)