From bef358eda65bda4b38c416a216a9d2f1e6033eef Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 5 Aug 2022 14:31:54 +0200 Subject: [PATCH] add RbacUser* tests and improved http status codes --- .../RestResponseEntityExceptionHandler.java | 38 ++- .../rbac/rbacuser/RbacUserController.java | 29 +- .../rbac/rbacuser/RbacUserEntity.java | 24 -- .../rbac/rbacuser/RbacUserRepository.java | 2 +- .../db/changelog/2022-07-28-005-rbac-base.sql | 17 +- .../changelog/2022-07-28-006-rbac-current.sql | 8 +- .../changelog/2022-07-28-007-rbac-views.sql | 38 ++- .../2022-07-29-061-hs-customer-rbac.sql | 2 +- .../2022-07-29-070-hs-package-test-data.sql | 17 +- .../CustomerRepositoryIntegrationTest.java | 2 +- .../RbacRoleRepositoryIntegrationTest.java | 12 +- .../rbacuser/RbacUserControllerRestTest.java | 72 ++++ .../RbacUserRepositoryIntegrationTest.java | 315 ++++++++++++++++++ .../hsadminng/rbac/rbacuser/TestRbacUser.java | 14 + src/test/java/net/hostsharing/test/Array.java | 13 + .../java/net/hostsharing/test/JpaAttempt.java | 10 +- 16 files changed, 540 insertions(+), 73 deletions(-) create mode 100644 src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerRestTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/TestRbacUser.java create mode 100644 src/test/java/net/hostsharing/test/Array.java diff --git a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java index f5e38ba7..78bd83ea 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/RestResponseEntityExceptionHandler.java @@ -13,6 +13,7 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import java.time.LocalDateTime; +import java.util.Optional; @ControllerAdvice public class RestResponseEntityExceptionHandler @@ -22,16 +23,39 @@ public class RestResponseEntityExceptionHandler protected ResponseEntity handleConflict( final RuntimeException exc, final WebRequest request) { - return new ResponseEntity<>( - new CustomErrorResponse(request.getContextPath(), exc, HttpStatus.CONFLICT), HttpStatus.CONFLICT); + final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage()); + return errorResponse(request, HttpStatus.CONFLICT, message); } @ExceptionHandler(JpaSystemException.class) protected ResponseEntity handleJpaExceptions( final RuntimeException exc, final WebRequest request) { + final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage()); + return errorResponse(request, httpStatus(message).orElse(HttpStatus.FORBIDDEN), message); + } + private Optional httpStatus(final String message) { + if (message.startsWith("ERROR: [")) { + for (HttpStatus status : HttpStatus.values()) { + if (message.startsWith("ERROR: [" + status.value() + "]")) { + return Optional.of(status); + } + } + return Optional.of(HttpStatus.INTERNAL_SERVER_ERROR); + } + return Optional.empty(); + } + + private static ResponseEntity errorResponse( + final WebRequest request, + final HttpStatus conflict, + final String message) { return new ResponseEntity<>( - new CustomErrorResponse(request.getContextPath(), exc, HttpStatus.FORBIDDEN), HttpStatus.FORBIDDEN); + new CustomErrorResponse(request.getContextPath(), conflict, message), conflict); + } + + private String firstLine(final String message) { + return message.split("\\r|\\n|\\r\\n", 0)[0]; } } @@ -49,15 +73,11 @@ class CustomErrorResponse { private final String message; - public CustomErrorResponse(final String path, final RuntimeException exc, final HttpStatus status) { + public CustomErrorResponse(final String path, final HttpStatus status, final String message) { this.timestamp = LocalDateTime.now(); this.path = path; this.status = status.value(); this.error = status.getReasonPhrase(); - this.message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage()); - } - - private String firstLine(final String message) { - return message.split("\\r|\\n|\\r\\n", 0)[0]; + this.message = message; } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java index b2ca0ea3..f938905e 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java @@ -1,11 +1,16 @@ package net.hostsharing.hsadminng.rbac.rbacuser; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import net.hostsharing.hsadminng.context.Context; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.transaction.Transactional; -import java.util.ArrayList; +import java.util.List; @RestController public class RbacUserController { @@ -16,9 +21,18 @@ public class RbacUserController { @Autowired private RbacUserRepository rbacUserRepository; - @GetMapping(value = "/api/rbacuser") + @GetMapping(value = "/api/rbacusers") + @Operation(description = "List accessible RBAC users with optional filter by name.", + responses = { + @ApiResponse(responseCode = "200", + content = @Content(array = @ArraySchema( + schema = @Schema(implementation = RbacUserEntity.class)))), + @ApiResponse(responseCode = "401", + description = "if the 'current-user' cannot be identified"), + @ApiResponse(responseCode = "403", + description = "if the 'current-user' is not allowed to assume any of the roles from 'assumed-roles'") }) @Transactional - public Iterable listUsers( + public List listUsers( @RequestHeader(name = "current-user") String currentUserName, @RequestHeader(name = "assumed-roles", required = false) String assumedRoles, @RequestParam(name="name", required = false) String userName @@ -31,8 +45,15 @@ public class RbacUserController { } @GetMapping(value = "/api/rbacuser/{userName}/permissions") + @Operation(description = "List all visible permissions granted to the given user; reduced ", responses = { + @ApiResponse(responseCode = "200", + content = @Content(array = @ArraySchema( schema = @Schema(implementation = RbacUserPermission.class)))), + @ApiResponse(responseCode = "401", + description = "if the 'current-user' cannot be identified"), + @ApiResponse(responseCode = "403", + description = "if the 'current-user' is not allowed to view permissions of the given user") }) @Transactional - public Iterable listUserPermissions( + public List listUserPermissions( @RequestHeader(name = "current-user") String currentUserName, @RequestHeader(name = "assumed-roles", required = false) String assumedRoles, @PathVariable(name= "userName") String userName diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserEntity.java index 4b930e3d..77899361 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserEntity.java @@ -14,30 +14,6 @@ import java.util.UUID; @Immutable @NoArgsConstructor @AllArgsConstructor -//@SqlResultSetMapping( -// name = "rbacUserPermissionMapping", -// classes = { -// @ConstructorResult( -// targetClass = RbacUserPermission.class, -// columns = { -// @ColumnResult(name = "roleUuid", type = UUID.class), -// @ColumnResult(name = "oleName", type = String.class), -// @ColumnResult(name = "permissionUuid", type = UUID.class), -// @ColumnResult(name = "op", type=String.class), -// @ColumnResult(name = "objectTable", type=String.class), -// @ColumnResult(name = "objectIdName", type =String.class), -// @ColumnResult(name = "objectUuid", type = UUID.class), -// @ColumnResult(name = "campId", type = Integer.class), -// @ColumnResult(name = "userCount", type = Byte.class) -// } -// ) -// } -//) -//@NamedNativeQuery( -// name = "grantedPermissions", -// query = "SELECT * FROM grantedPermissions(:userName)", -// resultSetMapping = "rbacUserPermissionMapping" -//) public class RbacUserEntity { @Id diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java index 502e0fb8..8e6f0342 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java @@ -13,5 +13,5 @@ public interface RbacUserRepository extends Repository { List findByOptionalNameLike(final String userName); @Query(value = "SELECT * FROM grantedPermissions(:userName)", nativeQuery = true) - Iterable findPermissionsOfUser(@Param("userName") String userName); + List findPermissionsOfUser(@Param("userName") String userName); } diff --git a/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql b/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql index c21c8aa9..18a68730 100644 --- a/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql +++ b/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql @@ -391,6 +391,19 @@ select exists( ); $$; +create or replace function hasGlobalRoleGranted(userUuid uuid) + returns bool + stable leakproof + language sql as $$ +select exists( + select r.uuid + from RbacGrants as g + join RbacRole as r on r.uuid = g.descendantuuid + join RbacObject as o on o.uuid = r.objectuuid + where g.ascendantuuid = userUuid and o.objecttable = 'global' + ); +$$; + create or replace procedure grantPermissionsToRole(roleUuid uuid, permissionIds uuid[]) language plpgsql as $$ begin @@ -417,7 +430,7 @@ begin perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole'); if (isGranted(subRoleId, superRoleId)) then - raise exception 'Cyclic role grant detected between % and %', subRoleId, superRoleId; + raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId; end if; insert @@ -487,7 +500,7 @@ begin foundRows = lastRowCount(); if foundRows > maxObjects then - raise exception 'Too many accessible objects, limit is %, found %.', maxObjects, foundRows + raise exception '[400] Too many accessible objects, limit is %, found %.', maxObjects, foundRows using errcode = 'P0003', hint = 'Please assume a sub-role and try again.'; diff --git a/src/main/resources/db/changelog/2022-07-28-006-rbac-current.sql b/src/main/resources/db/changelog/2022-07-28-006-rbac-current.sql index 6ef9a61c..e2f24423 100644 --- a/src/main/resources/db/changelog/2022-07-28-006-rbac-current.sql +++ b/src/main/resources/db/changelog/2022-07-28-006-rbac-current.sql @@ -21,7 +21,7 @@ begin currentUser := null; end; if (currentUser is null or currentUser = '') then - raise exception 'hsadminng.currentUser must be defined, please use "SET LOCAL ...;"'; + raise exception '[401] hsadminng.currentUser must be defined, please use "SET LOCAL ...;"'; end if; return currentUser; end; $$; @@ -37,7 +37,7 @@ begin currentUser := currentUser(); currentUserId = (select uuid from RbacUser where name = currentUser); if currentUserId is null then - raise exception 'hsadminng.currentUser defined as %, but does not exists', currentUser; + raise exception '[401] hsadminng.currentUser defined as %, but does not exists', currentUser; end if; return currentUserId; end; $$; @@ -150,7 +150,7 @@ declare begin currentUserId := currentUserId(); if currentUserId is null then - raise exception 'user % does not exist', currentUser(); + raise exception '[401] user % does not exist', currentUser(); end if; roleNames := assumedRoles(); @@ -176,7 +176,7 @@ begin and r.roleType = roleTypeToAssume into roleUuidToAssume; if (not isGranted(currentUserId, roleUuidToAssume)) then - raise exception 'user % (%) has no permission to assume role % (%)', currentUser(), currentUserId, roleName, roleUuidToAssume; + raise exception '[403] user % (%) has no permission to assume role % (%)', currentUser(), currentUserId, roleName, roleUuidToAssume; end if; roleIdsToAssume := roleIdsToAssume || roleUuidToAssume; end loop; diff --git a/src/main/resources/db/changelog/2022-07-28-007-rbac-views.sql b/src/main/resources/db/changelog/2022-07-28-007-rbac-views.sql index f54b1bed..241f1b09 100644 --- a/src/main/resources/db/changelog/2022-07-28-007-rbac-views.sql +++ b/src/main/resources/db/changelog/2022-07-28-007-rbac-views.sql @@ -61,31 +61,45 @@ grant all privileges on RbacOwnGrantedPermissions_rv to restricted; /* Returns all permissions granted to the given user, which are also visible to the current user or assumed roles. + + */ create or replace function grantedPermissions(userName varchar) returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, objectTable varchar, objectIdName varchar, objectUuid uuid) returns null on null input language plpgsql as $$ +declare + targetUserId uuid; + currentUserId uuid; begin -- @formatter:off if cardinality(assumedRoles()) > 0 then - raise exception 'grantedPermissions(...) does not support assumed roles'; + raise exception '[400] grantedPermissions(...) does not support assumed roles'; + end if; + + targetUserId := findRbacUserId(userName); + currentUserId := currentUserId(); + + if hasGlobalRoleGranted(targetUserId) and not hasGlobalRoleGranted(currentUserId) then + raise exception '[403] permissions of user "%" are not accessible to user "%"', userName, currentUser(); end if; return query select xp.roleUuid, - (xp.objecttable || '#' || xp.objectidname || '.' || xp.roletype) as roleName, - xp.permissionUuid, xp.op, xp.objecttable, xp.objectIdName, xp.objectuuid + (xp.roleObjectTable || '#' || xp.roleObjectIdName || '.' || xp.roleType) as roleName, + xp.permissionUuid, xp.op, xp.permissionObjectTable, xp.permissionObjectIdName, xp.permissionObjectUuid from (select - r.uuid as roleUuid, r.roletype, - p.uuid as permissionUuid, p.op, o.objecttable, - findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName, - o.uuid as objectuuid - from queryPermissionsGrantedToSubjectId( findRbacUserId(userName)) p - join rbacgrants g on g.descendantuuid = p.uuid - join rbacobject o on o.uuid = p.objectuuid - join rbacrole r on r.uuid = g.ascendantuuid - where isGranted(currentUserId(), r.uuid) + r.uuid as roleUuid, r.roletype, ro.objectTable as roleObjectTable, + findIdNameByObjectUuid(ro.objectTable, ro.uuid) as roleObjectIdName, + p.uuid as permissionUuid, p.op, po.objecttable as permissionObjectTable, + findIdNameByObjectUuid(po.objectTable, po.uuid) as permissionObjectIdName, + po.uuid as permissionObjectUuid + from queryPermissionsGrantedToSubjectId( targetUserId) as p + join rbacgrants as g on g.descendantUuid = p.uuid + join rbacobject as po on po.uuid = p.objectUuid + join rbacrole_rv as r on r.uuid = g.ascendantUuid + join rbacobject as ro on ro.uuid = r.objectUuid + where isGranted(targetUserId, r.uuid) ) xp; -- @formatter:on end; $$; diff --git a/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql b/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql index 5afa8dfb..18a4b659 100644 --- a/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql +++ b/src/main/resources/db/changelog/2022-07-29-061-hs-customer-rbac.sql @@ -220,7 +220,7 @@ create or replace function addCustomerNotAllowedForCurrentSubjects() language PLPGSQL as $$ begin - raise exception 'add-customer not permitted for %', array_to_string(currentSubjects(), ';', 'null'); + raise exception '[403] add-customer not permitted for %', array_to_string(currentSubjects(), ';', 'null'); end; $$; /** diff --git a/src/main/resources/db/changelog/2022-07-29-070-hs-package-test-data.sql b/src/main/resources/db/changelog/2022-07-29-070-hs-package-test-data.sql index 78274594..36877bd9 100644 --- a/src/main/resources/db/changelog/2022-07-29-070-hs-package-test-data.sql +++ b/src/main/resources/db/changelog/2022-07-29-070-hs-package-test-data.sql @@ -12,10 +12,11 @@ create or replace procedure createPackageTestData( ) language plpgsql as $$ declare - cust customer; - pacName varchar; - currentTask varchar; - custAdmin varchar; + cust customer; + pacName varchar; + currentTask varchar; + custAdmin varchar; + pac package; begin set hsadminng.currentUser to ''; @@ -37,7 +38,13 @@ create or replace procedure createPackageTestData( insert into package (name, customerUuid) - values (pacName, cust.uuid); + values (pacName, cust.uuid) + returning * into pac; + + call grantRoleToUser( + findRoleId(packageAdmin(pac)), + createRbacUser(pacName || '@' || cust.prefix || '.example.com')); + end loop; end loop; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerRepositoryIntegrationTest.java index 75691819..e5742b17 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerRepositoryIntegrationTest.java @@ -159,7 +159,7 @@ class CustomerRepositoryIntegrationTest { // then attempt.assertExceptionWithRootCauseMessage( JpaSystemException.class, - "user admin@aaa.example.com .* has no permission to assume role package#aab00#admin"); + "[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java index 5542bb9f..ccb5a43e 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.rbac.rbacrole; import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.test.Array; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -29,7 +30,7 @@ class RbacRoleRepositoryIntegrationTest { @Nested class FindAllRbacRoles { - private static final String[] ALL_TEST_DATA_ROLES = new String[] { + private static final String[] ALL_TEST_DATA_ROLES = Array.of( // @formatter:off "global#hostsharing.admin", "customer#aaa.admin", "customer#aaa.owner", "customer#aaa.tenant", @@ -45,7 +46,7 @@ class RbacRoleRepositoryIntegrationTest { "package#aac01.admin", "package#aac01.owner", "package#aac01.tenant", "package#aac02.admin", "package#aac02.owner", "package#aac02.tenant" // @formatter:on - }; + ); @Test public void hostsharingAdmin_withoutAssumedRole_canViewAllRbacRoles() { @@ -116,7 +117,7 @@ class RbacRoleRepositoryIntegrationTest { // then attempt.assertExceptionWithRootCauseMessage( JpaSystemException.class, - "user admin@aaa.example.com .* has no permission to assume role package#aab00#admin"); + "[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin"); } @Test @@ -159,11 +160,10 @@ class RbacRoleRepositoryIntegrationTest { assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";")); } - void exactlyTheseRbacRolesAreReturned(final Iterable actualResult, final String... rbacRoleNames) { + void exactlyTheseRbacRolesAreReturned(final Iterable actualResult, final String... expectedRoleNames) { assertThat(actualResult) - //.hasSize(rbacRoleNames.length) .extracting(RbacRoleEntity::getRoleName) - .containsExactlyInAnyOrder(rbacRoleNames); + .containsExactlyInAnyOrder(expectedRoleNames); } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerRestTest.java new file mode 100644 index 00000000..1d8aa0d5 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerRestTest.java @@ -0,0 +1,72 @@ +package net.hostsharing.hsadminng.rbac.rbacuser; + +import net.hostsharing.hsadminng.context.Context; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import java.util.List; + +import static net.hostsharing.hsadminng.rbac.rbacrole.TestRbacRole.*; +import static net.hostsharing.hsadminng.rbac.rbacuser.TestRbacUser.userAaa; +import static net.hostsharing.hsadminng.rbac.rbacuser.TestRbacUser.userBbb; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(RbacUserController.class) +class RbacUserControllerRestTest { + + @Autowired + MockMvc mockMvc; + @MockBean + Context contextMock; + @MockBean + RbacUserRepository rbacUserRepository; + + @Test + void willListAllUsers() throws Exception { + + // given + when(rbacUserRepository.findByOptionalNameLike(null)).thenReturn( + List.of(userAaa, userBbb)); + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/rbacusers") + .header("current-user", "mike@hostsharing.net") + .accept(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].name", is(userAaa.getName()))) + .andExpect(jsonPath("$[1].name", is(userBbb.getName()))); + } + + @Test + void willListUsersByName() throws Exception { + + // given + when(rbacUserRepository.findByOptionalNameLike("admin@aaa")).thenReturn( + List.of(userAaa)); + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/rbacusers") + .param("name", "admin@aaa") + .header("current-user", "mike@hostsharing.net") + .accept(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].name", is(userAaa.getName()))); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java new file mode 100644 index 00000000..6198585b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java @@ -0,0 +1,315 @@ +package net.hostsharing.hsadminng.rbac.rbacuser; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.test.Array; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.orm.jpa.JpaSystemException; + +import javax.persistence.EntityManager; +import java.util.List; + +import static net.hostsharing.test.JpaAttempt.attempt; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ComponentScan(basePackageClasses = { Context.class, RbacUserRepository.class }) +class RbacUserRepositoryIntegrationTest { + + @Autowired + Context context; + + @Autowired + RbacUserRepository rbacUserRepository; + + @Autowired EntityManager em; + + @Nested + class FindByOptionalNameLike { + + private static final String[] ALL_TEST_DATA_USERS = Array.of( + // @formatter:off + "mike@hostsharing.net", "sven@hostsharing.net", + "admin@aaa.example.com", + "aaa00@aaa.example.com", "aaa01@aaa.example.com", "aaa02@aaa.example.com", + "admin@aab.example.com", + "aab00@aab.example.com", "aab01@aab.example.com", "aab02@aab.example.com", + "admin@aac.example.com", + "aac00@aac.example.com", "aac01@aac.example.com", "aac02@aac.example.com" + // @formatter:on + ); + + @Test + public void hostsharingAdmin_withoutAssumedRole_canViewAllRbacUsers() { + // given + currentUser("mike@hostsharing.net"); + + // when + final var result = rbacUserRepository.findByOptionalNameLike(null); + + // then + exactlyTheseRbacUsersAreReturned(result, ALL_TEST_DATA_USERS); + } + + @Test + public void hostsharingAdmin_withAssumedHostsharingAdminRole_canViewAllRbacUsers() { + given: + currentUser("mike@hostsharing.net"); + assumedRoles("global#hostsharing.admin"); + + // when + final var result = rbacUserRepository.findByOptionalNameLike(null); + + then: + exactlyTheseRbacUsersAreReturned(result, ALL_TEST_DATA_USERS); + } + + @Test + public void hostsharingAdmin_withAssumedCustomerAdminRole_canViewOnlyUsersHavingRolesInThatCustomersRealm() { + given: + currentUser("mike@hostsharing.net"); + assumedRoles("customer#aaa.admin"); + + // when + final var result = rbacUserRepository.findByOptionalNameLike(null); + + then: + exactlyTheseRbacUsersAreReturned( + result, + "admin@aaa.example.com", + "aaa00@aaa.example.com", "aaa01@aaa.example.com", "aaa02@aaa.example.com" + ); + } + + @Test + public void customerAdmin_withoutAssumedRole_canViewOnlyUsersHavingRolesInThatCustomersRealm() { + // given: + currentUser("admin@aaa.example.com"); + + // when: + final var result = rbacUserRepository.findByOptionalNameLike(null); + + // then: + exactlyTheseRbacUsersAreReturned( + result, + "admin@aaa.example.com", + "aaa00@aaa.example.com", "aaa01@aaa.example.com", "aaa02@aaa.example.com" + ); + } + + @Test + public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyUsersHavingRolesInThatPackage() { + currentUser("admin@aaa.example.com"); + assumedRoles("package#aaa00.admin"); + + final var result = rbacUserRepository.findByOptionalNameLike(null); + + exactlyTheseRbacUsersAreReturned(result, "aaa00@aaa.example.com"); + } + + @Test + public void packageAdmin_withoutAssumedRole_canViewOnlyUsersHavingRolesInThatPackage() { + currentUser("aaa00@aaa.example.com"); + + final var result = rbacUserRepository.findByOptionalNameLike(null); + + exactlyTheseRbacUsersAreReturned(result, "aaa00@aaa.example.com"); + } + + } + + @Nested + class ListUserPermissions { + + private static final String[] ALL_USER_PERMISSIONS = Array.of( + // @formatter:off + "global#hostsharing.admin -> global#hostsharing: add-customer", + + "customer#aaa.admin -> customer#aaa: add-package", + "customer#aaa.admin -> customer#aaa: view", + "customer#aaa.owner -> customer#aaa: *", + "customer#aaa.tenant -> customer#aaa: view", + "package#aaa00.admin -> package#aaa00: add-domain", + "package#aaa00.admin -> package#aaa00: add-unixuser", + "package#aaa00.tenant -> package#aaa00: view", + "package#aaa01.admin -> package#aaa01: add-domain", + "package#aaa01.admin -> package#aaa01: add-unixuser", + "package#aaa01.tenant -> package#aaa01: view", + "package#aaa02.admin -> package#aaa02: add-domain", + "package#aaa02.admin -> package#aaa02: add-unixuser", + "package#aaa02.tenant -> package#aaa02: view", + + "customer#aab.admin -> customer#aab: add-package", + "customer#aab.admin -> customer#aab: view", + "customer#aab.owner -> customer#aab: *", + "customer#aab.tenant -> customer#aab: view", + "package#aab00.admin -> package#aab00: add-domain", + "package#aab00.admin -> package#aab00: add-unixuser", + "package#aab00.tenant -> package#aab00: view", + "package#aab01.admin -> package#aab01: add-domain", + "package#aab01.admin -> package#aab01: add-unixuser", + "package#aab01.tenant -> package#aab01: view", + "package#aab02.admin -> package#aab02: add-domain", + "package#aab02.admin -> package#aab02: add-unixuser", + "package#aab02.tenant -> package#aab02: view", + + "customer#aac.admin -> customer#aac: add-package", + "customer#aac.admin -> customer#aac: view", + "customer#aac.owner -> customer#aac: *", + "customer#aac.tenant -> customer#aac: view", + "package#aac00.admin -> package#aac00: add-domain", + "package#aac00.admin -> package#aac00: add-unixuser", + "package#aac00.tenant -> package#aac00: view", + "package#aac01.admin -> package#aac01: add-domain", + "package#aac01.admin -> package#aac01: add-unixuser", + "package#aac01.tenant -> package#aac01: view", + "package#aac02.admin -> package#aac02: add-domain", + "package#aac02.admin -> package#aac02: add-unixuser", + "package#aac02.tenant -> package#aac02: view" + // @formatter:on + ); + + @Test + public void hostsharingAdmin_withoutAssumedRole_canViewTheirOwnPermissions() { + // given + currentUser("mike@hostsharing.net"); + + // when + final var result = rbacUserRepository.findPermissionsOfUser("mike@hostsharing.net"); + + // then + exactlyTheseRbacPermissionsAreReturned(result, ALL_USER_PERMISSIONS); + } + + @Test + public void hostsharingAdmin_withAssumedHostmastersRole_willThrowException() { + // given + currentUser("mike@hostsharing.net"); + assumedRoles("global#hostsharing.admin"); + + // when + final var result = attempt(em, () -> + rbacUserRepository.findPermissionsOfUser("mike@hostsharing.net") + ); + + // then + result.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "[400] grantedPermissions(...) does not support assumed roles"); + } + + @Test + public void customerAdmin_withoutAssumedRole_canViewTheirOwnPermissions() { + // given + currentUser("admin@aaa.example.com"); + + // when + final var result = rbacUserRepository.findPermissionsOfUser("admin@aaa.example.com"); + + // then + exactlyTheseRbacPermissionsAreReturned(result, + // @formatter:off + "customer#aaa.admin -> customer#aaa: add-package", + "customer#aaa.admin -> customer#aaa: view", + "customer#aaa.tenant -> customer#aaa: view", + + "package#aaa00.admin -> package#aaa00: add-domain", + "package#aaa00.admin -> package#aaa00: add-unixuser", + "package#aaa00.tenant -> package#aaa00: view", + + "package#aaa01.admin -> package#aaa01: add-domain", + "package#aaa01.admin -> package#aaa01: add-unixuser", + "package#aaa01.tenant -> package#aaa01: view", + + "package#aaa02.admin -> package#aaa02: add-domain", + "package#aaa02.admin -> package#aaa02: add-unixuser", + "package#aaa02.tenant -> package#aaa02: view" + // @formatter:on + ); + } + + @Test + public void customerAdmin_withoutAssumedRole_isNotAllowedToViewGlobalAdminsPermissions() { + // given + currentUser("admin@aaa.example.com"); + + // when + final var result = attempt(em, () -> + rbacUserRepository.findPermissionsOfUser("mike@hostsharing.net") + ); + + // then + result.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "[403] permissions of user \"mike@hostsharing.net\" are not accessible to user \"admin@aaa.example.com\""); + } + + @Test + public void customerAdmin_withoutAssumedRole_canViewAllPermissionsWithinThePacketsRealm() { + // given + currentUser("admin@aaa.example.com"); + + // when + final var result = rbacUserRepository.findPermissionsOfUser("aaa00@aaa.example.com"); + + // then + exactlyTheseRbacPermissionsAreReturned(result, + // @formatter:off + "customer#aaa.tenant -> customer#aaa: view", + // "customer#aaa.admin -> customer#aaa: view" - Not permissions through the customer admin! + "package#aaa00.admin -> package#aaa00: add-unixuser", + "package#aaa00.admin -> package#aaa00: add-domain", + "package#aaa00.tenant -> package#aaa00: view" + // @formatter:on + ); + } + + @Test + public void packetAdmin_withoutAssumedRole_canViewAllPermissionsWithinThePacketsRealm() { + // given + currentUser("aaa00@aaa.example.com"); + + // when + final var result = rbacUserRepository.findPermissionsOfUser("aaa00@aaa.example.com"); + + // then + exactlyTheseRbacPermissionsAreReturned(result, + // @formatter:off + "customer#aaa.tenant -> customer#aaa: view", + // "customer#aaa.admin -> customer#aaa: view" - Not permissions through the customer admin! + "package#aaa00.admin -> package#aaa00: add-unixuser", + "package#aaa00.admin -> package#aaa00: add-domain", + "package#aaa00.tenant -> package#aaa00: view" + // @formatter:on + ); + } + } + + void currentUser(final String currentUser) { + context.setCurrentUser(currentUser); + assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser); + } + + void assumedRoles(final String assumedRoles) { + context.assumeRoles(assumedRoles); + assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";")); + } + + void exactlyTheseRbacUsersAreReturned(final List actualResult, final String... expectedUserNames) { + assertThat(actualResult) + .extracting(RbacUserEntity::getName) + .containsExactlyInAnyOrder(expectedUserNames); + } + + void exactlyTheseRbacPermissionsAreReturned( + final List actualResult, + final String... expectedRoleNames) { + assertThat(actualResult) + .extracting(p -> p.getRoleName() + " -> " + p.getObjectTable() + "#" + p.getObjectIdName() + ": " + p.getOp()) + .containsExactlyInAnyOrder(expectedRoleNames); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/TestRbacUser.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/TestRbacUser.java new file mode 100644 index 00000000..b7cc2f26 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/TestRbacUser.java @@ -0,0 +1,14 @@ +package net.hostsharing.hsadminng.rbac.rbacuser; + + +import static java.util.UUID.randomUUID; + +public class TestRbacUser { + + static final RbacUserEntity userAaa = rbacRole("admin@aaa.example.com"); + static final RbacUserEntity userBbb = rbacRole("admin@bbb.example.com"); + + static public RbacUserEntity rbacRole(final String userName) { + return new RbacUserEntity(randomUUID(), userName); + } +} diff --git a/src/test/java/net/hostsharing/test/Array.java b/src/test/java/net/hostsharing/test/Array.java new file mode 100644 index 00000000..f5221ef9 --- /dev/null +++ b/src/test/java/net/hostsharing/test/Array.java @@ -0,0 +1,13 @@ +package net.hostsharing.test; + +/** + * Java has List.of(...), Set.of(...) and Map.of(...) all with varargs parameter, + * but no Array.of(...). Here it is. + */ +public class Array { + + @SafeVarargs + public static E[] of(E... elements) { + return elements; + } +} diff --git a/src/test/java/net/hostsharing/test/JpaAttempt.java b/src/test/java/net/hostsharing/test/JpaAttempt.java index 358b1a6e..6d77bac4 100644 --- a/src/test/java/net/hostsharing/test/JpaAttempt.java +++ b/src/test/java/net/hostsharing/test/JpaAttempt.java @@ -71,9 +71,11 @@ public class JpaAttempt { public void assertExceptionWithRootCauseMessage( final Class expectedExceptionClass, - final String expectedRootCauseMessage) { - assertThat( - firstRootCauseMessageLineOf(caughtException(expectedExceptionClass))) - .matches(".*" + expectedRootCauseMessage + ".*"); + final String... expectedRootCauseMessages) { + assertThat(wasSuccessful()).isFalse(); + final String firstRootCauseMessageLine = firstRootCauseMessageLineOf(caughtException(expectedExceptionClass)); + for ( String expectedRootCauseMessage: expectedRootCauseMessages ) { + assertThat(firstRootCauseMessageLine).contains(expectedRootCauseMessage); + } } }