1919import com .fasterxml .jackson .annotation .JsonIgnore ;
2020import com .fasterxml .jackson .annotation .JsonProperty ;
2121import jakarta .servlet .http .HttpServletRequest ;
22+ import org .jetbrains .annotations .NotNull ;
2223import org .jetbrains .annotations .Nullable ;
2324import org .labkey .api .audit .AuditLogService ;
2425import org .labkey .api .data .Container ;
3536import org .labkey .api .security .permissions .ImpersonatePrivilegedSiteRolesPermission ;
3637import org .labkey .api .security .roles .AbstractRootContainerRole ;
3738import org .labkey .api .security .roles .Role ;
39+ import org .labkey .api .security .roles .RoleManager ;
3840import org .labkey .api .util .GUID ;
41+ import org .labkey .api .util .StringUtilsLabKey ;
3942import org .labkey .api .view .ActionURL ;
4043import org .labkey .api .view .NavTree ;
4144import org .labkey .api .view .ViewContext ;
4245
4346import java .util .Collection ;
44- import java .util .Collections ;
47+ import java .util .List ;
4548import java .util .Objects ;
4649import java .util .Set ;
4750import 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
0 commit comments