Skip to content

Commit 2752d46

Browse files
committed
Update role impersonation perm checking - more like it used to be
1 parent c093d2c commit 2752d46

3 files changed

Lines changed: 74 additions & 53 deletions

File tree

api/src/org/labkey/api/security/impersonation/ImpersonationTest.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import org.labkey.api.security.SecurityPolicyManager;
1313
import org.labkey.api.security.User;
1414
import org.labkey.api.security.UserManager;
15-
import org.labkey.api.security.UserPrincipal;
1615
import org.labkey.api.security.ValidEmail;
1716
import org.labkey.api.security.ValidEmail.InvalidEmailException;
1817
import org.labkey.api.security.permissions.ReadPermission;
@@ -133,21 +132,20 @@ private void testImpersonator(String roleName, boolean siteRole, Container proje
133132
SecurityPolicyManager.savePolicyForTests(rootPolicy, TestContext.get().getUser());
134133

135134
// Test impersonating at the site level
136-
// TODO: Active vs. all?
137135
Collection<User> validUsers = UserImpersonationContextFactory.getValidImpersonationUsers(null, user);
138136
assertEquals(expectedSiteUsers, validUsers.size());
139137
assertTrue(user + " is able to impersonate themselves!", validUsers.stream().noneMatch(u -> u.equals(user)));
140138
Collection<Group> validSiteGroups = GroupImpersonationContextFactory.getValidImpersonationGroups(root, user);
141139
assertEquals(expectedSiteGroups, validSiteGroups.size());
142-
Collection<Role> validSiteRoles = RoleImpersonationContextFactory.filterImpersonationRoles(null, user, RoleManager.getAllRoles());
140+
Collection<Role> validSiteRoles = RoleImpersonationContextFactory.getValidImpersonationRoles(ContainerManager.getRoot(), user).toList();
143141
assertEquals(expectedSiteRoles, validSiteRoles.size());
144142

145143
// Test impersonating at the project level
146144
Collection<User> projectUsers = UserImpersonationContextFactory.getValidImpersonationUsers(project, user);
147145
assertEquals(expectedProjectUsers, projectUsers.size());
148146
Collection<Group> validProjectGroups = GroupImpersonationContextFactory.getValidImpersonationGroups(project, user);
149147
assertEquals(expectedProjectGroups, validProjectGroups.size());
150-
Collection<Role> validProjectRoles = RoleImpersonationContextFactory.filterImpersonationRoles(project, user, RoleManager.getAllRoles());
148+
Collection<Role> validProjectRoles = RoleImpersonationContextFactory.getValidImpersonationRoles(project, user).toList();
151149
assertEquals(expectedProjectRoles, validProjectRoles.size());
152150
}
153151
finally

api/src/org/labkey/api/security/impersonation/RoleImpersonationContextFactory.java

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.fasterxml.jackson.annotation.JsonIgnore;
2020
import com.fasterxml.jackson.annotation.JsonProperty;
2121
import jakarta.servlet.http.HttpServletRequest;
22+
import org.jetbrains.annotations.NotNull;
2223
import org.jetbrains.annotations.Nullable;
2324
import org.labkey.api.audit.AuditLogService;
2425
import org.labkey.api.data.Container;
@@ -35,13 +36,15 @@
3536
import org.labkey.api.security.permissions.ImpersonatePrivilegedSiteRolesPermission;
3637
import org.labkey.api.security.roles.AbstractRootContainerRole;
3738
import org.labkey.api.security.roles.Role;
39+
import org.labkey.api.security.roles.RoleManager;
3840
import org.labkey.api.util.GUID;
41+
import org.labkey.api.util.StringUtilsLabKey;
3942
import org.labkey.api.view.ActionURL;
4043
import org.labkey.api.view.NavTree;
4144
import org.labkey.api.view.ViewContext;
4245

4346
import java.util.Collection;
44-
import java.util.Collections;
47+
import java.util.List;
4548
import java.util.Objects;
4649
import java.util.Set;
4750
import java.util.stream.Collectors;
@@ -180,35 +183,72 @@ private static void addMenu(NavTree menu, String text)
180183
menu.addChild(newRoleMenu);
181184
}
182185

183-
// Returns a collection of roles that this user is allowed to impersonate in this project (or root). Empty if user
184-
// can't impersonate these roles (or maybe at all). We always check "applicability" at the project or root level,
185-
// never folder, because when AuthFilter calls this on every impersonated request, it has no idea what the current
186-
// folder is. All it has is the project stashed in the factory that's in session. That means admins can't
187-
// impersonate roles that are applicable only in a folder (e.g., due to folder types).
188-
public static Collection<Role> filterImpersonationRoles(@Nullable Container project, User adminUser, Collection<Role> candidates)
186+
// Can this user impersonate in this container?
187+
private static boolean canImpersonate(@Nullable Container c, User user)
189188
{
190-
boolean canImpersonate = adminUser.hasRootPermission(ImpersonatePermission.class);
189+
return user.hasRootPermission(ImpersonatePermission.class) || (c != null && !c.isRoot() && c.hasPermission(user, ImpersonatePermission.class));
190+
}
191191

192-
if (!canImpersonate && project != null)
192+
public static Stream<Role> getValidImpersonationRoles(@NotNull Container c, User user)
193+
{
194+
if (canImpersonate(c, user))
193195
{
194-
canImpersonate = project.hasPermission(adminUser, ImpersonatePermission.class);
196+
SecurityPolicy policy = SecurityPolicyManager.getPolicy(c);
197+
boolean canImpersonatePrivilegedRoles = user.hasRootPermission(ImpersonatePrivilegedSiteRolesPermission.class);
198+
199+
// Stream the valid roles
200+
return RoleManager.getAllRoles().stream()
201+
.filter(Role::isAssignable)
202+
.filter(role -> role.isApplicable(policy, c))
203+
.filter(role -> !role.isPrivileged() || canImpersonatePrivilegedRoles);
195204
}
196-
197-
if (!canImpersonate)
205+
else
198206
{
199-
return Collections.emptyList();
207+
return Stream.empty();
200208
}
209+
}
201210

