add RbacUser* tests and improved http status codes
This commit is contained in:
parent
f2bc42bd85
commit
bef358eda6
@ -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<CustomErrorResponse> 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<CustomErrorResponse> 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> 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<CustomErrorResponse> 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;
|
||||
}
|
||||
}
|
||||
|
@ -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<RbacUserEntity> listUsers(
|
||||
public List<RbacUserEntity> 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<RbacUserPermission> listUserPermissions(
|
||||
public List<RbacUserPermission> listUserPermissions(
|
||||
@RequestHeader(name = "current-user") String currentUserName,
|
||||
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
|
||||
@PathVariable(name= "userName") String userName
|
||||
|
@ -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
|
||||
|
@ -13,5 +13,5 @@ public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> {
|
||||
List<RbacUserEntity> findByOptionalNameLike(final String userName);
|
||||
|
||||
@Query(value = "SELECT * FROM grantedPermissions(:userName)", nativeQuery = true)
|
||||
Iterable<RbacUserPermission> findPermissionsOfUser(@Param("userName") String userName);
|
||||
List<RbacUserPermission> findPermissionsOfUser(@Param("userName") String userName);
|
||||
}
|
||||
|
@ -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.';
|
||||
|
@ -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;
|
||||
|
@ -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; $$;
|
||||
|
@ -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; $$;
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<RbacRoleEntity> actualResult, final String... rbacRoleNames) {
|
||||
void exactlyTheseRbacRolesAreReturned(final Iterable<RbacRoleEntity> actualResult, final String... expectedRoleNames) {
|
||||
assertThat(actualResult)
|
||||
//.hasSize(rbacRoleNames.length)
|
||||
.extracting(RbacRoleEntity::getRoleName)
|
||||
.containsExactlyInAnyOrder(rbacRoleNames);
|
||||
.containsExactlyInAnyOrder(expectedRoleNames);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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())));
|
||||
}
|
||||
}
|
@ -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<RbacUserEntity> actualResult, final String... expectedUserNames) {
|
||||
assertThat(actualResult)
|
||||
.extracting(RbacUserEntity::getName)
|
||||
.containsExactlyInAnyOrder(expectedUserNames);
|
||||
}
|
||||
|
||||
void exactlyTheseRbacPermissionsAreReturned(
|
||||
final List<RbacUserPermission> actualResult,
|
||||
final String... expectedRoleNames) {
|
||||
assertThat(actualResult)
|
||||
.extracting(p -> p.getRoleName() + " -> " + p.getObjectTable() + "#" + p.getObjectIdName() + ": " + p.getOp())
|
||||
.containsExactlyInAnyOrder(expectedRoleNames);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
13
src/test/java/net/hostsharing/test/Array.java
Normal file
13
src/test/java/net/hostsharing/test/Array.java
Normal file
@ -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> E[] of(E... elements) {
|
||||
return elements;
|
||||
}
|
||||
}
|
@ -71,9 +71,11 @@ public class JpaAttempt<T> {
|
||||
|
||||
public void assertExceptionWithRootCauseMessage(
|
||||
final Class<? extends RuntimeException> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user