diff --git a/pom.xml b/pom.xml
index bfe8e24dc..37a7f9b81 100644
--- a/pom.xml
+++ b/pom.xml
@@ -482,6 +482,13 @@
${geojson-jackson.version}
+
+
+ com.jayway.jsonpath
+ json-path
+ 2.8.0
+
+
org.keycloak
diff --git a/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/ApplicationControllerTest.java b/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/ApplicationControllerTest.java
index d970b7bdd..2f256e0d8 100644
--- a/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/ApplicationControllerTest.java
+++ b/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/ApplicationControllerTest.java
@@ -17,12 +17,21 @@
package de.terrestris.shogun.boot.controller;
import de.terrestris.shogun.lib.controller.ApplicationController;
+import de.terrestris.shogun.lib.enumeration.PermissionCollectionType;
import de.terrestris.shogun.lib.model.Application;
import de.terrestris.shogun.lib.repository.ApplicationRepository;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.util.ArrayList;
import java.util.List;
+import static org.hamcrest.Matchers.hasSize;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+
public class ApplicationControllerTest extends BaseControllerTest {
public void setBaseEntity() {
@@ -53,4 +62,56 @@ public void insertTestData() {
testData = persistedEntities;
}
+ @Test
+ public void findAll_shouldReturnFilteredEntitiesForAdminUsers() throws Exception {
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .get(basePath + "?filter=$.name == \"Application 1\"")
+ .with(authentication(getMockAuthentication(this.adminUser)))
+ )
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$").isMap())
+ .andExpect(jsonPath("$.content").isArray())
+ .andExpect(jsonPath("$.content", hasSize(1)));
+ }
+
+ @Test
+ public void findAll_shouldReturnFilteredEntitiesForUsersByUserInstancePermission() throws Exception {
+
+ userInstancePermissionService.setPermission(testData.get(0), this.user, PermissionCollectionType.READ);
+ userInstancePermissionService.setPermission(testData.get(1), this.user, PermissionCollectionType.READ);
+
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .get(basePath + "?filter=$.name == \"Application 1\"")
+ .with(authentication(getMockAuthentication(this.user)))
+ )
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$").isMap())
+ .andExpect(jsonPath("$.content").isArray())
+ .andExpect(jsonPath("$.content", hasSize(1)));
+ }
+
+ @Test
+ public void findAll_shouldReturnFilteredEntitiesForUsersByGroupInstancePermission() throws Exception {
+
+ groupInstancePermissionService.setPermission(testData.get(0), this.group, PermissionCollectionType.READ);
+ groupInstancePermissionService.setPermission(testData.get(1), this.group, PermissionCollectionType.READ);
+
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .get(basePath + "?filter=$.name == \"Application 1\"")
+ .with(authentication(getMockAuthentication(this.user)))
+ )
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$").isMap())
+ .andExpect(jsonPath("$.content").isArray())
+ .andExpect(jsonPath("$.content", hasSize(1)));
+ }
}
diff --git a/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/BaseControllerTest.java b/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/BaseControllerTest.java
index 4f3fada17..9536218c0 100644
--- a/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/BaseControllerTest.java
+++ b/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/BaseControllerTest.java
@@ -24,20 +24,28 @@
import de.terrestris.shogun.lib.controller.BaseController;
import de.terrestris.shogun.lib.enumeration.PermissionCollectionType;
import de.terrestris.shogun.lib.model.BaseEntity;
+import de.terrestris.shogun.lib.model.Group;
import de.terrestris.shogun.lib.model.User;
import de.terrestris.shogun.lib.repository.BaseCrudRepository;
+import de.terrestris.shogun.lib.repository.GroupRepository;
import de.terrestris.shogun.lib.repository.UserRepository;
+import de.terrestris.shogun.lib.repository.security.permission.GroupClassPermissionRepository;
+import de.terrestris.shogun.lib.repository.security.permission.GroupInstancePermissionRepository;
import de.terrestris.shogun.lib.repository.security.permission.UserClassPermissionRepository;
import de.terrestris.shogun.lib.repository.security.permission.UserInstancePermissionRepository;
+import de.terrestris.shogun.lib.service.security.permission.GroupInstancePermissionService;
import de.terrestris.shogun.lib.service.security.permission.UserClassPermissionService;
import de.terrestris.shogun.lib.service.security.permission.UserInstancePermissionService;
+import de.terrestris.shogun.lib.service.security.provider.GroupProviderService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.keycloak.representations.IDToken;
+import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.web.servlet.server.Encoding;
import org.springframework.http.MediaType;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -57,6 +65,7 @@
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
@@ -79,15 +88,27 @@ public abstract class BaseControllerTest entityClass;
protected String basePath;
@@ -109,6 +133,8 @@ public abstract class BaseControllerTest group = new Group();
+ String authProviderId = "c886684b-1a22-4443-b0d8-36c006c19f08";
+ group.setAuthProviderId(authProviderId);
+ GroupRepresentation groupRepresentation = new GroupRepresentation();
+ groupRepresentation.setName("SHOGun Group");
+ group.setProviderDetails(groupRepresentation);
+
+ this.group = groupRepository.save(group);
+
+ when(groupProviderService.getGroupsForUser()).thenReturn(List.of(this.group));
+ when(groupProviderService.findByUser(this.user)).thenReturn(List.of(this.group));
+ }
+
public void cleanupPermissions() {
userInstancePermissionRepository.deleteAll();
+ groupInstancePermissionRepository.deleteAll();
userClassPermissionRepository.deleteAll();
+ groupClassPermissionRepository.deleteAll();
}
public void deinitAdminUser() {
@@ -162,6 +204,10 @@ public void deinitUser() {
userRepository.delete(this.user);
}
+ public void deinitGroup() {
+ groupRepository.delete(this.group);
+ }
+
public void cleanupTestData() {
repository.deleteAll();
}
@@ -192,6 +238,7 @@ public void setUp() {
initMockMvc();
initAdminUser();
initUser();
+ initGroup();
setBaseEntity();
setBasePath();
insertTestData();
@@ -202,6 +249,7 @@ public void teardown() {
cleanupPermissions();
deinitUser();
deinitAdminUser();
+ deinitGroup();
cleanupTestData();
}
diff --git a/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/GroupControllerTest.java b/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/GroupControllerTest.java
index c115b233a..114996c85 100644
--- a/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/GroupControllerTest.java
+++ b/shogun-boot/src/test/java/de/terrestris/shogun/boot/controller/GroupControllerTest.java
@@ -16,16 +16,33 @@
*/
package de.terrestris.shogun.boot.controller;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import de.terrestris.shogun.lib.controller.GroupController;
+import de.terrestris.shogun.lib.enumeration.PermissionCollectionType;
import de.terrestris.shogun.lib.model.Group;
import de.terrestris.shogun.lib.repository.GroupRepository;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.web.servlet.server.Encoding;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
+import static org.hamcrest.Matchers.hasKey;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.Assert.assertEquals;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+
public class GroupControllerTest extends BaseControllerTest {
+ private int numberOfInternalTestGroups = 1;
+
public void setBaseEntity() {
entityClass = Group.class;
}
@@ -52,4 +69,204 @@ public void insertTestData() {
testData = (List) repository.saveAll(entities);
}
+ @Test
+ @Override
+ public void findAll_shouldReturnAllAvailableEntitiesForRoleAdmin() throws Exception {
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .get(basePath)
+ .with(authentication(getMockAuthentication(this.adminUser)))
+ )
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$").isMap())
+ .andExpect(jsonPath("$.content").isArray())
+ .andExpect(jsonPath("$.content", hasSize(testData.size() + numberOfInternalTestGroups)));
+ }
+
+ @Test
+ @Override
+ public void findAll_shouldReturnAllAvailableEntitiesWithUserClassPermissions() throws Exception {
+
+ userClassPermissionService.setPermission(entityClass, this.user, PermissionCollectionType.READ);
+
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .get(basePath)
+ .with(authentication(getMockAuthentication(this.user)))
+ )
+ .andExpect(MockMvcResultMatchers.status().isOk())
+ .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$").isMap())
+ .andExpect(jsonPath("$.content").isArray())
+ .andExpect(jsonPath("$.content", hasSize(testData.size() + numberOfInternalTestGroups)));
+ }
+
+ @Test
+ @Override
+ public void add_shouldDenyAccessForRoleAnonymous() throws Exception {
+ JsonNode insertNode = objectMapper.valueToTree(testData.get(0));
+ List fieldsToRemove = List.of("id", "created", "modified");
+ insertNode = ((ObjectNode) insertNode).remove(fieldsToRemove);
+
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .post(String.format("%s", basePath))
+ .contentType(MediaType.APPLICATION_JSON)
+ .characterEncoding(Encoding.DEFAULT_CHARSET.toString())
+ .content(objectMapper.writeValueAsString(insertNode))
+ .with(csrf())
+ )
+ .andExpect(MockMvcResultMatchers.status().isUnauthorized());
+
+ List persistedEntities = repository.findAll();
+ assertEquals(testData.size() + numberOfInternalTestGroups, persistedEntities.size());
+ }
+
+ @Test
+ @Override
+ public void add_shouldDenyAccessForRoleUserWithoutExplicitPermission() throws Exception {
+ JsonNode insertNode = objectMapper.valueToTree(testData.get(0));
+ List fieldsToRemove = List.of("id", "created", "modified");
+ insertNode = ((ObjectNode) insertNode).remove(fieldsToRemove);
+
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .post(String.format("%s", basePath))
+ .contentType(MediaType.APPLICATION_JSON)
+ .characterEncoding(Encoding.DEFAULT_CHARSET.toString())
+ .content(objectMapper.writeValueAsString(insertNode))
+ .with(authentication(getMockAuthentication(this.user)))
+ .with(csrf())
+ )
+ .andExpect(MockMvcResultMatchers.status().isNotFound());
+
+ List persistedEntities = repository.findAll();
+ assertEquals(testData.size() + numberOfInternalTestGroups, persistedEntities.size());
+ }
+
+ @Test
+ @Override
+ public void add_shouldCreateTheEntityForRoleUser() throws Exception {
+
+ userClassPermissionService.setPermission(entityClass, this.user, PermissionCollectionType.CREATE);
+
+ JsonNode insertNode = objectMapper.valueToTree(testData.get(0));
+ List fieldsToRemove = List.of("id", "created", "modified");
+ insertNode = ((ObjectNode) insertNode).remove(fieldsToRemove);
+
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .post(String.format("%s", basePath))
+ .contentType(MediaType.APPLICATION_JSON)
+ .characterEncoding(Encoding.DEFAULT_CHARSET.toString())
+ .content(objectMapper.writeValueAsString(insertNode))
+ .with(authentication(getMockAuthentication(this.adminUser)))
+ .with(csrf())
+ )
+ .andExpect(MockMvcResultMatchers.status().isCreated())
+ .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$").exists())
+ .andExpect(jsonPath("$", hasKey("id")));
+
+ List persistedEntities = repository.findAll();
+ assertEquals(testData.size() + numberOfInternalTestGroups + 1, persistedEntities.size());
+ }
+
+ @Test
+ @Override
+ public void add_shouldCreateTheEntityForRoleAdmin() throws Exception {
+ JsonNode insertNode = objectMapper.valueToTree(testData.get(0));
+ List fieldsToRemove = List.of("id", "created", "modified");
+ insertNode = ((ObjectNode) insertNode).remove(fieldsToRemove);
+
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .post(String.format("%s", basePath))
+ .contentType(MediaType.APPLICATION_JSON)
+ .characterEncoding(Encoding.DEFAULT_CHARSET.toString())
+ .content(objectMapper.writeValueAsString(insertNode))
+ .with(authentication(getMockAuthentication(this.adminUser)))
+ .with(csrf())
+ )
+ .andExpect(MockMvcResultMatchers.status().isCreated())
+ .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$").exists())
+ .andExpect(jsonPath("$", hasKey("id")));
+
+ List persistedEntities = repository.findAll();
+ assertEquals(testData.size() + numberOfInternalTestGroups + 1, persistedEntities.size());
+ }
+
+ @Test
+ @Override
+ public void delete_shouldDenyAccessForRoleAnonymous() throws Exception {
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .delete(String.format("%s/%s", basePath, testData.get(0).getId()))
+ .with(csrf())
+ )
+ .andExpect(MockMvcResultMatchers.status().isUnauthorized());
+
+ List persistedEntities = repository.findAll();
+ assertEquals(testData.size() + numberOfInternalTestGroups, persistedEntities.size());
+ }
+
+ @Test
+ @Override
+ public void delete_shouldDenyAccessForRoleUserWithoutExplicitPermission() throws Exception {
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .delete(String.format("%s/%s", basePath, testData.get(0).getId()))
+ .with(authentication(getMockAuthentication(this.user)))
+ .with(csrf())
+ )
+ .andExpect(MockMvcResultMatchers.status().isNotFound());
+
+ List persistedEntities = repository.findAll();
+ assertEquals(testData.size() + numberOfInternalTestGroups, persistedEntities.size());
+ }
+
+ @Test
+ @Override
+ public void delete_shouldDeleteAnAvailableEntityForRoleUser() throws Exception {
+
+ userInstancePermissionService.setPermission(testData.get(0), this.user, PermissionCollectionType.DELETE);
+
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .delete(String.format("%s/%s", basePath, testData.get(0).getId()))
+ .with(authentication(getMockAuthentication(this.adminUser)))
+ .with(csrf())
+ )
+ .andExpect(MockMvcResultMatchers.status().isNoContent());
+
+ List persistedEntities = repository.findAll();
+ assertEquals(testData.size() + numberOfInternalTestGroups - 1, persistedEntities.size());
+ }
+
+ @Test
+ @Override
+ public void delete_shouldDeleteAnAvailableEntityForRoleAdmin() throws Exception {
+ this.mockMvc
+ .perform(
+ MockMvcRequestBuilders
+ .delete(String.format("%s/%s", basePath, testData.get(0).getId()))
+ .with(authentication(getMockAuthentication(this.adminUser)))
+ .with(csrf())
+ )
+ .andExpect(MockMvcResultMatchers.status().isNoContent());
+
+ List persistedEntities = repository.findAll();
+ assertEquals(testData.size() + numberOfInternalTestGroups - 1, persistedEntities.size());
+ }
}
diff --git a/shogun-lib/pom.xml b/shogun-lib/pom.xml
index db18ab20c..6a274c059 100644
--- a/shogun-lib/pom.xml
+++ b/shogun-lib/pom.xml
@@ -212,6 +212,11 @@
tika-core
+
+ com.jayway.jsonpath
+ json-path
+
+
org.springframework
diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/BaseController.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/BaseController.java
index 71ce4b7b4..1287ecdf9 100644
--- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/BaseController.java
+++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/BaseController.java
@@ -17,16 +17,20 @@
package de.terrestris.shogun.lib.controller;
import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
+import com.jayway.jsonpath.Filter;
import de.terrestris.shogun.lib.controller.security.permission.BasePermissionController;
import de.terrestris.shogun.lib.model.BaseEntity;
import de.terrestris.shogun.lib.service.BaseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.extern.log4j.Log4j2;
+import org.apache.commons.lang3.StringUtils;
import org.springdoc.api.annotations.ParameterObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
@@ -64,6 +68,34 @@ public abstract class BaseController, S extends Base
summary = "Returns all entities",
security = { @SecurityRequirement(name = "bearer-key") }
)
+ @Parameters(
+ @Parameter(
+ name = "filter",
+ description = "JSONPath predicate, see https://github.com/json-path/JsonPath#predicates",
+ examples = {
+ @ExampleObject(
+ name = "Example 1",
+ description = "Get all entities with name \"Countries\"",
+ value = "$.name == \"Countries\""
+ ),
+ @ExampleObject(
+ name = "Example 2",
+ description = "Get all hoverable layers",
+ value = "$.clientConfig.hoverable == true"
+ ),
+ @ExampleObject(
+ name = "Example 3",
+ description = "Get all layers with a greater minResolution of 100 and of type TILEWMS",
+ value = "$.clientConfig.minResolution > 100 && $.type == \"TILEWMS\""
+ ),
+ @ExampleObject(
+ name = "Example 4",
+ description = "Get all entities with the names \"Countries\" or \"World Map\"",
+ value = "$.name == \"Countries\" || $.name == \"World Map\""
+ )
+ }
+ )
+ )
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
@@ -80,14 +112,21 @@ public abstract class BaseController, S extends Base
),
@ApiResponse(
responseCode = "500",
- description = "Internal Server Error: Something internal went wrong while deleting the entity"
+ description = "Internal Server Error: Something internal went wrong while getting the entity list"
)
})
- public Page findAll(@PageableDefault(Integer.MAX_VALUE) @ParameterObject Pageable pageable) {
+ public Page findAll(@PageableDefault(Integer.MAX_VALUE) @ParameterObject Pageable pageable, @RequestParam(required = false) String filter) {
log.trace("Requested to return all entities of type {}", getGenericClassName());
try {
- Page persistedEntities = service.findAll(pageable);
+ Filter compiledFilter = null;
+ if (StringUtils.isNotEmpty(filter)) {
+ compiledFilter = Filter.parse(String.format("[?(%s)]", filter));
+
+ log.trace("Got filter " + compiledFilter.toString().replace("'", "\""));
+ }
+
+ Page persistedEntities = service.findAll(pageable, compiledFilter);
log.trace("Successfully got all entities of type {} (count: {})",
getGenericClassName(), persistedEntities.getTotalElements());
diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/repository/BaseCrudRepository.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/repository/BaseCrudRepository.java
index ee9eed334..b3912c6da 100644
--- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/repository/BaseCrudRepository.java
+++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/repository/BaseCrudRepository.java
@@ -16,6 +16,7 @@
*/
package de.terrestris.shogun.lib.repository;
+import com.jayway.jsonpath.Filter;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
@@ -44,17 +45,49 @@ public interface BaseCrudRepository extends
* @param userId ID of the authenticated user.
* @return A page of entities.
*/
- @Query("""
- FROM #{#entityName} m
- WHERE EXISTS (
- SELECT 1 FROM userinstancepermissions uip
- WHERE uip.user.id = :userId
- AND uip.entityId = m.id
- AND uip.permission.name IN ('ADMIN', 'READ', 'CREATE_READ', 'CREATE_READ_UPDATE', 'CREATE_READ_DELETE', 'READ_UPDATE', 'READ_DELETE', 'READ_UPDATE_DELETE')
- )
- """)
- @QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
- Page findAll(Pageable pageable, Long userId);
+ @Query(
+ value = """
+ SELECT
+ *
+ FROM
+ {h-schema}#{#entityName} e
+ WHERE (EXISTS (
+ SELECT
+ 1
+ FROM
+ {h-schema}userinstancepermissions uip
+ LEFT JOIN {h-schema}permissions p ON p.id = uip.permission_id
+ WHERE
+ uip.user_id = :userId AND
+ uip.entity_id = e.id AND
+ p."name" IN (
+ 'ADMIN',
+ 'READ',
+ 'CREATE_READ',
+ 'CREATE_READ_UPDATE',
+ 'CREATE_READ_DELETE',
+ 'READ_UPDATE',
+ 'READ_DELETE',
+ 'READ_UPDATE_DELETE'
+ )
+ )) AND (
+ :#{T(de.terrestris.shogun.lib.util.JsonPathFilterUtil).writeFilter(#filter)} = '$' OR CAST(row_to_json(e) AS JSONB) @@ CAST(:#{T(de.terrestris.shogun.lib.util.JsonPathFilterUtil).writeFilter(#filter)} AS JSONPATH)
+ )
+ """,
+ // TODO This should include the exists filter
+ countQuery = """
+ SELECT
+ COUNT(e.*)
+ FROM
+ {h-schema}#{#entityName} e
+ """,
+ nativeQuery = true
+ )
+ @QueryHints(
+ value = @QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"),
+ forCounting = false
+ )
+ Page findAll(Pageable pageable, Filter filter, Long userId);
/**
* Returns a {@link Page} of entities for which the user with userId has permission via UserInstancePermission or GroupInstancePermission.
@@ -64,22 +97,67 @@ AND uip.permission.name IN ('ADMIN', 'READ', 'CREATE_READ', 'CREATE_READ_UPDATE'
* @param groupIds All IDs of the groups of the authenticated user.
* @return A page of entities.
*/
- @Query("""
- FROM #{#entityName} m
- WHERE EXISTS (
- SELECT 1 FROM userinstancepermissions uip
- WHERE uip.user.id = :userId
- AND uip.entityId = m.id
- AND uip.permission.name IN ('ADMIN', 'READ', 'CREATE_READ', 'CREATE_READ_UPDATE', 'CREATE_READ_DELETE', 'READ_UPDATE', 'READ_DELETE', 'READ_UPDATE_DELETE')
- ) OR EXISTS (
- SELECT 1 FROM groupinstancepermissions gip
- WHERE gip.group.id IN :groupIds
- AND gip.entityId = m.id
- AND gip.permission.name IN ('ADMIN', 'READ', 'CREATE_READ', 'CREATE_READ_UPDATE', 'CREATE_READ_DELETE', 'READ_UPDATE', 'READ_DELETE', 'READ_UPDATE_DELETE')
- )
- """)
- @QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
- Page findAll(Pageable pageable, Long userId, List groupIds);
+ @Query(
+ value = """
+ SELECT
+ *
+ FROM
+ {h-schema}#{#entityName} e
+ WHERE (EXISTS (
+ SELECT
+ 1
+ FROM
+ {h-schema}userinstancepermissions uip
+ LEFT JOIN {h-schema}permissions p ON p.id = uip.permission_id
+ WHERE
+ uip.user_id = :userId AND
+ uip.entity_id = e.id AND
+ p."name" IN (
+ 'ADMIN',
+ 'READ',
+ 'CREATE_READ',
+ 'CREATE_READ_UPDATE',
+ 'CREATE_READ_DELETE',
+ 'READ_UPDATE',
+ 'READ_DELETE',
+ 'READ_UPDATE_DELETE'
+ )
+ ) OR EXISTS (
+ SELECT
+ 1
+ FROM
+ {h-schema}groupinstancepermissions gip
+ LEFT JOIN {h-schema}permissions p ON p.id = gip.permission_id
+ WHERE
+ gip.group_id IN :groupIds AND
+ gip.entity_id = e.id AND
+ p."name" IN (
+ 'ADMIN',
+ 'READ',
+ 'CREATE_READ',
+ 'CREATE_READ_UPDATE',
+ 'CREATE_READ_DELETE',
+ 'READ_UPDATE',
+ 'READ_DELETE',
+ 'READ_UPDATE_DELETE'
+ )
+ )) AND (
+ :#{T(de.terrestris.shogun.lib.util.JsonPathFilterUtil).writeFilter(#filter)} = '$' OR CAST(row_to_json(e) AS JSONB) @@ CAST(:#{T(de.terrestris.shogun.lib.util.JsonPathFilterUtil).writeFilter(#filter)} AS JSONPATH)
+ )
+ """,
+ countQuery = """
+ SELECT
+ COUNT(e.*)
+ FROM
+ {h-schema}#{#entityName} e
+ """,
+ nativeQuery = true
+ )
+ @QueryHints(
+ value = @QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"),
+ forCounting = false
+ )
+ Page findAll(Pageable pageable, Filter filter, Long userId, List groupIds);
/**
* Returns a {@link Page} of entities without checking any permissions.
@@ -88,7 +166,27 @@ AND gip.permission.name IN ('ADMIN', 'READ', 'CREATE_READ', 'CREATE_READ_UPDATE'
* {@literal null}.
* @return A page of entities.
*/
- @QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"))
- Page findAll(Pageable pageable);
+ @Query(
+ value = """
+ SELECT
+ e.*
+ FROM
+ {h-schema}#{#entityName} e
+ WHERE
+ :#{T(de.terrestris.shogun.lib.util.JsonPathFilterUtil).writeFilter(#filter)} = '$' OR CAST(row_to_json(e) AS JSONB) @@ CAST(:#{T(de.terrestris.shogun.lib.util.JsonPathFilterUtil).writeFilter(#filter)} AS JSONPATH)
+ """,
+ countQuery = """
+ SELECT
+ COUNT(e.*)
+ FROM
+ {h-schema}#{#entityName} e
+ """,
+ nativeQuery = true
+ )
+ @QueryHints(
+ value = @QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE, value = "true"),
+ forCounting = false
+ )
+ Page findAll(Pageable pageable, Filter filter);
}
diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/BaseEntityPermissionEvaluator.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/BaseEntityPermissionEvaluator.java
index 29d55fad7..02c45df16 100644
--- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/BaseEntityPermissionEvaluator.java
+++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/BaseEntityPermissionEvaluator.java
@@ -16,6 +16,7 @@
*/
package de.terrestris.shogun.lib.security.access.entity;
+import com.jayway.jsonpath.Filter;
import de.terrestris.shogun.lib.enumeration.PermissionType;
import de.terrestris.shogun.lib.model.BaseEntity;
import de.terrestris.shogun.lib.model.Group;
@@ -167,7 +168,6 @@ public boolean hasPermission(User user, Class> clazz, PermissionType permissio
log.trace("Evaluating whether user with ID '{}' has permission '{}' on class '{}'",
user.getId(), permission, clazz.getCanonicalName());
-
Optional userClassPermission = userClassPermissionService.findFor((Class extends BaseEntity>) clazz, user);
Optional groupClassPermission = groupClassPermissionService.findFor((Class extends BaseEntity>) clazz, user);
@@ -255,7 +255,7 @@ public boolean hasPermissionByGroupClassPermission(User user, BaseEntity entity,
* @return A page of entities.
*/
@Override
- public Page findAll(User user, Pageable pageable, BaseCrudRepository repository, Class baseEntityClass) {
+ public Page findAll(User user, Pageable pageable, Filter filter, BaseCrudRepository repository, Class baseEntityClass) {
if (user == null) {
throw new RuntimeException("No user provided!");
}
@@ -269,7 +269,7 @@ public Page findAll(User user, Pageable pageable, BaseCrudRepository
);
if (isAdmin) {
- return repository.findAll(pageable);
+ return repository.findAll(pageable, filter);
}
// option B: user has permission through class permissions
@@ -277,20 +277,20 @@ public Page findAll(User user, Pageable pageable, BaseCrudRepository
Optional groupClassPermission = groupClassPermissionService.findFor(baseEntityClass, user);
if (containsReadPermission(userClassPermission.orElse(null), groupClassPermission.orElse(null))) {
- return repository.findAll(pageable);
+ return repository.findAll(pageable, filter);
}
// option C: check instance permissions for each entity with a single query
List> userGroups = groupProviderService.getGroupsForUser();
if (userGroups.isEmpty()) {
// user has no groups so only user instance permissions have to be checked
- return repository.findAll(pageable, user.getId());
+ return repository.findAll(pageable, filter, user.getId());
} else {
// check both user and group instance permissions
List groupIds = userGroups.stream()
.map(BaseEntity::getId)
.toList();
- return repository.findAll(pageable, user.getId(), groupIds);
+ return repository.findAll(pageable, filter, user.getId(), groupIds);
}
}
diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/EntityPermissionEvaluator.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/EntityPermissionEvaluator.java
index c3e6b67f7..672df3a3f 100644
--- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/EntityPermissionEvaluator.java
+++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/EntityPermissionEvaluator.java
@@ -16,6 +16,7 @@
*/
package de.terrestris.shogun.lib.security.access.entity;
+import com.jayway.jsonpath.Filter;
import de.terrestris.shogun.lib.enumeration.PermissionType;
import de.terrestris.shogun.lib.model.User;
import de.terrestris.shogun.lib.repository.BaseCrudRepository;
@@ -37,5 +38,5 @@ public interface EntityPermissionEvaluator {
* with pagination. See {@link BaseEntityPermissionEvaluator#findAll(User, Pageable, BaseCrudRepository)} for the
* default implementation for {@link de.terrestris.shogun.lib.model.BaseEntity}.
*/
- Page findAll(User user, Pageable pageable, BaseCrudRepository repository, Class baseEntityClass);
+ Page findAll(User user, Pageable pageable, Filter filter, BaseCrudRepository repository, Class baseEntityClass);
}
diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/GroupPermissionEvaluator.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/GroupPermissionEvaluator.java
index 6a37fc64e..a2d771bf9 100644
--- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/GroupPermissionEvaluator.java
+++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/GroupPermissionEvaluator.java
@@ -16,6 +16,7 @@
*/
package de.terrestris.shogun.lib.security.access.entity;
+import com.jayway.jsonpath.Filter;
import de.terrestris.shogun.lib.model.Group;
import de.terrestris.shogun.lib.model.User;
import de.terrestris.shogun.lib.repository.BaseCrudRepository;
@@ -32,9 +33,9 @@ public class GroupPermissionEvaluator extends BaseEntityPermissionEvaluator findAll(User user, Pageable pageable, BaseCrudRepository repository,
- Class baseEntityClass) {
- Page groups = super.findAll(user, pageable, repository, baseEntityClass);
+ public Page findAll(User user, Pageable pageable, Filter filter, BaseCrudRepository repository,
+ Class baseEntityClass) {
+ Page groups = super.findAll(user, pageable, filter, repository, baseEntityClass);
groups.forEach(u -> groupProviderService.setTransientRepresentations(u));
diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/UserPermissionEvaluator.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/UserPermissionEvaluator.java
index f7ac61b78..900d14f33 100644
--- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/UserPermissionEvaluator.java
+++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/security/access/entity/UserPermissionEvaluator.java
@@ -16,6 +16,7 @@
*/
package de.terrestris.shogun.lib.security.access.entity;
+import com.jayway.jsonpath.Filter;
import de.terrestris.shogun.lib.model.User;
import de.terrestris.shogun.lib.repository.BaseCrudRepository;
import de.terrestris.shogun.lib.service.security.provider.UserProviderService;
@@ -31,9 +32,9 @@ public class UserPermissionEvaluator extends BaseEntityPermissionEvaluator
UserProviderService userProviderService;
@Override
- public Page findAll(User user, Pageable pageable, BaseCrudRepository repository,
- Class baseEntityClass) {
- Page users = super.findAll(user, pageable, repository, baseEntityClass);
+ public Page findAll(User user, Pageable pageable, Filter filter, BaseCrudRepository repository,
+ Class baseEntityClass) {
+ Page users = super.findAll(user, pageable, filter, repository, baseEntityClass);
users.forEach(u -> userProviderService.setTransientRepresentations(u));
diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/BaseService.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/BaseService.java
index 4118013d6..a29e13758 100644
--- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/BaseService.java
+++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/BaseService.java
@@ -20,6 +20,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jsonpatch.JsonPatchException;
import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
+import com.jayway.jsonpath.Filter;
import de.terrestris.shogun.lib.enumeration.PermissionCollectionType;
import de.terrestris.shogun.lib.model.BaseEntity;
import de.terrestris.shogun.lib.model.User;
@@ -89,7 +90,7 @@ public List findAll() {
}
@Transactional(readOnly = true)
- public Page findAll(Pageable pageable) {
+ public Page findAll(Pageable pageable, Filter filter) {
// note: security check is done in permission evaluator
Optional userOpt = userProviderService.getUserBySession();
@@ -99,7 +100,12 @@ public Page findAll(Pageable pageable) {
BaseEntityPermissionEvaluator entityPermissionEvaluator =
this.getPermissionEvaluatorForClass(entityClass.getCanonicalName());
- return entityPermissionEvaluator.findAll(userOpt.orElse(null), pageable, repository, entityClass);
+ return entityPermissionEvaluator.findAll(userOpt.orElse(null), pageable, filter, repository, entityClass);
+ }
+
+ @Transactional(readOnly = true)
+ public Page findAll(Pageable pageable) {
+ return this.findAll(pageable, null);
}
@PostFilter("hasRole('ROLE_ADMIN') or hasPermission(filterObject, 'READ')")
diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/util/JsonPathFilterUtil.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/util/JsonPathFilterUtil.java
new file mode 100644
index 000000000..26701ee24
--- /dev/null
+++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/util/JsonPathFilterUtil.java
@@ -0,0 +1,77 @@
+/* SHOGun, https://terrestris.github.io/shogun/
+ *
+ * Copyright © 2023-present terrestris GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0.txt
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.terrestris.shogun.lib.util;
+
+import com.jayway.jsonpath.Filter;
+import lombok.extern.log4j.Log4j2;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Log4j2
+public class JsonPathFilterUtil {
+
+ /**
+ * Receives a {@link com.jayway.jsonpath.Filter} and returns the PostgreSQL compatible
+ * JSON path predicate.
+ *
+ * @param filter The filter to write as string.
+ * @return {@link String} The JSON path predicate.
+ */
+ public static String writeFilter(Filter filter) {
+ String placeholder = "$";
+
+ if (filter == null) {
+ return placeholder;
+ }
+
+ String filterString = filter.toString();
+
+ Pattern pathPattern = Pattern.compile("\\[\\?\\((.+)\\)\\]", Pattern.CASE_INSENSITIVE);
+ Matcher pathPatternMatcher = pathPattern.matcher(filterString);
+
+ if (!pathPatternMatcher.find()) {
+ return placeholder;
+ }
+
+ String extractedFilterString = pathPatternMatcher.group(1);
+
+ Pattern camelCasePattern = Pattern.compile("(?<=\\$\\[')(\\w+)(?='\\])", Pattern.CASE_INSENSITIVE);
+ Matcher camelCasePatternMatcher = camelCasePattern.matcher(extractedFilterString);
+
+ String replacedFilterString = camelCasePatternMatcher
+ .replaceAll(match -> match.group()
+ .replaceAll("([a-z])([A-Z]+)", "$1_$2")
+ .toLowerCase()
+ );
+
+ Pattern regeExPattern = Pattern.compile("=~ \\/(.+)\\/");
+ Matcher regeExPatternMatcher = regeExPattern.matcher(replacedFilterString);
+
+ replacedFilterString = regeExPatternMatcher
+ .replaceAll(match -> match.group()
+ .replace("/", "\"")
+ .replace("=~", "like_regex")
+ );
+
+ return replacedFilterString
+ .replace("['", ".")
+ .replace("']", "")
+ .replace("'", "\"");
+ }
+
+}
diff --git a/shogun-lib/src/test/java/de/terrestris/shogun/lib/util/JsonPathFilterUtilTest.java b/shogun-lib/src/test/java/de/terrestris/shogun/lib/util/JsonPathFilterUtilTest.java
new file mode 100644
index 000000000..943a0efa8
--- /dev/null
+++ b/shogun-lib/src/test/java/de/terrestris/shogun/lib/util/JsonPathFilterUtilTest.java
@@ -0,0 +1,86 @@
+/* SHOGun, https://terrestris.github.io/shogun/
+ *
+ * Copyright © 2023-present terrestris GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0.txt
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package de.terrestris.shogun.lib.util;
+
+import com.jayway.jsonpath.Filter;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class JsonPathFilterUtilTest {
+
+ @Test
+ public void writeFilter_returns_a_placeholder_without_any_given_filter() {
+ String filter = JsonPathFilterUtil.writeFilter(null);
+
+ assertEquals("$", filter);
+ }
+
+ @Test
+ public void writeFilter_returns_the_filter_without_surrounding_brackets() {
+ Filter testFilter = Filter.parse("[?($.name == \"Countries\")]");
+ String filter = JsonPathFilterUtil.writeFilter(testFilter);
+
+ assertEquals("$.name == \"Countries\"", filter);
+
+ testFilter = Filter.parse("[?($.min > 1909)]");
+ filter = JsonPathFilterUtil.writeFilter(testFilter);
+
+ assertEquals("$.min > 1909", filter);
+ }
+
+// // TODO Not supported right now.
+// @Test
+// public void writeFilter_returns_the_negate_filter() {
+// Filter testFilter = Filter.parse("[?(!($.min > 1909))]");
+// String filter = JsonPathFilterUtil.writeFilter(testFilter);
+//
+// assertEquals("$.min > 1909", filter);
+// }
+
+ @Test
+ public void writeFilter_returns_the_regex_filter() {
+ Filter testFilter = Filter.parse("[?($.name =~ /^ab.*c/)]");
+ String filter = JsonPathFilterUtil.writeFilter(testFilter);
+
+ assertEquals("$.name like_regex \"^ab.*c\"", filter);
+ }
+
+ @Test
+ public void writeFilter_replaces_camel_case_attributes_with_snake_case() {
+ Filter testFilter = Filter.parse("[?($.clientConfig.minResolution > 1909)]");
+ String filter = JsonPathFilterUtil.writeFilter(testFilter);
+
+ assertEquals("$.client_config.minResolution > 1909", filter);
+
+ testFilter = Filter.parse("[?($.clientConfig.minResolution > 1909 && $.sourceConfig.maxResolution < 1909)]");
+ filter = JsonPathFilterUtil.writeFilter(testFilter);
+
+ assertEquals("$.client_config.minResolution > 1909 && $.source_config.maxResolution < 1909", filter);
+
+ testFilter = Filter.parse("[?($.clientConfig.minResolution > 1909 && $.clientConfig.maxResolution < 1909)]");
+ filter = JsonPathFilterUtil.writeFilter(testFilter);
+
+ assertEquals("$.client_config.minResolution > 1909 && $.client_config.maxResolution < 1909", filter);
+
+ testFilter = Filter.parse("[?($.clientConfig.clientConfig.minResolution > 1909 && $.clientConfig.maxResolution < 1909)]");
+ filter = JsonPathFilterUtil.writeFilter(testFilter);
+
+ assertEquals("$.client_config.clientConfig.minResolution > 1909 && $.client_config.maxResolution < 1909", filter);
+ }
+
+}