202-
Container c = project != null ? project : ContainerManager.getRoot();
203-
boolean canImpersonatePrivilegedRoles = adminUser.hasRootPermission(ImpersonatePrivilegedSiteRolesPermission.class);
204-
SecurityPolicy policy = SecurityPolicyManager.getPolicy(c);
205-
206-
// Stream the valid roles
207-
return candidates.stream()
208-
.filter(Role::isAssignable)
209-
.filter(role -> role.isApplicable(policy, c))
210-
.filter(role -> !role.isPrivileged() || canImpersonatePrivilegedRoles)
211-
.toList();
211+
// Throws if user is not authorized to impersonate all roles
212+
private static void verifyPermissions(@Nullable Container project, User adminUser, Set<Role> roles, ImpersonationContextFactory factory)
213+
{
214+
if (canImpersonate(project, adminUser))
215+
{
216+
// null project means a root admin is impersonating. Impersonation could have started in the root, project, or folder... we don't know.
217+
if (null == project)
218+
{
219+
// Ensure we have either site roles or project roles, not both. UI prevents this, but crafty admin could
220+
// attempt it by crafting a post with specific class names
221+
if (roles.stream()
222+
.collect(Collectors.groupingBy(role -> role instanceof AbstractRootContainerRole))
223+
.size() > 1)
224+
{
225+
throw new UnauthorizedImpersonationException("You are not allowed to impersonate site roles and project roles at the same time", factory);
226+
}
227+
228+
if (!adminUser.hasRootPermission(ImpersonatePrivilegedSiteRolesPermission.class))
229+
{
230+
// Application Administrator is not allowed to impersonate privileged roles
231+
List<String> privileged = roles.stream()
232+
.filter(Role::isPrivileged)
233+
.map(Role::getDisplayName)
234+
.toList();
235+
236+
if (!privileged.isEmpty())
237+
throw new UnauthorizedImpersonationException("You are not allowed to impersonate " + StringUtilsLabKey.joinWithConjunction(privileged, "or"), factory);
238+
}
239+
}
240+
else
241+
{
242+
// Must not be impersonating any site roles
243+
if (roles.stream().anyMatch(role -> (role instanceof AbstractRootContainerRole)))
244+
throw new UnauthorizedImpersonationException("You are not allowed to impersonate site roles", factory);
245+
}
246+
}
247+
else
248+
{
249+
// Admin's permissions must have been revoked since impersonation began
250+
throw new UnauthorizedImpersonationException("You are not allowed to impersonate here", factory);
251+
}
212252
}
213253

214254
public static class RoleImpersonationContext extends AbstractImpersonationContext
@@ -229,35 +269,18 @@ private RoleImpersonationContext(
229269
}
230270

231271
private RoleImpersonationContext(
232-
@Nullable Container project,
233-
User adminUser,
234-
RoleSet roles,
235-
ActionURL returnUrl,
236-
ImpersonationContextFactory factory,
237-
String cacheKey)
272+
@Nullable Container project,
273+
User adminUser,
274+
RoleSet roles,
275+
ActionURL returnUrl,
276+
ImpersonationContextFactory factory,
277+
String cacheKey
278+
)
238279
{
239280
super(adminUser, project, returnUrl, factory);
240281
_roles = roles;
241282
_cacheKey = cacheKey;
242-
verifyPermissions(project, adminUser, _roles.getRoles());
243-
}
244-
245-
// Throws if user is not authorized to impersonate all roles
246-
private void verifyPermissions(@Nullable Container project, User adminUser, Set<Role> roles)
247-
{
248-
// Ensure we have either site roles or project roles, not both. UI prevents this, but crafty admin could
249-
// attempt it by crafting a post with specific class names
250-
if (roles.stream()
251-
.collect(Collectors.groupingBy(role -> role instanceof AbstractRootContainerRole))
252-
.size() > 1)
253-
{
254-
throw new UnauthorizedImpersonationException("You are not allowed to impersonate site roles and project roles at the same time", getFactory());
255-
}
256-
257-
Collection<Role> filteredRoles = filterImpersonationRoles(project, adminUser, roles);
258-
259-
if (filteredRoles.size() != roles.size())
260-
throw new UnauthorizedImpersonationException("One or more impersonation roles are not authorized", getFactory());
283+
verifyPermissions(project, adminUser, _roles.getRoles(), factory);
261284
}
262285

263286
@Override

core/src/org/labkey/core/user/UserController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3009,7 +3009,7 @@ public ApiResponse execute(Object object, BindException errors)
30093009

30103010
User user = context.isImpersonating() ? context.getAdminUser() : getUser();
30113011
ApiSimpleResponse response = new ApiSimpleResponse();
3012-
Collection<Map<String, Object>> responseRoles = RoleImpersonationContextFactory.filterImpersonationRoles(getContainer(), user, RoleManager.getAllRoles()).stream()
3012+
Collection<Map<String, Object>> responseRoles = RoleImpersonationContextFactory.getValidImpersonationRoles(getContainer(), user)
30133013
.map(role -> {
30143014
Map<String, Object> map = new HashMap<>();
30153015
map.put("displayName", role.getDisplayName());

0 commit comments

Comments
 (0)