From 7bbe7db80b76e614c445132842ebabca404ef3ff Mon Sep 17 00:00:00 2001 From: Gary Roybal Date: Fri, 4 Aug 2023 16:11:10 -0700 Subject: [PATCH] UPSE-387: Update portlets.json REST endpoint to allow filtering by user's portlet permission and favorite/unfavorite (#2689) --- .../portal/rest/PortletsRESTController.java | 120 +++++-- .../rest/PortletsRESTControllerTest.java | 311 ++++++++++++++++++ 2 files changed, 406 insertions(+), 25 deletions(-) create mode 100644 uPortal-api/uPortal-api-rest/src/test/java/org/apereo/portal/rest/PortletsRESTControllerTest.java diff --git a/uPortal-api/uPortal-api-rest/src/main/java/org/apereo/portal/rest/PortletsRESTController.java b/uPortal-api/uPortal-api-rest/src/main/java/org/apereo/portal/rest/PortletsRESTController.java index 871080582d1..6eaf763240e 100644 --- a/uPortal-api/uPortal-api-rest/src/main/java/org/apereo/portal/rest/PortletsRESTController.java +++ b/uPortal-api/uPortal-api-rest/src/main/java/org/apereo/portal/rest/PortletsRESTController.java @@ -15,14 +15,19 @@ package org.apereo.portal.rest; import java.io.Serializable; -import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.apereo.portal.EntityIdentifier; +import org.apereo.portal.UserPreferencesManager; +import org.apereo.portal.layout.IUserLayout; +import org.apereo.portal.layout.IUserLayoutManager; import org.apereo.portal.layout.LayoutPortlet; import org.apereo.portal.portlet.om.IPortletDefinition; import org.apereo.portal.portlet.om.IPortletWindow; @@ -31,18 +36,20 @@ import org.apereo.portal.portlet.registry.IPortletDefinitionRegistry; import org.apereo.portal.portlet.registry.IPortletWindowRegistry; import org.apereo.portal.portlet.rendering.IPortletExecutionManager; +import org.apereo.portal.portlets.favorites.FavoritesUtils; import org.apereo.portal.security.IAuthorizationPrincipal; +import org.apereo.portal.security.IAuthorizationService; import org.apereo.portal.security.IPerson; import org.apereo.portal.security.IPersonManager; -import org.apereo.portal.services.AuthorizationServiceFacade; import org.apereo.portal.url.PortalHttpServletFactoryService; +import org.apereo.portal.user.IUserInstance; +import org.apereo.portal.user.IUserInstanceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; @@ -55,6 +62,11 @@ @Controller public class PortletsRESTController { + public static final String FAVORITE_FLAG = "favorite"; + public static final String REQUIRED_PERMISSION_TYPE = "requiredPermissionType"; + + @Autowired private IAuthorizationService authorizationService; + @Autowired private IPortletDefinitionRegistry portletDefinitionRegistry; @Autowired private IPortletCategoryRegistry portletCategoryRegistry; @@ -67,35 +79,86 @@ public class PortletsRESTController { @Autowired private IPortletExecutionManager portletExecutionManager; + @Autowired private IUserInstanceManager userInstanceManager; + + @Autowired private FavoritesUtils favoritesUtils; + private final Logger logger = LoggerFactory.getLogger(getClass()); + public enum PortletPermissionType { + BROWSE, + CONFIGURE, + MANAGE, + RENDER, + SUBSCRIBE + } + /** * Provides information about all portlets in the portlet registry. NOTE: The response is * governed by the IPermission.PORTLET_MANAGER_xyz series of permissions. The * actual level of permission required is based on the current lifecycle state of the portlet. */ - @RequestMapping(value = "/portlets.json", method = RequestMethod.GET) - public ModelAndView getManageablePortlets( - HttpServletRequest request, HttpServletResponse response) throws Exception { - // get a list of all channels - List allPortlets = portletDefinitionRegistry.getAllPortletDefinitions(); - IAuthorizationPrincipal ap = getAuthorizationPrincipal(request); + @GetMapping(value = "/portlets.json") + public ModelAndView getPortlets(HttpServletRequest request) { + final boolean limitByFavoriteFlag = request.getParameter(FAVORITE_FLAG) != null; + final boolean favoriteValueToMatch = + Boolean.parseBoolean(request.getParameter(FAVORITE_FLAG)); + final String requiredPermissionTypeParameter = + request.getParameter(REQUIRED_PERMISSION_TYPE); + final PortletPermissionType requiredPermissionType = + (requiredPermissionTypeParameter == null) + ? PortletPermissionType.MANAGE + : PortletPermissionType.valueOf( + requiredPermissionTypeParameter.toUpperCase()); + + final Set favorites = + limitByFavoriteFlag ? getFavorites(request) : Collections.emptySet(); + final List allPortlets = + portletDefinitionRegistry.getAllPortletDefinitions(); + final IAuthorizationPrincipal ap = getAuthorizationPrincipal(request); + + final Predicate favoritesPredicate = + p -> !limitByFavoriteFlag || favorites.contains(p) == favoriteValueToMatch; + final Predicate permissionsPredicate = + p -> this.doesUserHavePermissionToViewPortlet(ap, p, requiredPermissionType); + final List results = + allPortlets.stream() + .filter(favoritesPredicate.and(permissionsPredicate)) + .map(PortletTuple::new) + .collect(Collectors.toList()); + return new ModelAndView("json", "portlets", results); + } - List rslt = new ArrayList(); - for (IPortletDefinition pdef : allPortlets) { - if (ap.canManage(pdef.getPortletDefinitionId().getStringId())) { - rslt.add(new PortletTuple(pdef)); - } + private boolean doesUserHavePermissionToViewPortlet( + IAuthorizationPrincipal ap, + IPortletDefinition portletDefinition, + PortletPermissionType requiredPermissionType) { + switch (requiredPermissionType) { + case BROWSE: + return this.authorizationService.canPrincipalBrowse(ap, portletDefinition); + case CONFIGURE: + return this.authorizationService.canPrincipalConfigure( + ap, portletDefinition.getPortletDefinitionId().getStringId()); + case MANAGE: + return this.authorizationService.canPrincipalManage( + ap, portletDefinition.getPortletDefinitionId().getStringId()); + case RENDER: + return this.authorizationService.canPrincipalRender( + ap, portletDefinition.getPortletDefinitionId().getStringId()); + case SUBSCRIBE: + return this.authorizationService.canPrincipalSubscribe( + ap, portletDefinition.getPortletDefinitionId().getStringId()); + default: + throw new IllegalArgumentException( + "Unknown requiredPermissionType: " + requiredPermissionType); } - - return new ModelAndView("json", "portlets", rslt); } /** - * Provides information about a single portlet in the registry. NOTE: Access to this API enpoint - * requires only IPermission.PORTAL_SUBSCRIBE permission. + * Provides information about a single portlet in the registry. NOTE: Access to this API + * endpoint requires only IPermission.PORTAL_SUBSCRIBE permission. */ - @RequestMapping(value = "/portlet/{fname}.json", method = RequestMethod.GET) + @GetMapping(value = "/portlet/{fname}.json") public ModelAndView getPortlet( HttpServletRequest request, HttpServletResponse response, @PathVariable String fname) throws Exception { @@ -112,10 +175,10 @@ public ModelAndView getPortlet( } /** - * Provides a single, fully-rendered portlet. NOTE: Access to this API enpoint requires only + * Provides a single, fully-rendered portlet. NOTE: Access to this API endpoint requires only * IPermission.PORTAL_SUBSCRIBE permission. */ - @RequestMapping(value = "/v4-3/portlet/{fname}.html", method = RequestMethod.GET) + @GetMapping(value = "/v4-3/portlet/{fname}.html") public @ResponseBody String getRenderedPortlet( HttpServletRequest req, HttpServletResponse res, @PathVariable String fname) { @@ -163,8 +226,7 @@ public ModelAndView getPortlet( private IAuthorizationPrincipal getAuthorizationPrincipal(HttpServletRequest req) { IPerson user = personManager.getPerson(req); EntityIdentifier ei = user.getEntityIdentifier(); - IAuthorizationPrincipal rslt = - AuthorizationServiceFacade.instance().newPrincipal(ei.getKey(), ei.getType()); + IAuthorizationPrincipal rslt = authorizationService.newPrincipal(ei.getKey(), ei.getType()); return rslt; } @@ -177,12 +239,20 @@ private Set getPortletCategories(IPortletDefinition pdef) { return rslt; } + private Set getFavorites(HttpServletRequest request) { + final IUserInstance ui = userInstanceManager.getUserInstance(request); + final UserPreferencesManager upm = (UserPreferencesManager) ui.getPreferencesManager(); + final IUserLayoutManager ulm = upm.getUserLayoutManager(); + final IUserLayout layout = ulm.getUserLayout(); + return favoritesUtils.getFavoritePortletDefinitions(layout); + } + /* * Nested Types */ @SuppressWarnings("unused") - private /* non-static */ final class PortletTuple implements Serializable { + protected /* non-static */ final class PortletTuple implements Serializable { private static final long serialVersionUID = 1L; diff --git a/uPortal-api/uPortal-api-rest/src/test/java/org/apereo/portal/rest/PortletsRESTControllerTest.java b/uPortal-api/uPortal-api-rest/src/test/java/org/apereo/portal/rest/PortletsRESTControllerTest.java new file mode 100644 index 00000000000..808a18d78a5 --- /dev/null +++ b/uPortal-api/uPortal-api-rest/src/test/java/org/apereo/portal/rest/PortletsRESTControllerTest.java @@ -0,0 +1,311 @@ +package org.apereo.portal.rest; + +import static org.apereo.portal.rest.PortletsRESTController.PortletPermissionType.BROWSE; +import static org.apereo.portal.rest.PortletsRESTController.PortletPermissionType.CONFIGURE; +import static org.apereo.portal.rest.PortletsRESTController.PortletPermissionType.MANAGE; +import static org.apereo.portal.rest.PortletsRESTController.PortletPermissionType.RENDER; +import static org.apereo.portal.rest.PortletsRESTController.PortletPermissionType.SUBSCRIBE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.BDDMockito.given; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import org.apereo.portal.EntityIdentifier; +import org.apereo.portal.UserPreferencesManager; +import org.apereo.portal.layout.IUserLayout; +import org.apereo.portal.layout.IUserLayoutManager; +import org.apereo.portal.portlet.dao.jpa.PortletTypeImpl; +import org.apereo.portal.portlet.om.IPortletDefinition; +import org.apereo.portal.portlet.om.IPortletDefinitionId; +import org.apereo.portal.portlet.om.IPortletType; +import org.apereo.portal.portlet.om.PortletLifecycleState; +import org.apereo.portal.portlet.registry.IPortletCategoryRegistry; +import org.apereo.portal.portlet.registry.IPortletDefinitionRegistry; +import org.apereo.portal.portlets.favorites.FavoritesUtils; +import org.apereo.portal.security.IAuthorizationPrincipal; +import org.apereo.portal.security.IAuthorizationService; +import org.apereo.portal.security.IPerson; +import org.apereo.portal.security.IPersonManager; +import org.apereo.portal.user.IUserInstance; +import org.apereo.portal.user.IUserInstanceManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.web.servlet.ModelAndView; + +@RunWith(MockitoJUnitRunner.class) +public class PortletsRESTControllerTest { + + @InjectMocks private PortletsRESTController portletsRESTController; + + @Mock private IAuthorizationService authorizationService; + @Mock private IAuthorizationPrincipal authorizationPrincipal; + @Mock private IPortletCategoryRegistry portletCategoryRegistry; + @Mock private IPortletDefinitionRegistry portletDefinitionRegistry; + @Mock private IPerson user; + @Mock private IPersonManager personManager; + @Mock private EntityIdentifier userEntityIdentifier; + @Mock private FavoritesUtils favoritesUtils; + @Mock private UserPreferencesManager userPreferencesManager; + @Mock private IUserInstanceManager userInstanceManager; + @Mock private IUserLayoutManager userLayoutManager; + + @Mock private HttpServletRequest request; + @Mock private IPortletDefinition portletDefinition1, portletDefinition2, portletDefinition3; + @Mock private IPortletType portletType; + @Mock private IUserInstance userInstance; + @Mock private IUserLayout userLayout; + + private static final String entityIdentifierKey = "entityIdentifierKey"; + private static final long portletDefinitionId1Long = 1L; + private static final long portletDefinitionId2Long = 2L; + private static final long portletDefinitionId3Long = 3L; + private static final IPortletDefinitionId portletDefinitionId1 = + new PortletDefinitionId(portletDefinitionId1Long); + private static final IPortletDefinitionId portletDefinitionId2 = + new PortletDefinitionId(portletDefinitionId2Long); + private static final IPortletDefinitionId portletDefinitionId3 = + new PortletDefinitionId(portletDefinitionId3Long); + private List portletsFromRegistry; + + @Before + public void setUp() throws Exception { + this.portletsFromRegistry = new ArrayList<>(); + this.userEntityIdentifier = new EntityIdentifier(entityIdentifierKey, IPerson.class); + this.portletType = new PortletTypeImpl("portletType", "portletType"); + this.setupMockPortletDefinition(portletDefinition1, portletDefinitionId1); + given(this.portletDefinitionRegistry.getAllPortletDefinitions()) + .willReturn(portletsFromRegistry); + given(this.personManager.getPerson(this.request)).willReturn(this.user); + given(this.user.getEntityIdentifier()).willReturn(this.userEntityIdentifier); + given( + this.authorizationService.newPrincipal( + this.userEntityIdentifier.getKey(), + this.userEntityIdentifier.getType())) + .willReturn(this.authorizationPrincipal); + this.setupMockPortletDefinition(this.portletDefinition1, portletDefinitionId1); + this.setupMockPortletDefinition(this.portletDefinition2, portletDefinitionId2); + this.setupMockPortletDefinition(this.portletDefinition3, portletDefinitionId3); + this.givenPortletDefinitionsInRegistry( + this.portletDefinition1, this.portletDefinition2, this.portletDefinition3); + } + + @Test + public void testGetPortletsWithNoPermissionsTypeSpecifiedDefaultsToManagePermissionsType() { + this.givenUserHasPermissionForPortlets( + this.user, MANAGE, this.portletDefinition1, this.portletDefinition3); + final ModelAndView mav = this.portletsRESTController.getPortlets(request); + this.verifyPortletResults(mav, this.portletDefinition1, this.portletDefinition3); + } + + @Test + public void testGetPortletsWithManagePermissionsTypeSpecified() { + this.givenRequestSpecifiesPermissionType(MANAGE); + this.givenUserHasPermissionForPortlets( + this.user, MANAGE, this.portletDefinition1, this.portletDefinition3); + final ModelAndView mav = this.portletsRESTController.getPortlets(request); + this.verifyPortletResults(mav, this.portletDefinition1, this.portletDefinition3); + } + + @Test + public void testGetPortletsWithBrowseManagePermissionsTypeSpecified() { + this.givenRequestSpecifiesPermissionType(BROWSE); + this.givenUserHasPermissionForPortlets( + this.user, BROWSE, this.portletDefinition1, this.portletDefinition3); + final ModelAndView mav = this.portletsRESTController.getPortlets(request); + this.verifyPortletResults(mav, this.portletDefinition1, this.portletDefinition3); + } + + @Test + public void testGetPortletsWithConfigurePermissionsTypeSpecified() { + this.givenRequestSpecifiesPermissionType(CONFIGURE); + this.givenUserHasPermissionForPortlets( + this.user, CONFIGURE, this.portletDefinition1, this.portletDefinition3); + final ModelAndView mav = this.portletsRESTController.getPortlets(request); + this.verifyPortletResults(mav, this.portletDefinition1, this.portletDefinition3); + } + + @Test + public void testGetPortletsWithRenderPermissionsTypeSpecified() { + this.givenRequestSpecifiesPermissionType(RENDER); + this.givenUserHasPermissionForPortlets( + this.user, RENDER, this.portletDefinition1, this.portletDefinition3); + final ModelAndView mav = this.portletsRESTController.getPortlets(request); + this.verifyPortletResults(mav, this.portletDefinition1, this.portletDefinition3); + } + + @Test + public void testGetPortletsWithSubscribePermissionsTypeSpecified() { + this.givenRequestSpecifiesPermissionType(SUBSCRIBE); + this.givenUserHasPermissionForPortlets( + this.user, SUBSCRIBE, this.portletDefinition1, this.portletDefinition3); + final ModelAndView mav = this.portletsRESTController.getPortlets(request); + this.verifyPortletResults(mav, this.portletDefinition1, this.portletDefinition3); + } + + @Test + public void testGetPortletsWhenLimitingToFavoritedPortlets() { + this.givenRequestSpecifiesFavoriteFlag(true); + this.givenUserHasPermissionForPortlets( + this.user, + MANAGE, + this.portletDefinition1, + this.portletDefinition2, + this.portletDefinition3); + this.givenUserHasFavoritePortlets( + this.user, this.portletDefinition1, this.portletDefinition3); + final ModelAndView mav = this.portletsRESTController.getPortlets(request); + this.verifyPortletResults(mav, this.portletDefinition1, this.portletDefinition3); + } + + @Test + public void testGetPortletsWhenLimitingToNonFavoritedPortlets() { + this.givenRequestSpecifiesFavoriteFlag(false); + this.givenUserHasPermissionForPortlets( + this.user, + MANAGE, + this.portletDefinition1, + this.portletDefinition2, + this.portletDefinition3); + this.givenUserHasFavoritePortlets(this.user, this.portletDefinition1); + final ModelAndView mav = this.portletsRESTController.getPortlets(request); + this.verifyPortletResults(mav, this.portletDefinition2, this.portletDefinition3); + } + + private void setupMockPortletDefinition( + IPortletDefinition portletDefinition, IPortletDefinitionId id) { + final String stringId = id.getStringId(); + given(portletDefinition.getFName()).willReturn("fname" + stringId); + given(portletDefinition.getName()).willReturn("name" + stringId); + given(portletDefinition.getDescription()).willReturn("description" + stringId); + given(portletDefinition.getType()).willReturn(this.portletType); + given(portletDefinition.getLifecycleState()).willReturn(PortletLifecycleState.PUBLISHED); + given(portletDefinition.getPortletDefinitionId()).willReturn(id); + } + + private void givenRequestSpecifiesPermissionType( + PortletsRESTController.PortletPermissionType permissionType) { + given(this.request.getParameter(PortletsRESTController.REQUIRED_PERMISSION_TYPE)) + .willReturn(permissionType.toString()); + } + + private void givenRequestSpecifiesFavoriteFlag(boolean flagValue) { + given(this.request.getParameter(PortletsRESTController.FAVORITE_FLAG)) + .willReturn(flagValue ? "true" : "false"); + } + + private void givenPortletDefinitionsInRegistry(IPortletDefinition... portletDefinitions) { + portletsFromRegistry.addAll(Arrays.asList(portletDefinitions)); + } + + private void givenUserHasPermissionForPortlets( + IPerson user, + PortletsRESTController.PortletPermissionType permissionType, + IPortletDefinition... portletDefinitions) { + for (IPortletDefinition portletDefinition : portletDefinitions) { + final String portletDefinitionStringId = + portletDefinition.getPortletDefinitionId().getStringId(); + switch (permissionType) { + case BROWSE: + given( + this.authorizationService.canPrincipalBrowse( + this.authorizationPrincipal, portletDefinition)) + .willReturn(true); + break; + case CONFIGURE: + given( + this.authorizationService.canPrincipalConfigure( + this.authorizationPrincipal, portletDefinitionStringId)) + .willReturn(true); + break; + case MANAGE: + given( + this.authorizationService.canPrincipalManage( + this.authorizationPrincipal, portletDefinitionStringId)) + .willReturn(true); + break; + case SUBSCRIBE: + given( + this.authorizationService.canPrincipalSubscribe( + this.authorizationPrincipal, portletDefinitionStringId)) + .willReturn(true); + break; + case RENDER: + given( + this.authorizationService.canPrincipalRender( + this.authorizationPrincipal, portletDefinitionStringId)) + .willReturn(true); + break; + default: + throw new IllegalArgumentException( + "Unknown permission type: " + permissionType); + } + } + } + + private void givenUserHasFavoritePortlets( + IPerson user, IPortletDefinition... portletDefinitions) { + final Set favoritePortlets = new HashSet<>(); + for (IPortletDefinition portletDefinition : portletDefinitions) { + favoritePortlets.add(portletDefinition); + } + given(this.userInstanceManager.getUserInstance(this.request)).willReturn(this.userInstance); + given(this.userInstance.getPreferencesManager()).willReturn(this.userPreferencesManager); + given(this.userPreferencesManager.getUserLayoutManager()) + .willReturn(this.userLayoutManager); + given(this.userLayoutManager.getUserLayout()).willReturn(this.userLayout); + given(this.favoritesUtils.getFavoritePortletDefinitions(this.userLayout)) + .willReturn(favoritePortlets); + } + + private void verifyPortletResults(ModelAndView mav, IPortletDefinition... expectedPortlets) { + assertNotNull(mav); + assertEquals("json", mav.getViewName()); + final Map model = mav.getModel(); + assertNotNull(model); + final List portlets = + (List) model.get("portlets"); + assertNotNull(portlets); + assertEquals(expectedPortlets.length, portlets.size()); + for (int i = 0; i < expectedPortlets.length; i++) { + final IPortletDefinition expectedPortlet = expectedPortlets[i]; + final PortletsRESTController.PortletTuple portletTuple = portlets.get(i); + assertEquals(expectedPortlet.getFName(), portletTuple.getFname()); + assertEquals(expectedPortlet.getName(), portletTuple.getName()); + assertEquals(expectedPortlet.getDescription(), portletTuple.getDescription()); + assertEquals(expectedPortlet.getType().getName(), portletTuple.getType()); + assertEquals( + expectedPortlet.getLifecycleState().toString(), + portletTuple.getLifecycleState()); + assertEquals( + expectedPortlet.getPortletDefinitionId().getStringId(), portletTuple.getId()); + } + } + + static class PortletDefinitionId implements IPortletDefinitionId { + private long id; + + public PortletDefinitionId(long id) { + this.id = id; + } + + @Override + public long getLongId() { + return id; + } + + @Override + public String getStringId() { + return String.valueOf(id); + } + } +}