Skip to content

Commit be0ad67

Browse files
committed
Make RoleHierarchyImpl internals a bit simpler.
Issue: gh-7035
1 parent d3eaef6 commit be0ad67

File tree

1 file changed

+93
-105
lines changed

1 file changed

+93
-105
lines changed

core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java

+93-105
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,9 +15,6 @@
1515
*/
1616
package org.springframework.security.access.hierarchicalroles;
1717

18-
import java.io.BufferedReader;
19-
import java.io.IOException;
20-
import java.io.StringReader;
2118
import java.util.ArrayList;
2219
import java.util.Collection;
2320
import java.util.HashMap;
@@ -34,61 +31,68 @@
3431

3532
/**
3633
* <p>
37-
* This class defines a role hierarchy for use with the UserDetailsServiceWrapper.
34+
* This class defines a role hierarchy for use with various access checking components.
3835
*
3936
* <p>
40-
* Here is an example configuration of a role hierarchy (hint: read the "&gt;" sign as
41-
* "includes"):
37+
* Here is an example configuration of a role hierarchy (hint: read the "&gt;" sign as "includes"):
4238
*
4339
* <pre>
44-
* &lt;property name="hierarchy"&gt;
45-
* &lt;value&gt;
46-
* ROLE_A &gt; ROLE_B
47-
* ROLE_B &gt; ROLE_AUTHENTICATED
48-
* ROLE_AUTHENTICATED &gt; ROLE_UNAUTHENTICATED
49-
* &lt;/value&gt;
50-
* &lt;/property&gt;
40+
* &lt;property name="hierarchy"&gt;
41+
* &lt;value&gt;
42+
* ROLE_A &gt; ROLE_B
43+
* ROLE_B &gt; ROLE_AUTHENTICATED
44+
* ROLE_AUTHENTICATED &gt; ROLE_UNAUTHENTICATED
45+
* &lt;/value&gt;
46+
* &lt;/property&gt;
5147
* </pre>
5248
*
5349
* <p>
54-
* Explanation of the above:<br>
55-
* In effect every user with ROLE_A also has ROLE_B, ROLE_AUTHENTICATED and
56-
* ROLE_UNAUTHENTICATED;<br>
57-
* every user with ROLE_B also has ROLE_AUTHENTICATED and ROLE_UNAUTHENTICATED;<br>
58-
* every user with ROLE_AUTHENTICATED also has ROLE_UNAUTHENTICATED.
50+
* Explanation of the above:
51+
* <ul>
52+
* <li>In effect every user with ROLE_A also has ROLE_B, ROLE_AUTHENTICATED and ROLE_UNAUTHENTICATED;</li>
53+
* <li>every user with ROLE_B also has ROLE_AUTHENTICATED and ROLE_UNAUTHENTICATED;</li>
54+
* <li>every user with ROLE_AUTHENTICATED also has ROLE_UNAUTHENTICATED.</li>
55+
* </ul>
5956
*
6057
* <p>
61-
* Hierarchical Roles will dramatically shorten your access rules (and also make the
62-
* access rules much more elegant).
58+
* Hierarchical Roles will dramatically shorten your access rules (and also make the access rules
59+
* much more elegant).
6360
*
6461
* <p>
65-
* Consider this access rule for Spring Security's RoleVoter (background: every user that
66-
* is authenticated should be able to log out):<br>
67-
* /logout.html=ROLE_A,ROLE_B,ROLE_AUTHENTICATED<br>
68-
* With hierarchical roles this can now be shortened to:<br>
69-
* /logout.html=ROLE_AUTHENTICATED<br>
70-
* In addition to shorter rules this will also make your access rules more readable and
71-
* your intentions clearer.
62+
* Consider this access rule for Spring Security's RoleVoter (background: every user that is
63+
* authenticated should be able to log out):
64+
* <pre>/logout.html=ROLE_A,ROLE_B,ROLE_AUTHENTICATED</pre>
65+
*
66+
* With hierarchical roles this can now be shortened to:
67+
* <pre>/logout.html=ROLE_AUTHENTICATED</pre>
68+
*
69+
* In addition to shorter rules this will also make your access rules more readable and your
70+
* intentions clearer.
7271
*
7372
* @author Michael Mayr
7473
*/
7574
public class RoleHierarchyImpl implements RoleHierarchy {
7675

7776
private static final Log logger = LogFactory.getLog(RoleHierarchyImpl.class);
7877

78+
/**
79+
* Raw hierarchy configuration where each line represents single or multiple level role chain.
80+
*/
7981
private String roleHierarchyStringRepresentation = null;
8082

8183
/**
82-
* rolesReachableInOneStepMap is a Map that under the key of a specific role name
84+
* {@code rolesReachableInOneStepMap} is a Map that under the key of a specific role name
8385
* contains a set of all roles reachable from this role in 1 step
86+
* (i.e. parsed {@link #roleHierarchyStringRepresentation} grouped by the higher role)
8487
*/
85-
private Map<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneStepMap = null;
88+
private Map<String, Set<GrantedAuthority>> rolesReachableInOneStepMap = null;
8689

8790
/**
88-
* rolesReachableInOneOrMoreStepsMap is a Map that under the key of a specific role
91+
* {@code rolesReachableInOneOrMoreStepsMap} is a Map that under the key of a specific role
8992
* name contains a set of all roles reachable from this role in 1 or more steps
93+
* (i.e. fully resolved hierarchy from {@link #rolesReachableInOneStepMap})
9094
*/
91-
private Map<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;
95+
private Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;
9296

9397
/**
9498
* Set the role hierarchy and pre-calculate for every role the set of all reachable
@@ -102,27 +106,46 @@ public class RoleHierarchyImpl implements RoleHierarchy {
102106
public void setHierarchy(String roleHierarchyStringRepresentation) {
103107
this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;
104108

105-
logger.debug("setHierarchy() - The following role hierarchy was set: "
106-
+ roleHierarchyStringRepresentation);
109+
if (logger.isDebugEnabled()) {
110+
logger.debug("setHierarchy() - The following role hierarchy was set: "
111+
+ roleHierarchyStringRepresentation);
112+
}
107113

108114
buildRolesReachableInOneStepMap();
109115
buildRolesReachableInOneOrMoreStepsMap();
110116
}
111117

118+
@Override
112119
public Collection<GrantedAuthority> getReachableGrantedAuthorities(
113120
Collection<? extends GrantedAuthority> authorities) {
114121
if (authorities == null || authorities.isEmpty()) {
115122
return AuthorityUtils.NO_AUTHORITIES;
116123
}
117124

118125
Set<GrantedAuthority> reachableRoles = new HashSet<>();
126+
Set<String> processedNames = new HashSet<>();
119127

120128
for (GrantedAuthority authority : authorities) {
121-
addReachableRoles(reachableRoles, authority);
122-
Set<GrantedAuthority> additionalReachableRoles = getRolesReachableInOneOrMoreSteps(
123-
authority);
124-
if (additionalReachableRoles != null) {
125-
reachableRoles.addAll(additionalReachableRoles);
129+
// Do not process authorities without string representation
130+
if (authority.getAuthority() == null) {
131+
reachableRoles.add(authority);
132+
continue;
133+
}
134+
// Do not process already processed roles
135+
if (!processedNames.add(authority.getAuthority())) {
136+
continue;
137+
}
138+
// Add original authority
139+
reachableRoles.add(authority);
140+
// Add roles reachable in one or more steps
141+
Set<GrantedAuthority> lowerRoles = this.rolesReachableInOneOrMoreStepsMap.get(authority.getAuthority());
142+
if (lowerRoles == null) {
143+
continue; // No hierarchy for the role
144+
}
145+
for (GrantedAuthority role : lowerRoles) {
146+
if (processedNames.add(role.getAuthority())) {
147+
reachableRoles.add(role);
148+
}
126149
}
127150
}
128151

@@ -132,75 +155,40 @@ public Collection<GrantedAuthority> getReachableGrantedAuthorities(
132155
+ " in zero or more steps.");
133156
}
134157

135-
List<GrantedAuthority> reachableRoleList = new ArrayList<>(
136-
reachableRoles.size());
158+
List<GrantedAuthority> reachableRoleList = new ArrayList<>(reachableRoles.size());
137159
reachableRoleList.addAll(reachableRoles);
138160

139161
return reachableRoleList;
140162
}
141163

142-
// SEC-863
143-
private void addReachableRoles(Set<GrantedAuthority> reachableRoles,
144-
GrantedAuthority authority) {
145-
146-
for (GrantedAuthority testAuthority : reachableRoles) {
147-
String testKey = testAuthority.getAuthority();
148-
if ((testKey != null) && (testKey.equals(authority.getAuthority()))) {
149-
return;
150-
}
151-
}
152-
reachableRoles.add(authority);
153-
}
154-
155-
// SEC-863
156-
private Set<GrantedAuthority> getRolesReachableInOneOrMoreSteps(
157-
GrantedAuthority authority) {
158-
159-
if (authority.getAuthority() == null) {
160-
return null;
161-
}
162-
163-
for (GrantedAuthority testAuthority : this.rolesReachableInOneOrMoreStepsMap
164-
.keySet()) {
165-
String testKey = testAuthority.getAuthority();
166-
if ((testKey != null) && (testKey.equals(authority.getAuthority()))) {
167-
return this.rolesReachableInOneOrMoreStepsMap.get(testAuthority);
168-
}
169-
}
170-
171-
return null;
172-
}
173-
174164
/**
175165
* Parse input and build the map for the roles reachable in one step: the higher role
176166
* will become a key that references a set of the reachable lower roles.
177167
*/
178168
private void buildRolesReachableInOneStepMap() {
179169
this.rolesReachableInOneStepMap = new HashMap<>();
180-
try (BufferedReader bufferedReader = new BufferedReader(
181-
new StringReader(this.roleHierarchyStringRepresentation))) {
182-
for (String readLine; (readLine = bufferedReader.readLine()) != null;) {
183-
String[] roles = readLine.split(" > ");
184-
for (int i = 1; i < roles.length; i++) {
185-
GrantedAuthority higherRole = new SimpleGrantedAuthority(
186-
roles[i - 1].replaceAll("^\\s+|\\s+$", ""));
187-
GrantedAuthority lowerRole = new SimpleGrantedAuthority(roles[i].replaceAll("^\\s+|\\s+$", ""));
188-
Set<GrantedAuthority> rolesReachableInOneStepSet;
189-
if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
190-
rolesReachableInOneStepSet = new HashSet<>();
191-
this.rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
192-
} else {
193-
rolesReachableInOneStepSet = this.rolesReachableInOneStepMap.get(higherRole);
194-
}
195-
rolesReachableInOneStepSet.add(lowerRole);
196-
if (logger.isDebugEnabled()) {
197-
logger.debug("buildRolesReachableInOneStepMap() - From role " + higherRole
198-
+ " one can reach role " + lowerRole + " in one step.");
199-
}
170+
for (String line : this.roleHierarchyStringRepresentation.split("\n")) {
171+
// Split on > and trim excessive whitespace
172+
String[] roles = line.trim().split("\\s+>\\s+");
173+
174+
for (int i = 1; i < roles.length; i++) {
175+
String higherRole = roles[i - 1];
176+
GrantedAuthority lowerRole = new SimpleGrantedAuthority(roles[i]);
177+
178+
Set<GrantedAuthority> rolesReachableInOneStepSet;
179+
if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
180+
rolesReachableInOneStepSet = new HashSet<>();
181+
this.rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
182+
} else {
183+
rolesReachableInOneStepSet = this.rolesReachableInOneStepMap.get(higherRole);
184+
}
185+
rolesReachableInOneStepSet.add(lowerRole);
186+
187+
if (logger.isDebugEnabled()) {
188+
logger.debug("buildRolesReachableInOneStepMap() - From role " + higherRole
189+
+ " one can reach role " + lowerRole + " in one step.");
200190
}
201191
}
202-
} catch (IOException e) {
203-
throw new IllegalStateException(e);
204192
}
205193
}
206194

@@ -213,25 +201,25 @@ private void buildRolesReachableInOneStepMap() {
213201
private void buildRolesReachableInOneOrMoreStepsMap() {
214202
this.rolesReachableInOneOrMoreStepsMap = new HashMap<>();
215203
// iterate over all higher roles from rolesReachableInOneStepMap
216-
217-
for (GrantedAuthority role : this.rolesReachableInOneStepMap.keySet()) {
218-
Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(this.rolesReachableInOneStepMap.get(role));
204+
for (String roleName : this.rolesReachableInOneStepMap.keySet()) {
205+
Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(this.rolesReachableInOneStepMap.get(roleName));
219206
Set<GrantedAuthority> visitedRolesSet = new HashSet<>();
220207

221208
while (!rolesToVisitSet.isEmpty()) {
222209
// take a role from the rolesToVisit set
223-
GrantedAuthority aRole = rolesToVisitSet.iterator().next();
224-
rolesToVisitSet.remove(aRole);
225-
if (!visitedRolesSet.add(aRole) || !this.rolesReachableInOneStepMap.containsKey(aRole)) {
210+
GrantedAuthority lowerRole = rolesToVisitSet.iterator().next();
211+
rolesToVisitSet.remove(lowerRole);
212+
if (!visitedRolesSet.add(lowerRole) ||
213+
!this.rolesReachableInOneStepMap.containsKey(lowerRole.getAuthority())) {
226214
continue; // Already visited role or role with missing hierarchy
227-
} else if (role.equals(aRole)) {
215+
} else if (roleName.equals(lowerRole.getAuthority())) {
228216
throw new CycleInRoleHierarchyException();
229217
}
230-
rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(aRole));
218+
rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(lowerRole.getAuthority()));
231219
}
232-
this.rolesReachableInOneOrMoreStepsMap.put(role, visitedRolesSet);
220+
this.rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
233221

234-
logger.debug("buildRolesReachableInOneOrMoreStepsMap() - From role " + role
222+
logger.debug("buildRolesReachableInOneOrMoreStepsMap() - From role " + roleName
235223
+ " one can reach " + visitedRolesSet + " in one or more steps.");
236224
}
237225

0 commit comments

Comments
 (0)