From bedfe3da2b9461e5549f6c9935a5f9c73a9a4f97 Mon Sep 17 00:00:00 2001 From: wangf1122 <74916635+wangf1122@users.noreply.github.com> Date: Thu, 7 Nov 2024 07:23:33 -0500 Subject: [PATCH] Enhance static page with group user accessibility (#7707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add group/accessExpression field to static page feature * Filter none group member access to page belong to certain group * code improvement * multi selections for groups * multi selections for groups. * Refactoring group checking logic * Update services/src/main/java/org/fao/geonet/api/pages/model/GroupAccessExpression.java Co-authored-by: Jose García * ui change menu behavior * Fixing ui issue * pageGroup joint relation in page entity * pageGroup joint relation in page entity (add SQL) * remove page creation script * Update domain/src/main/java/org/fao/geonet/domain/page/Page.java Co-authored-by: Jose García * rename group * prevent group remove if it has association with static pages * JPA ManyToMany bug fix --------- Co-authored-by: Jose García --- .../java/org/fao/geonet/domain/page/Page.java | 35 +++++- .../repository/page/PageRepository.java | 4 +- .../org/fao/geonet/api/groups/GroupsApi.java | 20 ++++ .../fao/geonet/api/pages/PageProperties.java | 18 +++ .../org/fao/geonet/api/pages/PagesAPI.java | 108 ++++++++++++++++-- .../catalog/js/admin/StaticPagesController.js | 24 ++++ .../main/resources/catalog/locales/en-v4.json | 1 + .../admin/settings/static-pages.html | 21 ++++ 8 files changed, 219 insertions(+), 12 deletions(-) diff --git a/domain/src/main/java/org/fao/geonet/domain/page/Page.java b/domain/src/main/java/org/fao/geonet/domain/page/Page.java index fb3247a4308..563e58663fa 100644 --- a/domain/src/main/java/org/fao/geonet/domain/page/Page.java +++ b/domain/src/main/java/org/fao/geonet/domain/page/Page.java @@ -23,10 +23,13 @@ package org.fao.geonet.domain.page; import java.io.Serializable; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import javax.annotation.Nullable; import javax.persistence.Basic; +import javax.persistence.CascadeType; import javax.persistence.CollectionTable; import javax.persistence.Column; import javax.persistence.ElementCollection; @@ -35,10 +38,14 @@ import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; import javax.persistence.Lob; +import javax.persistence.ManyToMany; import javax.persistence.Table; import org.fao.geonet.domain.GeonetEntity; +import org.fao.geonet.domain.Group; import org.hibernate.annotations.Type; /** @@ -56,6 +63,7 @@ public class Page extends GeonetEntity implements Serializable { private PageFormat format; private List sections; private PageStatus status; + private Set groups = new LinkedHashSet<>(); private String label; private String icon; @@ -64,7 +72,7 @@ public Page() { } - public Page(PageIdentity pageIdentity, byte[] data, String link, PageFormat format, List sections, PageStatus status, String label, String icon) { + public Page(PageIdentity pageIdentity, byte[] data, String link, PageFormat format, List sections, PageStatus status, String label, String icon, Set groups) { super(); this.pageIdentity = pageIdentity; this.data = data; @@ -74,10 +82,11 @@ public Page(PageIdentity pageIdentity, byte[] data, String link, PageFormat form this.status = status; this.label = label; this.icon = icon; + this.groups = groups; } public enum PageStatus { - PUBLIC, PUBLIC_ONLY, PRIVATE, HIDDEN; + PUBLIC, PUBLIC_ONLY, GROUPS, PRIVATE, HIDDEN; } public enum PageFormat { @@ -146,6 +155,28 @@ public String getIcon() { return icon; } + /** + * Get all the page's groups. + * + * @return all the page's groups. + */ + @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.DETACH, CascadeType.PERSIST, CascadeType.REFRESH}) + @JoinTable(name = "spg_page_group", joinColumns = {@JoinColumn(name = "language"), @JoinColumn(name = "linktext")}, + inverseJoinColumns = {@JoinColumn(name = "groupid", referencedColumnName = "id", unique = false)}) + public Set getGroups() { + return groups; + } + + /** + * Set all the page's groups. + * + * @param groups all the page's groups. + * @return this group object + */ + public void setGroups(Set groups) { + this.groups = groups; + } + public void setPageIdentity(PageIdentity pageIdentity) { this.pageIdentity = pageIdentity; } diff --git a/domain/src/main/java/org/fao/geonet/repository/page/PageRepository.java b/domain/src/main/java/org/fao/geonet/repository/page/PageRepository.java index b76094c770f..1245a752d99 100644 --- a/domain/src/main/java/org/fao/geonet/repository/page/PageRepository.java +++ b/domain/src/main/java/org/fao/geonet/repository/page/PageRepository.java @@ -29,7 +29,9 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface PageRepository extends JpaRepository { - + List findByPageIdentityLanguage(String language); + List findPageByStatus(Page.PageStatus status); + } diff --git a/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java b/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java index 12479f28e49..2e88e8a42bc 100644 --- a/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java +++ b/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java @@ -33,6 +33,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jeeves.server.UserSession; import jeeves.server.context.ServiceContext; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.exception.ExceptionUtils; @@ -47,8 +48,10 @@ import org.fao.geonet.api.tools.i18n.TranslationPackBuilder; import org.fao.geonet.constants.Geonet; import org.fao.geonet.domain.*; +import org.fao.geonet.domain.page.Page; import org.fao.geonet.kernel.DataManager; import org.fao.geonet.repository.*; +import org.fao.geonet.repository.page.PageRepository; import org.fao.geonet.repository.specification.GroupSpecs; import org.fao.geonet.repository.specification.MetadataSpecs; import org.fao.geonet.repository.specification.OperationAllowedSpecs; @@ -79,6 +82,7 @@ import java.nio.file.attribute.FileTime; import java.sql.SQLException; import java.util.*; +import java.util.stream.Collectors; import static org.springframework.data.jpa.domain.Specification.where; @@ -151,6 +155,9 @@ public class GroupsApi { @Autowired private MetadataRepository metadataRepository; + @Autowired + private PageRepository pageRepository; + private static Resources.ResourceHolder getImage(Resources resources, ServiceContext serviceContext, Group group) throws IOException { final Path logosDir = resources.locateLogosDir(serviceContext); final Path harvesterLogosDir = resources.locateHarvesterLogosDir(serviceContext); @@ -543,6 +550,19 @@ public void deleteGroup( )); } + List staticPages = pageRepository.findPageByStatus(Page.PageStatus.GROUPS); + List staticPagesAssignedToGroup = + staticPages.stream().filter(p -> + !p.getGroups().stream().filter(g -> g.getId() == groupIdentifier).collect(Collectors.toList()).isEmpty()) + .collect(Collectors.toList()); + + if (!staticPagesAssignedToGroup.isEmpty()) { + throw new NotAllowedException(String.format( + "Group %s is associated with '%s' static page(s). Please remove the static page(s) associated with that group first.", + group.get().getName(), staticPagesAssignedToGroup.stream().map(p -> p.getLabel()).collect(Collectors.joining()) + )); + } + groupRepository.deleteById(groupIdentifier); translationPackBuilder.clearCache(); diff --git a/services/src/main/java/org/fao/geonet/api/pages/PageProperties.java b/services/src/main/java/org/fao/geonet/api/pages/PageProperties.java index 227908a4082..d19070345c7 100644 --- a/services/src/main/java/org/fao/geonet/api/pages/PageProperties.java +++ b/services/src/main/java/org/fao/geonet/api/pages/PageProperties.java @@ -1,11 +1,14 @@ package org.fao.geonet.api.pages; +import org.apache.commons.collections4.CollectionUtils; +import org.fao.geonet.domain.Group; import org.fao.geonet.domain.page.Page; import org.fao.geonet.domain.page.Page.PageFormat; import org.fao.geonet.domain.page.Page.PageSection; import org.fao.geonet.domain.page.Page.PageStatus; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; public class PageProperties implements Serializable { @@ -21,6 +24,7 @@ public class PageProperties implements Serializable { private String label; private String icon; private Page.PageFormat format; + private List groups; private Page page; public PageProperties() { @@ -36,6 +40,12 @@ public PageProperties(Page p) { status = p.getStatus(); label = p.getLabel(); icon = p.getIcon(); + if (CollectionUtils.isNotEmpty(p.getGroups())) { + groups = new ArrayList<>(); + for (Group g : p.getGroups()) { + groups.add(g.getName()); + } + } } @Override @@ -114,4 +124,12 @@ public String getIcon() { public void setIcon(String icon) { this.icon = icon; } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } } diff --git a/services/src/main/java/org/fao/geonet/api/pages/PagesAPI.java b/services/src/main/java/org/fao/geonet/api/pages/PagesAPI.java index 491c04878e2..14da2d9e93c 100644 --- a/services/src/main/java/org/fao/geonet/api/pages/PagesAPI.java +++ b/services/src/main/java/org/fao/geonet/api/pages/PagesAPI.java @@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jeeves.server.UserSession; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.fao.geonet.api.ApiParams; @@ -35,10 +36,16 @@ import org.fao.geonet.api.exception.ResourceNotFoundException; import org.fao.geonet.api.exception.WebApplicationException; import org.fao.geonet.api.tools.i18n.LanguageUtils; +import org.fao.geonet.domain.Group; import org.fao.geonet.domain.Profile; +import org.fao.geonet.domain.UserGroup; import org.fao.geonet.domain.page.Page; import org.fao.geonet.domain.page.PageIdentity; +import org.fao.geonet.repository.GroupRepository; +import org.fao.geonet.repository.UserGroupRepository; import org.fao.geonet.repository.page.PageRepository; +import org.fao.geonet.repository.specification.UserGroupSpecs; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -55,8 +62,10 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; @@ -75,11 +84,16 @@ public class PagesAPI { private static final String ERROR_CREATE = "Wrong parameters are provided"; private final PageRepository pageRepository; + private final GroupRepository groupRepository; + + @Autowired + UserGroupRepository userGroupRepository; private final LanguageUtils languageUtils; - public PagesAPI(PageRepository pageRepository, LanguageUtils languageUtils) { + public PagesAPI(PageRepository pageRepository, GroupRepository groupRepository, LanguageUtils languageUtils) { this.pageRepository = pageRepository; + this.groupRepository = groupRepository; this.languageUtils = languageUtils; } @@ -157,6 +171,7 @@ private ResponseEntity createPage(PageProperties pageProperties, String language = pageProperties.getLanguage(); String pageId = pageProperties.getPageId(); Page.PageFormat format = pageProperties.getFormat(); + List groups = pageProperties.getGroups(); if (language != null) { checkValidLanguage(language); @@ -182,6 +197,15 @@ private ResponseEntity createPage(PageProperties pageProperties, if (status != null) { newPage.setStatus(status); + + if (status == Page.PageStatus.GROUPS && CollectionUtils.isNotEmpty(groups)) { + Set pageGroups = new LinkedHashSet<>(); + for (String groupName : groups) { + Group group = groupRepository.findByName(groupName); + pageGroups.add(group); + } + newPage.setGroups(pageGroups); + } } pageRepository.save(newPage); @@ -203,6 +227,30 @@ private ResponseEntity updatePageInternal(@NotNull String language, String newLabel = pageProperties.getLabel(); String newIcon = pageProperties.getIcon(); + Set _groups = new LinkedHashSet<>(); + if (CollectionUtils.isNotEmpty(pageProperties.getGroups())) { + for (String groupName : pageProperties.getGroups()) { + Group group = groupRepository.findByName(groupName); + + Group groupToAdd= new Group(); + groupToAdd.setId(group.getId()); + groupToAdd.setAllowedCategories(group.getAllowedCategories()); + groupToAdd.setDescription(group.getDescription()); + groupToAdd.setEmail(group.getEmail()); + groupToAdd.setLogo(group.getLogo()); + groupToAdd.setEnableAllowedCategories(group.getEnableAllowedCategories()); + groupToAdd.setDefaultCategory(group.getDefaultCategory()); + groupToAdd.setName(group.getName()); + groupToAdd.setReferrer(group.getReferrer()); + groupToAdd.setWebsite(group.getWebsite()); + groupToAdd.setLabelTranslations(group.getLabelTranslations()); + + _groups.add(groupToAdd); + } + + } + + checkValidLanguage(language); if (newLanguage != null) { @@ -240,7 +288,8 @@ private ResponseEntity updatePageInternal(@NotNull String language, pageProperties.getSections() != null ? pageProperties.getSections() : pageToUpdate.getSections(), pageProperties.getStatus() != null ? pageProperties.getStatus() : pageToUpdate.getStatus(), newLabel != null ? newLabel : pageToUpdate.getLabel(), - newIcon != null ? newIcon : pageToUpdate.getIcon()); + newIcon != null ? newIcon : pageToUpdate.getIcon(), + CollectionUtils.isNotEmpty(_groups)? _groups: null); pageRepository.save(pageCopy); pageRepository.delete(pageToUpdate); @@ -251,7 +300,14 @@ private ResponseEntity updatePageInternal(@NotNull String language, pageToUpdate.setStatus(pageProperties.getStatus() != null ? pageProperties.getStatus() : pageToUpdate.getStatus()); pageToUpdate.setLabel(newLabel); pageToUpdate.setIcon(newIcon); + + pageToUpdate.getGroups().clear(); + if (pageToUpdate.getStatus() == Page.PageStatus.GROUPS) { + pageToUpdate.getGroups().addAll(_groups); + } + pageRepository.save(pageToUpdate); + } return ResponseEntity.noContent().build(); @@ -349,7 +405,7 @@ public ResponseEntity getPageContent( final UserSession us = ApiUtils.getUserSession(session); if (page.get().getStatus().equals(Page.PageStatus.HIDDEN) && us.getProfile() != Profile.Administrator) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); - } else if (page.get().getStatus().equals(Page.PageStatus.PRIVATE) && (us.getProfile() == null || us.getProfile() == Profile.Guest)) { + } else if ((page.get().getStatus().equals(Page.PageStatus.PRIVATE) || page.get().getStatus().equals(Page.PageStatus.GROUPS)) && (us.getProfile() == null || us.getProfile() == Profile.Guest)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } else { String content; @@ -383,6 +439,7 @@ public ResponseEntity> listPages( @Parameter(hidden = true) final HttpSession session) { final UserSession us = ApiUtils.getUserSession(session); + List unfilteredResult; if (language == null) { @@ -396,15 +453,16 @@ public ResponseEntity> listPages( for (final Page page : unfilteredResult) { if (page.getStatus().equals(Page.PageStatus.HIDDEN) && us.getProfile() == Profile.Administrator || page.getStatus().equals(Page.PageStatus.PRIVATE) && us.getProfile() != null && us.getProfile() != Profile.Guest + || page.getStatus().equals(Page.PageStatus.GROUPS) && us.getProfile() != null && us.getProfile() != Profile.Guest && checkGroupPermission(us, page) || page.getStatus().equals(Page.PageStatus.PUBLIC) || page.getStatus().equals(Page.PageStatus.PUBLIC_ONLY) && !us.isAuthenticated()) { if (section == null) { - filteredResult.add(new org.fao.geonet.api.pages.PageProperties(page)); + filteredResult.add(new PageProperties(page)); } else { final List sections = page.getSections(); final boolean containsRequestedSection = sections.contains(section); if (containsRequestedSection) { - filteredResult.add(new org.fao.geonet.api.pages.PageProperties(page)); + filteredResult.add(new PageProperties(page)); } } } @@ -497,17 +555,17 @@ private void checkValidLanguage(String language) { * @param page the page * @return the response entity */ - private ResponseEntity checkPermissionsOnSinglePageAndReturn(final HttpSession session, final Page page) { + private ResponseEntity checkPermissionsOnSinglePageAndReturn(final HttpSession session, final Page page) { if (page == null) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } else { final UserSession us = ApiUtils.getUserSession(session); if (page.getStatus().equals(Page.PageStatus.HIDDEN) && us.getProfile() != Profile.Administrator) { return new ResponseEntity<>(HttpStatus.FORBIDDEN); - } else if (page.getStatus().equals(Page.PageStatus.PRIVATE) && (us.getProfile() == null || us.getProfile() == Profile.Guest)) { + } else if ((page.getStatus().equals(Page.PageStatus.PRIVATE) || page.getStatus().equals(Page.PageStatus.GROUPS)) && (us.getProfile() == null || us.getProfile() == Profile.Guest)) { return new ResponseEntity<>(HttpStatus.FORBIDDEN); } else { - return new ResponseEntity<>(new org.fao.geonet.api.pages.PageProperties(page), HttpStatus.OK); + return new ResponseEntity<>(new PageProperties(page), HttpStatus.OK); } } } @@ -536,7 +594,7 @@ private Page searchPage(final String language, final String pageId, final PageRe */ protected Page getEmptyHiddenDraftPage(final String language, final String pageId, final String label, final String icon, final Page.PageFormat format) { final List sections = new ArrayList<>(); - return new Page(new PageIdentity(language, pageId), null, null, format, sections, Page.PageStatus.HIDDEN, label, icon); + return new Page(new PageIdentity(language, pageId), null, null, format, sections, Page.PageStatus.HIDDEN, label, icon, null); } /** @@ -569,4 +627,36 @@ private void fillContent(final MultipartFile data, } } + + /** + * Check is the user is in designated group to access the static page when page permission level is set to GROUP + * @param us Current User Session + * @param page static page object + * @return permission granted + */ + private boolean checkGroupPermission (UserSession us, Page page) { + boolean isGranted = false; + String currentUserId = us.getUserId(); + + if (us.getProfile() == Profile.Administrator) { + isGranted = true; + } else if (page.getStatus().equals(Page.PageStatus.GROUPS) && StringUtils.isNotEmpty(currentUserId)) { + List userGroups = userGroupRepository.findAll(UserGroupSpecs.hasUserId(Integer.parseInt(currentUserId))); + + Set accessingGroups = page.getGroups(); + + if (CollectionUtils.isNotEmpty(userGroups) && CollectionUtils.isNotEmpty(accessingGroups)) { + for (UserGroup userGroup : userGroups) { + for (Group group : accessingGroups) { + if (org.apache.commons.lang3.StringUtils.equals(userGroup.getGroup().getName(), group.getName())) { + isGranted = true; + break; + } + } + } + } + } + + return isGranted; + } } diff --git a/web-ui/src/main/resources/catalog/js/admin/StaticPagesController.js b/web-ui/src/main/resources/catalog/js/admin/StaticPagesController.js index 69656db2847..74bc2be7ad8 100644 --- a/web-ui/src/main/resources/catalog/js/admin/StaticPagesController.js +++ b/web-ui/src/main/resources/catalog/js/admin/StaticPagesController.js @@ -41,6 +41,7 @@ $scope.staticPageSelected = null; $scope.queue = []; $scope.uploadScope = angular.element("#gn-static-page-edit").scope(); + $scope.groups = []; $scope.unsupportedFile = false; $scope.$watchCollection("queue", function (n, o) { @@ -64,6 +65,12 @@ }); } + function loadGroups() { + $http.get("../api/groups").then(function (r) { + $scope.groups = r.data; + }); + } + function loadStaticPages() { $scope.staticPageSelected = null; $http.get("../api/pages").then(function (r) { @@ -144,6 +151,7 @@ $scope.addStaticPage = function () { $scope.isUpdate = false; + $scope.isGroupEnabled = false; $scope.staticPageSelected = { language: "", pageId: "", @@ -152,6 +160,7 @@ data: "", content: "", status: "HIDDEN", + groups: "", label: "", sections: [] }; @@ -164,6 +173,7 @@ $scope.selectStaticPage = function (v) { $scope.isUpdate = true; $scope.staticPageSelected = v; + $scope.isGroupEnabled = $scope.staticPageSelected.status == "GROUPS"; var link = "api/pages/" + @@ -214,6 +224,12 @@ $scope.uploadScope.submit(); } else { delete sp.data; + + // Reset empty string to null to avoid parsing error + if (sp.groups == "") { + sp.groups = null; + } + return $http .put(action, sp, { headers: { @@ -225,6 +241,13 @@ }); } }; + $scope.updateGroupSelection = function () { + if ($scope.staticPageSelected.status === "GROUPS") { + $scope.isGroupEnabled = true; + } else { + $scope.isGroupEnabled = false; + } + }; $scope.deleteStaticPageConfig = function () { $("#gn-confirm-remove-static-page").modal("show"); @@ -262,6 +285,7 @@ loadFormats(); loadDbLanguages(); loadStaticPages(); + loadGroups(); } ]); })(); diff --git a/web-ui/src/main/resources/catalog/locales/en-v4.json b/web-ui/src/main/resources/catalog/locales/en-v4.json index 59927c5984e..b200ed3f3d9 100644 --- a/web-ui/src/main/resources/catalog/locales/en-v4.json +++ b/web-ui/src/main/resources/catalog/locales/en-v4.json @@ -419,6 +419,7 @@ "staticPageFormat-TEXT": "Plain text content", "staticPageStatus-HIDDEN": "Visible only to the administrator", "staticPageStatus-PRIVATE": "Visible to logged users", + "staticPageStatus-GROUPS": "Visible to users belonging to the groups", "staticPageStatus-PUBLIC": "Visible to everyone", "pageLink": "Link", "pageSection-help": "Currently, the default UI view only supports TOP and FOOTER values. Custom UI views can make use of additional values.", diff --git a/web-ui/src/main/resources/catalog/templates/admin/settings/static-pages.html b/web-ui/src/main/resources/catalog/templates/admin/settings/static-pages.html index 90eec4c0449..7e469580a40 100644 --- a/web-ui/src/main/resources/catalog/templates/admin/settings/static-pages.html +++ b/web-ui/src/main/resources/catalog/templates/admin/settings/static-pages.html @@ -306,15 +306,36 @@ class="form-control" required="" data-ng-model="staticPageSelected.status" + data-ng-change="updateGroupSelection()" > + + +
+ + +
+ +
+