From d766e2eaf6617a0112d3ef27993e1d2f7c70c1e5 Mon Sep 17 00:00:00 2001 From: Romain Bioteau Date: Tue, 3 Dec 2024 16:58:56 +0100 Subject: [PATCH] fix(perf): reduce number of queries when loading an application page (#3239) Refactor application authorization mechanism at Servlet level. Remove a search query in authorization mechanism. If for some reason, no page is found for a given page token, a 404 is displayed which seems reasonable (instead of a 403). I also replaced a search query with a get query to remove the additional count query performed by a search. Cache application by token requests to reduce the database access. We can't rely on L2 cache for this kind of query so I used the `CacheService` as a cache given `applicationToken` are unique across the system. Closes [RUNTIME-1885](https://bonitasoft.atlassian.net/browse/RUNTIME-1885) --- .../cache/ehcache/CacheConfigurationIT.java | 3 +- .../page/ApplicationAuthorizationsHelper.java | 40 ++++++++++ .../page/CustomPageAuthorizationsHelper.java | 75 ------------------- .../common/server/page/CustomPageServlet.java | 19 +++-- .../livingapps/ApplicationModelFactory.java | 25 ++----- .../LivingApplicationPageServlet.java | 9 +-- .../CustomPageAuthorizationsHelperTest.java | 58 ++------------ .../server/page/CustomPageServletTest.java | 10 +-- .../ApplicationModelFactoryTest.java | 37 +-------- .../livingapps/ApplicationModelTest.java | 12 +-- .../LivingApplicationPageServletTest.java | 16 ++-- .../application/ApplicationService.java | 5 +- .../impl/ApplicationServiceImpl.java | 33 +++++++- .../impl/ApplicationServiceImplTest.java | 7 +- .../configuration/CacheConfiguration.java | 49 ++++++++++++ 15 files changed, 176 insertions(+), 222 deletions(-) create mode 100644 bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/ApplicationAuthorizationsHelper.java delete mode 100644 bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/CustomPageAuthorizationsHelper.java create mode 100644 services/bonita-cache/src/main/java/org/bonitasoft/engine/cache/configuration/CacheConfiguration.java diff --git a/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/cache/ehcache/CacheConfigurationIT.java b/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/cache/ehcache/CacheConfigurationIT.java index 196a82b2f07..dcb955b28b5 100644 --- a/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/cache/ehcache/CacheConfigurationIT.java +++ b/bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/cache/ehcache/CacheConfigurationIT.java @@ -43,6 +43,7 @@ public void all_required_cache_configurations_should_exist() { "SYNCHRO_SERVICE_CACHE", "parameters", "DEFAULT_PLATFORM", - "CONNECTOR"); + "CONNECTOR", + "application-token"); } } diff --git a/bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/ApplicationAuthorizationsHelper.java b/bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/ApplicationAuthorizationsHelper.java new file mode 100644 index 00000000000..78a2aff9d68 --- /dev/null +++ b/bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/ApplicationAuthorizationsHelper.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2022 Bonitasoft S.A. + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ +package org.bonitasoft.console.common.server.page; + +import org.bonitasoft.engine.session.APISession; +import org.bonitasoft.livingapps.ApplicationModel; +import org.bonitasoft.livingapps.ApplicationModelFactory; + +public class ApplicationAuthorizationsHelper { + + private final APISession apiSession; + private final ApplicationModelFactory applicationFactory; + + public ApplicationAuthorizationsHelper(final APISession apiSession, + final ApplicationModelFactory applicationModelFactory) { + this.apiSession = apiSession; + this.applicationFactory = applicationModelFactory; + } + + public boolean isAuthorized(final String applicationToken) { + try { + final ApplicationModel application = applicationFactory.createApplicationModel(applicationToken); + return application.authorize(apiSession); + } catch (final Exception e) { + return false; + } + } + +} diff --git a/bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/CustomPageAuthorizationsHelper.java b/bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/CustomPageAuthorizationsHelper.java deleted file mode 100644 index 145095f2dad..00000000000 --- a/bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/CustomPageAuthorizationsHelper.java +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (C) 2022 Bonitasoft S.A. - * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble - * This library is free software; you can redistribute it and/or modify it under the terms - * of the GNU Lesser General Public License as published by the Free Software Foundation - * version 2.1 of the License. - * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * You should have received a copy of the GNU Lesser General Public License along with this - * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth - * Floor, Boston, MA 02110-1301, USA. - **/ -package org.bonitasoft.console.common.server.page; - -import org.bonitasoft.engine.api.ApplicationAPI; -import org.bonitasoft.engine.api.PageAPI; -import org.bonitasoft.engine.business.application.Application; -import org.bonitasoft.engine.business.application.ApplicationPageSearchDescriptor; -import org.bonitasoft.engine.business.application.ApplicationSearchDescriptor; -import org.bonitasoft.engine.exception.BonitaException; -import org.bonitasoft.engine.search.SearchOptionsBuilder; -import org.bonitasoft.engine.search.SearchResult; -import org.bonitasoft.engine.session.APISession; -import org.bonitasoft.livingapps.ApplicationModel; -import org.bonitasoft.livingapps.ApplicationModelFactory; - -/** - * @author Julien Mege - */ -public class CustomPageAuthorizationsHelper { - - private final ApplicationAPI applicationAPI; - private final PageAPI pageApi; - private final APISession apiSession; - private final ApplicationModelFactory applicationFactory; - - public CustomPageAuthorizationsHelper(final APISession apiSession, final ApplicationAPI applicationAPI, - final PageAPI pageApi, final ApplicationModelFactory applicationModelFactory) { - this.applicationAPI = applicationAPI; - this.pageApi = pageApi; - this.apiSession = apiSession; - this.applicationFactory = applicationModelFactory; - } - - public boolean isPageAuthorized(final String applicationToken, final String pageToken) { - try { - Long applicationId = getApplicationId(applicationToken); - final ApplicationModel application = applicationFactory.createApplicationModel(applicationToken); - - if (applicationId == null || !application.authorize(apiSession)) { - return false; - } - - return applicationAPI.searchApplicationPages(new SearchOptionsBuilder(0, 0) - .filter(ApplicationPageSearchDescriptor.APPLICATION_ID, applicationId) - .filter(ApplicationPageSearchDescriptor.PAGE_ID, pageApi.getPageByName(pageToken).getId()) - .done()).getCount() > 0; - } catch (final Exception e) { - return false; - } - } - - private Long getApplicationId(String applicationToken) throws BonitaException { - SearchResult applicationSResult = applicationAPI.searchApplications(new SearchOptionsBuilder(0, 1) - .filter(ApplicationSearchDescriptor.TOKEN, applicationToken).done()); - - if (applicationSResult.getResult().isEmpty()) { - return null; - } - - return applicationSResult.getResult().get(0).getId(); - } - -} diff --git a/bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/CustomPageServlet.java b/bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/CustomPageServlet.java index 75f35beee76..80d1f2c42e1 100644 --- a/bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/CustomPageServlet.java +++ b/bpm/bonita-web-server/src/main/java/org/bonitasoft/console/common/server/page/CustomPageServlet.java @@ -27,7 +27,11 @@ import org.bonitasoft.console.common.server.utils.BonitaHomeFolderAccessor; import org.bonitasoft.console.common.server.utils.SessionUtil; import org.bonitasoft.engine.api.TenantAPIAccessor; -import org.bonitasoft.engine.exception.*; +import org.bonitasoft.engine.exception.BonitaException; +import org.bonitasoft.engine.exception.BonitaHomeNotSetException; +import org.bonitasoft.engine.exception.ServerAPIException; +import org.bonitasoft.engine.exception.UnauthorizedAccessException; +import org.bonitasoft.engine.exception.UnknownAPITypeException; import org.bonitasoft.engine.session.APISession; import org.bonitasoft.livingapps.ApplicationModelFactory; import org.slf4j.Logger; @@ -78,7 +82,7 @@ protected void doGet(final HttpServletRequest request, final HttpServletResponse try { - if (isAuthorized(apiSession, appToken, pageName)) { + if (isAuthorized(apiSession, appToken)) { if (isPageRequest(pathSegments)) { pageRenderer.displayCustomPage(request, response, apiSession, pageName); } else { @@ -133,25 +137,24 @@ private String getResourcePathWithoutPageName(final String resourcePath, final S return resourcePath.substring(pageName.length() + 2); } - private boolean isAuthorized(final APISession apiSession, final String appToken, final String pageName) + private boolean isAuthorized(final APISession apiSession, final String appToken) throws BonitaException { //Technical user should be authorized in order for the custom pages to be displayed in his profile return apiSession.isTechnicalUser() - || getCustomPageAuthorizationsHelper(apiSession).isPageAuthorized(appToken, pageName); + || getCustomPageAuthorizationsHelper(apiSession).isAuthorized(appToken); } private void handleException(final String pageName, final Exception e) throws ServletException { if (LOGGER.isWarnEnabled()) { - LOGGER.warn("Error while trying to render the custom page " + pageName, e); + LOGGER.warn("Error while trying to render the custom page {}", pageName, e); } throw new ServletException(e.getMessage()); } - protected CustomPageAuthorizationsHelper getCustomPageAuthorizationsHelper(final APISession apiSession) + protected ApplicationAuthorizationsHelper getCustomPageAuthorizationsHelper(final APISession apiSession) throws BonitaHomeNotSetException, ServerAPIException, UnknownAPITypeException { - return new CustomPageAuthorizationsHelper(apiSession, - TenantAPIAccessor.getLivingApplicationAPI(apiSession), TenantAPIAccessor.getCustomPageAPI(apiSession), + return new ApplicationAuthorizationsHelper(apiSession, new ApplicationModelFactory( TenantAPIAccessor.getLivingApplicationAPI(apiSession), TenantAPIAccessor.getCustomPageAPI(apiSession), diff --git a/bpm/bonita-web-server/src/main/java/org/bonitasoft/livingapps/ApplicationModelFactory.java b/bpm/bonita-web-server/src/main/java/org/bonitasoft/livingapps/ApplicationModelFactory.java index 590f43e395d..fba2d6a3625 100644 --- a/bpm/bonita-web-server/src/main/java/org/bonitasoft/livingapps/ApplicationModelFactory.java +++ b/bpm/bonita-web-server/src/main/java/org/bonitasoft/livingapps/ApplicationModelFactory.java @@ -16,11 +16,7 @@ import org.bonitasoft.engine.api.ApplicationAPI; import org.bonitasoft.engine.api.PageAPI; import org.bonitasoft.engine.api.ProfileAPI; -import org.bonitasoft.engine.business.application.Application; -import org.bonitasoft.engine.business.application.ApplicationSearchDescriptor; -import org.bonitasoft.engine.exception.SearchException; -import org.bonitasoft.engine.search.SearchOptionsBuilder; -import org.bonitasoft.engine.search.SearchResult; +import org.bonitasoft.engine.business.application.ApplicationNotFoundException; import org.bonitasoft.livingapps.exception.CreationException; import org.bonitasoft.livingapps.menu.MenuFactory; @@ -37,26 +33,17 @@ public ApplicationModelFactory(final ApplicationAPI applicationApi, final PageAP this.profileApi = profileApi; } - public ApplicationModel createApplicationModel(final String name) throws CreationException { - + public ApplicationModel createApplicationModel(final String applicationToken) throws CreationException { try { - final SearchResult result = applicationApi.searchApplications( - new SearchOptionsBuilder(0, 1) - .filter(ApplicationSearchDescriptor.TOKEN, name) - .done()); - - if (result.getCount() == 0) { - throw new CreationException("No application found with name " + name); - } - + var application = applicationApi.getApplicationByToken(applicationToken); return new ApplicationModel( applicationApi, customPageApi, profileApi, - result.getResult().get(0), + application, new MenuFactory(applicationApi)); - } catch (final SearchException e) { - throw new CreationException("Error while searching for the application " + name, e); + } catch (final ApplicationNotFoundException e) { + throw new CreationException("Error while searching for the application " + applicationToken, e); } } } diff --git a/bpm/bonita-web-server/src/main/java/org/bonitasoft/livingapps/LivingApplicationPageServlet.java b/bpm/bonita-web-server/src/main/java/org/bonitasoft/livingapps/LivingApplicationPageServlet.java index b3d121b1dd3..25370b78d53 100644 --- a/bpm/bonita-web-server/src/main/java/org/bonitasoft/livingapps/LivingApplicationPageServlet.java +++ b/bpm/bonita-web-server/src/main/java/org/bonitasoft/livingapps/LivingApplicationPageServlet.java @@ -24,7 +24,7 @@ import javax.servlet.http.HttpSession; import org.apache.commons.lang3.StringUtils; -import org.bonitasoft.console.common.server.page.CustomPageAuthorizationsHelper; +import org.bonitasoft.console.common.server.page.ApplicationAuthorizationsHelper; import org.bonitasoft.console.common.server.page.CustomPageRequestModifier; import org.bonitasoft.console.common.server.page.CustomPageService; import org.bonitasoft.console.common.server.page.PageRenderer; @@ -224,7 +224,7 @@ private File getResourceFile(final String resourcePath, final String pageName) t private boolean isAuthorized(final APISession apiSession, final String appToken, final String pageName) throws BonitaException { - return getCustomPageAuthorizationsHelper(apiSession).isPageAuthorized(appToken, pageName); + return getCustomPageAuthorizationsHelper(apiSession).isAuthorized(appToken); } private void handleException(final String pageName, final Exception e, final HttpServletRequest request, @@ -258,11 +258,10 @@ protected PageAPI getPageApi(final APISession apiSession) return TenantAPIAccessor.getCustomPageAPI(apiSession); } - protected CustomPageAuthorizationsHelper getCustomPageAuthorizationsHelper(final APISession apiSession) + protected ApplicationAuthorizationsHelper getCustomPageAuthorizationsHelper(final APISession apiSession) throws BonitaHomeNotSetException, ServerAPIException, UnknownAPITypeException { - return new CustomPageAuthorizationsHelper(apiSession, - TenantAPIAccessor.getLivingApplicationAPI(apiSession), TenantAPIAccessor.getCustomPageAPI(apiSession), + return new ApplicationAuthorizationsHelper(apiSession, new ApplicationModelFactory( TenantAPIAccessor.getLivingApplicationAPI(apiSession), TenantAPIAccessor.getCustomPageAPI(apiSession), diff --git a/bpm/bonita-web-server/src/test/java/org/bonitasoft/console/common/server/page/CustomPageAuthorizationsHelperTest.java b/bpm/bonita-web-server/src/test/java/org/bonitasoft/console/common/server/page/CustomPageAuthorizationsHelperTest.java index 6b61c3d7f76..33d5cd8ae18 100644 --- a/bpm/bonita-web-server/src/test/java/org/bonitasoft/console/common/server/page/CustomPageAuthorizationsHelperTest.java +++ b/bpm/bonita-web-server/src/test/java/org/bonitasoft/console/common/server/page/CustomPageAuthorizationsHelperTest.java @@ -18,25 +18,12 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.*; -import java.util.Arrays; -import java.util.Collections; - import org.bonitasoft.engine.api.ApplicationAPI; -import org.bonitasoft.engine.api.PageAPI; import org.bonitasoft.engine.business.application.Application; -import org.bonitasoft.engine.business.application.ApplicationPage; -import org.bonitasoft.engine.business.application.ApplicationPageSearchDescriptor; -import org.bonitasoft.engine.page.ContentType; -import org.bonitasoft.engine.page.impl.PageImpl; -import org.bonitasoft.engine.search.SearchOptions; -import org.bonitasoft.engine.search.SearchResult; -import org.bonitasoft.engine.search.impl.SearchFilter; -import org.bonitasoft.engine.search.impl.SearchResultImpl; import org.bonitasoft.livingapps.ApplicationModel; import org.bonitasoft.livingapps.ApplicationModelFactory; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -47,14 +34,11 @@ public class CustomPageAuthorizationsHelperTest { @Mock ApplicationAPI applicationAPI; - @Mock - PageAPI pageAPI; - @Mock ApplicationModelFactory applicationFactory; @InjectMocks - CustomPageAuthorizationsHelper customPageAuthorizationsHelper; + ApplicationAuthorizationsHelper applicationAuthorizationsHelper; @Mock ApplicationModel applicationModel; @@ -62,52 +46,22 @@ public class CustomPageAuthorizationsHelperTest { @Mock Application application; - @Mock - SearchResult applicationResult; - @Test public void should_authorize_page_when_appToken_not_null_and_page_authorized_in_application() throws Exception { - given(pageAPI.getPageByName("pageToken")) - .willReturn(new PageImpl(2L, "", "", false, "", 0, 0, - 0, 0, "", ContentType.PAGE, null)); - given(applicationAPI.searchApplicationPages(any())) - .willReturn(new SearchResultImpl<>(1, Collections. emptyList())); - given(applicationAPI.searchApplications(any())) - .willReturn(applicationResult); - - given(applicationResult.getResult()).willReturn(Arrays.asList(application)); - given(application.getId()).willReturn(1L); - given(applicationFactory.createApplicationModel(any())).willReturn(applicationModel); when(applicationModel.authorize(any())).thenReturn(true); - final boolean isPageAuthorized = customPageAuthorizationsHelper.isPageAuthorized("appToken", "pageToken"); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(SearchOptions.class); - verify(applicationAPI).searchApplicationPages(captor.capture()); - - SearchFilter filter = captor.getValue().getFilters().get(0); - assertThat(filter.getField()).isEqualTo(ApplicationPageSearchDescriptor.APPLICATION_ID); - assertThat(filter.getValue()).isEqualTo(1L); - - filter = captor.getValue().getFilters().get(1); - assertThat(filter.getField()).isEqualTo(ApplicationPageSearchDescriptor.PAGE_ID); - assertThat(filter.getValue()).isEqualTo(2L); + final boolean isPageAuthorized = applicationAuthorizationsHelper.isAuthorized("appToken"); assertThat(isPageAuthorized).isTrue(); - verify(applicationModel).authorize(any()); } @Test public void should_unAuthorize_page_when_appToken_not_null_and_page_not_authorized_in_application() throws Exception { - - given(applicationAPI.searchApplications(any())) - .willReturn(applicationResult); - given(applicationResult.getResult()).willReturn(Arrays.asList(application)); given(applicationFactory.createApplicationModel(any(String.class))).willReturn(applicationModel); when(applicationModel.authorize(any())).thenReturn(false); - final boolean isPageAuthorized = customPageAuthorizationsHelper.isPageAuthorized("appToken", "pageToken"); + final boolean isPageAuthorized = applicationAuthorizationsHelper.isAuthorized("appToken"); assertThat(isPageAuthorized).isFalse(); verify(applicationModel).authorize(any()); @@ -116,16 +70,14 @@ public void should_unAuthorize_page_when_appToken_not_null_and_page_not_authoriz @Test public void should_not_authorize_page_when_appToken_not_null_and_page_unauthorized_in_application() { - - final boolean isPageAuthorized = customPageAuthorizationsHelper.isPageAuthorized("appToken", "pageToken"); + final boolean isPageAuthorized = applicationAuthorizationsHelper.isAuthorized("appToken"); assertThat(isPageAuthorized).isFalse(); } @Test public void should_not_authorize_page_when_appToken_is_null() { - - final boolean isPageAuthorized = customPageAuthorizationsHelper.isPageAuthorized("", "pageToken"); + final boolean isPageAuthorized = applicationAuthorizationsHelper.isAuthorized(""); assertThat(isPageAuthorized).isFalse(); } diff --git a/bpm/bonita-web-server/src/test/java/org/bonitasoft/console/common/server/page/CustomPageServletTest.java b/bpm/bonita-web-server/src/test/java/org/bonitasoft/console/common/server/page/CustomPageServletTest.java index 637201b157d..9def5ec6e83 100644 --- a/bpm/bonita-web-server/src/test/java/org/bonitasoft/console/common/server/page/CustomPageServletTest.java +++ b/bpm/bonita-web-server/src/test/java/org/bonitasoft/console/common/server/page/CustomPageServletTest.java @@ -51,7 +51,7 @@ public class CustomPageServletTest { APISession apiSession; @Mock - CustomPageAuthorizationsHelper customPageAuthorizationsHelper; + ApplicationAuthorizationsHelper customPageAuthorizationsHelper; @Mock PageRenderer pageRenderer; @@ -85,7 +85,7 @@ public void should_get_Forbidden_Status_when_page_unAuthorize() throws Exception hsRequest.setParameter("appToken", "myApp"); given(resourceRenderer.getPathSegments("/pageToken/")).willReturn(Arrays.asList("pageToken")); doReturn(false).when(apiSession).isTechnicalUser(); - given(customPageAuthorizationsHelper.isPageAuthorized("myApp", "pageToken")).willReturn(false); + given(customPageAuthorizationsHelper.isAuthorized("myApp")).willReturn(false); servlet.doGet(hsRequest, hsResponse); @@ -129,7 +129,7 @@ private void testPageIsWellCalled(final String token, final String path, final L throws Exception { hsRequest.setPathInfo(path); given(resourceRenderer.getPathSegments(path)).willReturn(pathSegment); - given(customPageAuthorizationsHelper.isPageAuthorized(null, token)).willReturn(true); + given(customPageAuthorizationsHelper.isAuthorized(null)).willReturn(true); servlet.doGet(hsRequest, hsResponse); @@ -146,7 +146,7 @@ public void getResource_should_call_the_resource_renderer() throws Exception { doReturn(pageResourceProvider).when(pageRenderer).getPageResourceProvider(pageName); doReturn(pageDir).when(pageResourceProvider).getPageDirectory(); doReturn(true).when(bonitaHomeFolderAccessor).isInFolder(any(File.class), any(File.class)); - given(customPageAuthorizationsHelper.isPageAuthorized(null, "custompage_htmlexample")).willReturn(true); + given(customPageAuthorizationsHelper.isAuthorized(null)).willReturn(true); servlet.doGet(hsRequest, hsResponse); @@ -163,7 +163,7 @@ public void should_get_Forbidden_Status_when_we_try_to_access_to_unAuthorize_fil Arrays.asList("custompage_htmlexample", "css", "..", "..", "..", "file.css")); doReturn(pageResourceProvider).when(pageRenderer).getPageResourceProvider("custompage_htmlexample"); given(pageResourceProvider.getPageDirectory()).willReturn(pageDir); - given(customPageAuthorizationsHelper.isPageAuthorized(null, "custompage_htmlexample")).willReturn(true); + given(customPageAuthorizationsHelper.isAuthorized(null)).willReturn(true); // folder we wants to access is not authorized doReturn(false).when(bonitaHomeFolderAccessor).isInFolder(any(File.class), any(File.class)); diff --git a/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/ApplicationModelFactoryTest.java b/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/ApplicationModelFactoryTest.java index 5fc9c1a798f..4fb6a42b2d5 100644 --- a/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/ApplicationModelFactoryTest.java +++ b/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/ApplicationModelFactoryTest.java @@ -13,28 +13,19 @@ **/ package org.bonitasoft.livingapps; -import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import java.util.Collections; import org.bonitasoft.engine.api.ApplicationAPI; import org.bonitasoft.engine.business.application.Application; +import org.bonitasoft.engine.business.application.ApplicationNotFoundException; import org.bonitasoft.engine.business.application.impl.ApplicationImpl; import org.bonitasoft.engine.business.application.impl.ApplicationPageImpl; -import org.bonitasoft.engine.exception.SearchException; -import org.bonitasoft.engine.search.SearchOptions; -import org.bonitasoft.engine.search.impl.SearchFilter; -import org.bonitasoft.engine.search.impl.SearchResultImpl; import org.bonitasoft.livingapps.exception.CreationException; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; @@ -50,15 +41,7 @@ public class ApplicationModelFactoryTest { @Test(expected = CreationException.class) public void should_throw_create_error_exception_when_search_fail() throws Exception { - given(applicationApi.searchApplications(any(SearchOptions.class))).willThrow(SearchException.class); - - factory.createApplicationModel("foo"); - } - - @Test(expected = CreationException.class) - public void should_throw_create_error_exception_when_application_is_not_found() throws Exception { - given(applicationApi.searchApplications(any(SearchOptions.class))).willReturn( - new SearchResultImpl<>(0, Collections. emptyList())); + given(applicationApi.getApplicationByToken(any(String.class))).willThrow(ApplicationNotFoundException.class); factory.createApplicationModel("foo"); } @@ -67,8 +50,7 @@ public void should_throw_create_error_exception_when_application_is_not_found() public void should_return_application_found() throws Exception { final ApplicationImpl application = new ApplicationImpl("foobar", "1.0", "bazqux"); application.setId(3); - given(applicationApi.searchApplications(any(SearchOptions.class))).willReturn( - new SearchResultImpl<>(1, asList((Application) application))); + given(applicationApi.getApplicationByToken(any(String.class))).willReturn((Application) application); given(applicationApi.getApplicationHomePage(3)).willReturn(new ApplicationPageImpl(1, 1, "home")); final ApplicationModel model = factory.createApplicationModel("foo"); @@ -76,17 +58,4 @@ public void should_return_application_found() throws Exception { assertThat(model.getApplicationHomePage()).isEqualTo("home/"); } - @Test - public void should_filter_search_using_given_name() throws Exception { - final ArgumentCaptor captor = ArgumentCaptor.forClass(SearchOptions.class); - given(applicationApi.searchApplications(any(SearchOptions.class))).willReturn( - new SearchResultImpl<>(1, asList(mock(Application.class)))); - - factory.createApplicationModel("bar"); - verify(applicationApi).searchApplications(captor.capture()); - - final SearchFilter filter = captor.getValue().getFilters().get(0); - assertThat(filter.getField()).isEqualTo("token"); - assertThat(filter.getValue()).isEqualTo("bar"); - } } diff --git a/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/ApplicationModelTest.java b/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/ApplicationModelTest.java index f743d47dae9..08b7e0ba0de 100644 --- a/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/ApplicationModelTest.java +++ b/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/ApplicationModelTest.java @@ -191,7 +191,7 @@ public void should_hasPage_return_true() throws Exception { given(applicationApi.getApplicationPage("token", "pageToken")) .willReturn(new ApplicationPageImpl(1, 1, "pageToken")); - assertThat(model.hasPage("pageToken")).isEqualTo(true); + assertThat(model.hasPage("pageToken")).isTrue(); } @Test @@ -199,7 +199,7 @@ public void should_hasPage_return_false() throws Exception { given(applicationApi.getApplicationPage("token", "pageToken")) .willThrow(new ApplicationPageNotFoundException("")); - assertThat(model.hasPage("pageToken")).isEqualTo(false); + assertThat(model.hasPage("pageToken")).isFalse(); } @Test @@ -216,15 +216,15 @@ public void should_getCustomPage_return_expectedPage() throws Exception { public void should_check_that_application_has_a_profile_mapped_to_it() throws Exception { application.setProfileId(1L); application.setVisibility(ApplicationVisibility.RESTRICTED); - assertThat(model.hasProfileMapped()).isEqualTo(true); + assertThat(model.hasProfileMapped()).isTrue(); application.setProfileId(null); - assertThat(model.hasProfileMapped()).isEqualTo(false); + assertThat(model.hasProfileMapped()).isFalse(); application.setVisibility(ApplicationVisibility.ALL); - assertThat(model.hasProfileMapped()).isEqualTo(true); + assertThat(model.hasProfileMapped()).isTrue(); application.setVisibility(ApplicationVisibility.TECHNICAL_USER); - assertThat(model.hasProfileMapped()).isEqualTo(true); + assertThat(model.hasProfileMapped()).isTrue(); } } diff --git a/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/LivingApplicationPageServletTest.java b/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/LivingApplicationPageServletTest.java index d7d12c2a235..3c2a7b8a13d 100644 --- a/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/LivingApplicationPageServletTest.java +++ b/bpm/bonita-web-server/src/test/java/org/bonitasoft/livingapps/LivingApplicationPageServletTest.java @@ -15,11 +15,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; import java.io.File; import java.util.Arrays; @@ -28,7 +24,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import org.bonitasoft.console.common.server.page.CustomPageAuthorizationsHelper; +import org.bonitasoft.console.common.server.page.ApplicationAuthorizationsHelper; import org.bonitasoft.console.common.server.page.PageRenderer; import org.bonitasoft.console.common.server.page.ResourceRenderer; import org.bonitasoft.console.common.server.page.extension.PageResourceProviderImpl; @@ -66,7 +62,7 @@ public class LivingApplicationPageServletTest { APISession apiSession; @Mock - CustomPageAuthorizationsHelper customPageAuthorizationsHelper; + ApplicationAuthorizationsHelper customPageAuthorizationsHelper; @Mock PageRenderer pageRenderer; @@ -111,7 +107,7 @@ public void should_get_Forbidden_Status_when_page_unAuthorize() throws Exception hsRequest.setPathInfo("/AppToken/pageToken/content/"); given(resourceRenderer.getPathSegments("/AppToken/pageToken/content/")) .willReturn(Arrays.asList("AppToken", "pageToken", "content")); - given(customPageAuthorizationsHelper.isPageAuthorized("AppToken", "customPageName")).willReturn(false); + given(customPageAuthorizationsHelper.isAuthorized("AppToken")).willReturn(false); doReturn(applicationPage).when(applicationAPI).getApplicationPage("AppToken", "pageToken"); doReturn(2L).when(applicationPage).getPageId(); @@ -176,7 +172,7 @@ private void testPageIsWellCalled(final String appToken, final String pageToken, final List pathSegment) throws Exception { hsRequest.setPathInfo(path); given(resourceRenderer.getPathSegments(path)).willReturn(pathSegment); - given(customPageAuthorizationsHelper.isPageAuthorized(appToken, "customPage_" + pageToken)).willReturn(true); + given(customPageAuthorizationsHelper.isAuthorized(appToken)).willReturn(true); doReturn(applicationPage).when(applicationAPI).getApplicationPage(appToken, pageToken); doReturn(2L).when(applicationPage).getPageId(); @@ -210,7 +206,7 @@ public void getResource_should_call_the_resource_renderer() throws Exception { given(resourceRenderer.getPathSegments("/AppToken/htmlexample/content/css/file.css")) .willReturn(Arrays.asList("AppToken", "htmlexample", "content", "css", "file.css")); final String pageName = "customPage_htmlexample"; - doReturn(true).when(customPageAuthorizationsHelper).isPageAuthorized(any(String.class), any(String.class)); + doReturn(true).when(customPageAuthorizationsHelper).isAuthorized(any(String.class)); doReturn(pageResourceProvider).when(pageRenderer).getPageResourceProvider(pageName); doReturn(pageDir).when(pageResourceProvider).getPageDirectory(); doReturn(true).when(bonitaHomeFolderAccessor).isInFolder(any(File.class), any(File.class)); diff --git a/services/bonita-business-application/src/main/java/org/bonitasoft/engine/business/application/ApplicationService.java b/services/bonita-business-application/src/main/java/org/bonitasoft/engine/business/application/ApplicationService.java index 630147204b0..f2f3fe9c1b5 100755 --- a/services/bonita-business-application/src/main/java/org/bonitasoft/engine/business/application/ApplicationService.java +++ b/services/bonita-business-application/src/main/java/org/bonitasoft/engine/business/application/ApplicationService.java @@ -20,7 +20,10 @@ import org.bonitasoft.engine.business.application.model.SApplicationPage; import org.bonitasoft.engine.business.application.model.SApplicationWithIcon; import org.bonitasoft.engine.commons.TenantLifecycleService; -import org.bonitasoft.engine.commons.exceptions.*; +import org.bonitasoft.engine.commons.exceptions.SObjectAlreadyExistsException; +import org.bonitasoft.engine.commons.exceptions.SObjectCreationException; +import org.bonitasoft.engine.commons.exceptions.SObjectModificationException; +import org.bonitasoft.engine.commons.exceptions.SObjectNotFoundException; import org.bonitasoft.engine.persistence.QueryOptions; import org.bonitasoft.engine.persistence.SBonitaReadException; import org.bonitasoft.engine.recorder.model.EntityUpdateDescriptor; diff --git a/services/bonita-business-application/src/main/java/org/bonitasoft/engine/business/application/impl/ApplicationServiceImpl.java b/services/bonita-business-application/src/main/java/org/bonitasoft/engine/business/application/impl/ApplicationServiceImpl.java index 01ca4880f37..a7a9e09c7cd 100755 --- a/services/bonita-business-application/src/main/java/org/bonitasoft/engine/business/application/impl/ApplicationServiceImpl.java +++ b/services/bonita-business-application/src/main/java/org/bonitasoft/engine/business/application/impl/ApplicationServiceImpl.java @@ -38,6 +38,9 @@ import org.bonitasoft.engine.business.application.model.builder.impl.SApplicationLogBuilderImpl; import org.bonitasoft.engine.business.application.model.builder.impl.SApplicationMenuLogBuilderImpl; import org.bonitasoft.engine.business.application.model.builder.impl.SApplicationPageLogBuilderImpl; +import org.bonitasoft.engine.cache.CacheService; +import org.bonitasoft.engine.cache.SCacheException; +import org.bonitasoft.engine.cache.configuration.CacheConfiguration; import org.bonitasoft.engine.commons.exceptions.SBonitaException; import org.bonitasoft.engine.commons.exceptions.SObjectAlreadyExistsException; import org.bonitasoft.engine.commons.exceptions.SObjectCreationException; @@ -81,13 +84,15 @@ public class ApplicationServiceImpl implements ApplicationService { private final ApplicationDestructor applicationDestructor; private final ApplicationPageDestructor applicationPageDestructor; private final ApplicationMenuDestructor applicationMenuDestructor; + private final CacheService cacheService; @Autowired public ApplicationServiceImpl(Recorder recorder, ReadPersistenceService persistenceService, - final QueriableLoggerService queriableLoggerService) { + final QueriableLoggerService queriableLoggerService, CacheService cacheService) { this.recorder = recorder; this.persistenceService = persistenceService; this.queriableLoggerService = queriableLoggerService; + this.cacheService = cacheService; indexManager = new IndexManager(new IndexUpdater(this, MAX_RESULTS), new MenuIndexValidator()); menuIndexConverter = new MenuIndexConverter(this); final ApplicationMenuCleaner applicationMenuCleaner = new ApplicationMenuCleaner(this); @@ -103,10 +108,11 @@ public ApplicationServiceImpl(Recorder recorder, ReadPersistenceService persiste MenuIndexConverter menuIndexConverter, ApplicationDestructor applicationDestructor, ApplicationPageDestructor applicationPageDestructor, - ApplicationMenuDestructor applicationMenuDestructor) { + ApplicationMenuDestructor applicationMenuDestructor, CacheService cacheService) { this.recorder = recorder; this.persistenceService = persistenceService; this.queriableLoggerService = queriableLoggerService; + this.cacheService = cacheService; this.indexManager = indexManager; this.menuIndexConverter = menuIndexConverter; this.applicationDestructor = applicationDestructor; @@ -161,8 +167,21 @@ public boolean hasApplicationWithToken(final String name) throws SBonitaReadExce @Override public SApplication getApplicationByToken(final String token) throws SBonitaReadException { - return persistenceService.selectOne(new SelectOneDescriptor<>("getApplicationByToken", Collections - .singletonMap("token", token), SApplication.class)); + try { + SApplication application = (SApplication) cacheService.get(CacheConfiguration.APPLICATION_TOKEN_CACHE_NAME, + token); + if (application == null) { + application = persistenceService + .selectOne(new SelectOneDescriptor<>("getApplicationByToken", Collections + .singletonMap("token", token), SApplication.class)); + if (application != null) { + cacheService.store(CacheConfiguration.APPLICATION_TOKEN_CACHE_NAME, token, application); + } + } + return application; + } catch (SCacheException e) { + throw new SBonitaReadException(e); + } } private void initializeLogBuilder(final T logBuilder, final String message, @@ -250,6 +269,9 @@ public void deleteApplication(final long applicationId) } private void deleteApplication(SApplication application) throws SBonitaException { + if (application.getToken() != null) { + cacheService.remove(CacheConfiguration.APPLICATION_TOKEN_CACHE_NAME, application.getToken()); + } applicationDestructor.onDeleteApplication(application); recorder.recordDelete(new DeleteRecord(application), APPLICATION); log(application.getId(), SQueriableLog.STATUS_OK, getApplicationLogBuilder(ActionType.DELETED, @@ -306,6 +328,9 @@ public SApplicationWithIcon updateApplication(final SApplicationWithIcon applica "Updating application with id " + application.getId()); try { validateUpdatedFields(updateDescriptor, application); + if (application.getToken() != null) { + cacheService.remove(CacheConfiguration.APPLICATION_TOKEN_CACHE_NAME, application.getToken()); + } updateDescriptor.addField(AbstractSApplication.LAST_UPDATE_DATE, now); recorder.recordUpdate(UpdateRecord.buildSetFields(application, diff --git a/services/bonita-business-application/src/test/java/org/bonitasoft/engine/business/application/impl/ApplicationServiceImplTest.java b/services/bonita-business-application/src/test/java/org/bonitasoft/engine/business/application/impl/ApplicationServiceImplTest.java index 207a329147c..e5ae8da64bb 100755 --- a/services/bonita-business-application/src/test/java/org/bonitasoft/engine/business/application/impl/ApplicationServiceImplTest.java +++ b/services/bonita-business-application/src/test/java/org/bonitasoft/engine/business/application/impl/ApplicationServiceImplTest.java @@ -41,6 +41,7 @@ import org.bonitasoft.engine.business.application.model.SApplicationWithIcon; import org.bonitasoft.engine.business.application.model.builder.SApplicationMenuUpdateBuilder; import org.bonitasoft.engine.business.application.model.builder.SApplicationUpdateBuilder; +import org.bonitasoft.engine.cache.CacheService; import org.bonitasoft.engine.commons.exceptions.SObjectAlreadyExistsException; import org.bonitasoft.engine.commons.exceptions.SObjectCreationException; import org.bonitasoft.engine.commons.exceptions.SObjectModificationException; @@ -103,6 +104,9 @@ public class ApplicationServiceImplTest { @Mock private ApplicationMenuDestructor applicationMenuDestructor; + @Mock + private CacheService cacheService; + private SApplicationWithIcon application; private ApplicationServiceImpl applicationServiceImpl; @@ -110,7 +114,8 @@ public class ApplicationServiceImplTest { @Before public void setUp() { applicationServiceImpl = new ApplicationServiceImpl(recorder, persistenceService, queriableLogService, - indexManager, convertor, applicationDestructor, applicationPageDestructor, applicationMenuDestructor); + indexManager, convertor, applicationDestructor, applicationPageDestructor, applicationMenuDestructor, + cacheService); when(queriableLogService.isLoggable(anyString(), any(SQueriableLogSeverity.class))).thenReturn(true); application = buildApplication(APPLICATION_TOKEN, APPLICATION_DISPLAY_NAME); diff --git a/services/bonita-cache/src/main/java/org/bonitasoft/engine/cache/configuration/CacheConfiguration.java b/services/bonita-cache/src/main/java/org/bonitasoft/engine/cache/configuration/CacheConfiguration.java new file mode 100644 index 00000000000..75e85afb357 --- /dev/null +++ b/services/bonita-cache/src/main/java/org/bonitasoft/engine/cache/configuration/CacheConfiguration.java @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2024 Bonitasoft S.A. + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble + * This library is free software; you can redistribute it and/or modify it under the terms + * of the GNU Lesser General Public License as published by the Free Software Foundation + * version 2.1 of the License. + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License along with this + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth + * Floor, Boston, MA 02110-1301, USA. + **/ +package org.bonitasoft.engine.cache.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CacheConfiguration { + + public static final String APPLICATION_TOKEN_CACHE_NAME = "application-token"; + + @Bean + public org.bonitasoft.engine.cache.CacheConfiguration applicationTokenCacheConfiguration( + @Value("${bonita.runtime.cache.application-token.maxElementsInMemory:1000}") final int maxElementsInMemory, + @Value("${bonita.runtime.cache.application-token.inMemoryOnly:true}") final boolean inMemoryOnly, + @Value("${bonita.runtime.cache.application-token.maxElementsOnDisk:20000}") final int maxElementsOnDisk, + @Value("${bonita.runtime.cache.application-token.eternal:false}") final boolean eternal, + @Value("${bonita.runtime.cache.application-token.evictionPolicy:LRU}") final String evictionPolicy, + @Value("${bonita.runtime.cache.application-token.copyOnRead:false}") final boolean copyOnRead, + @Value("${bonita.runtime.cache.application-token.copyOnWrite:false}") final boolean copyOnWrite, + @Value("${bonita.runtime.cache.application-token.readIntensive:false}") final boolean readIntensive, + @Value("${bonita.runtime.cache.application-token.timeToLiveSeconds:3600}") final int timeToLiveSeconds) { + var cacheConfiguration = new org.bonitasoft.engine.cache.CacheConfiguration(); + cacheConfiguration.setName(APPLICATION_TOKEN_CACHE_NAME); + cacheConfiguration.setMaxElementsInMemory(maxElementsInMemory); + cacheConfiguration.setInMemoryOnly(inMemoryOnly); + cacheConfiguration.setMaxElementsOnDisk(maxElementsOnDisk); + cacheConfiguration.setEternal(eternal); + cacheConfiguration.setEvictionPolicy(evictionPolicy); + cacheConfiguration.setCopyOnRead(copyOnRead); + cacheConfiguration.setCopyOnWrite(copyOnWrite); + cacheConfiguration.setReadIntensive(readIntensive); + cacheConfiguration.setTimeToLiveSeconds(timeToLiveSeconds); + return cacheConfiguration; + } +}