add RbacUser* tests and improved http status codes

This commit is contained in:
Michael Hoennig 2022-08-05 14:31:54 +02:00
parent f2bc42bd85
commit bef358eda6
16 changed files with 540 additions and 73 deletions

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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);
}

View File

@ -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.';

View File

@ -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;

View File

@ -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; $$;

View File

@ -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; $$;
/**

View File

@ -16,6 +16,7 @@ create or replace procedure createPackageTestData(
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;

View File

@ -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

View File

@ -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);
}
}

View File

@ -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())));
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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;
}
}

View File

@ -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);
}
}
}