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 org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Optional;
@ControllerAdvice @ControllerAdvice
public class RestResponseEntityExceptionHandler public class RestResponseEntityExceptionHandler
@ -22,16 +23,39 @@ public class RestResponseEntityExceptionHandler
protected ResponseEntity<CustomErrorResponse> handleConflict( protected ResponseEntity<CustomErrorResponse> handleConflict(
final RuntimeException exc, final WebRequest request) { final RuntimeException exc, final WebRequest request) {
return new ResponseEntity<>( final var message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage());
new CustomErrorResponse(request.getContextPath(), exc, HttpStatus.CONFLICT), HttpStatus.CONFLICT); return errorResponse(request, HttpStatus.CONFLICT, message);
} }
@ExceptionHandler(JpaSystemException.class) @ExceptionHandler(JpaSystemException.class)
protected ResponseEntity<CustomErrorResponse> handleJpaExceptions( protected ResponseEntity<CustomErrorResponse> handleJpaExceptions(
final RuntimeException exc, final WebRequest request) { 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<>( 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; 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.timestamp = LocalDateTime.now();
this.path = path; this.path = path;
this.status = status.value(); this.status = status.value();
this.error = status.getReasonPhrase(); this.error = status.getReasonPhrase();
this.message = firstLine(NestedExceptionUtils.getMostSpecificCause(exc).getMessage()); this.message = message;
}
private String firstLine(final String message) {
return message.split("\\r|\\n|\\r\\n", 0)[0];
} }
} }

View File

@ -1,11 +1,16 @@
package net.hostsharing.hsadminng.rbac.rbacuser; 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 net.hostsharing.hsadminng.context.Context;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import java.util.ArrayList; import java.util.List;
@RestController @RestController
public class RbacUserController { public class RbacUserController {
@ -16,9 +21,18 @@ public class RbacUserController {
@Autowired @Autowired
private RbacUserRepository rbacUserRepository; 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 @Transactional
public Iterable<RbacUserEntity> listUsers( public List<RbacUserEntity> listUsers(
@RequestHeader(name = "current-user") String currentUserName, @RequestHeader(name = "current-user") String currentUserName,
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles, @RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
@RequestParam(name="name", required = false) String userName @RequestParam(name="name", required = false) String userName
@ -31,8 +45,15 @@ public class RbacUserController {
} }
@GetMapping(value = "/api/rbacuser/{userName}/permissions") @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 @Transactional
public Iterable<RbacUserPermission> listUserPermissions( public List<RbacUserPermission> listUserPermissions(
@RequestHeader(name = "current-user") String currentUserName, @RequestHeader(name = "current-user") String currentUserName,
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles, @RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
@PathVariable(name= "userName") String userName @PathVariable(name= "userName") String userName

View File

@ -14,30 +14,6 @@ import java.util.UUID;
@Immutable @Immutable
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @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 { public class RbacUserEntity {
@Id @Id

View File

@ -13,5 +13,5 @@ public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> {
List<RbacUserEntity> findByOptionalNameLike(final String userName); List<RbacUserEntity> findByOptionalNameLike(final String userName);
@Query(value = "SELECT * FROM grantedPermissions(:userName)", nativeQuery = true) @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[]) create or replace procedure grantPermissionsToRole(roleUuid uuid, permissionIds uuid[])
language plpgsql as $$ language plpgsql as $$
begin begin
@ -417,7 +430,7 @@ begin
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole'); perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
if (isGranted(subRoleId, superRoleId)) then 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; end if;
insert insert
@ -487,7 +500,7 @@ begin
foundRows = lastRowCount(); foundRows = lastRowCount();
if foundRows > maxObjects then 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 using
errcode = 'P0003', errcode = 'P0003',
hint = 'Please assume a sub-role and try again.'; hint = 'Please assume a sub-role and try again.';

View File

@ -21,7 +21,7 @@ begin
currentUser := null; currentUser := null;
end; end;
if (currentUser is null or currentUser = '') then 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; end if;
return currentUser; return currentUser;
end; $$; end; $$;
@ -37,7 +37,7 @@ begin
currentUser := currentUser(); currentUser := currentUser();
currentUserId = (select uuid from RbacUser where name = currentUser); currentUserId = (select uuid from RbacUser where name = currentUser);
if currentUserId is null then 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; end if;
return currentUserId; return currentUserId;
end; $$; end; $$;
@ -150,7 +150,7 @@ declare
begin begin
currentUserId := currentUserId(); currentUserId := currentUserId();
if currentUserId is null then if currentUserId is null then
raise exception 'user % does not exist', currentUser(); raise exception '[401] user % does not exist', currentUser();
end if; end if;
roleNames := assumedRoles(); roleNames := assumedRoles();
@ -176,7 +176,7 @@ begin
and r.roleType = roleTypeToAssume and r.roleType = roleTypeToAssume
into roleUuidToAssume; into roleUuidToAssume;
if (not isGranted(currentUserId, roleUuidToAssume)) then 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; end if;
roleIdsToAssume := roleIdsToAssume || roleUuidToAssume; roleIdsToAssume := roleIdsToAssume || roleUuidToAssume;
end loop; end loop;

View File

@ -61,31 +61,45 @@ grant all privileges on RbacOwnGrantedPermissions_rv to restricted;
/* /*
Returns all permissions granted to the given user, Returns all permissions granted to the given user,
which are also visible to the current user or assumed roles. which are also visible to the current user or assumed roles.
*/ */
create or replace function grantedPermissions(userName varchar) create or replace function grantedPermissions(userName varchar)
returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, objectTable varchar, objectIdName varchar, objectUuid uuid) returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, objectTable varchar, objectIdName varchar, objectUuid uuid)
returns null on null input returns null on null input
language plpgsql as $$ language plpgsql as $$
declare
targetUserId uuid;
currentUserId uuid;
begin begin
-- @formatter:off -- @formatter:off
if cardinality(assumedRoles()) > 0 then 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; end if;
return query select return query select
xp.roleUuid, xp.roleUuid,
(xp.objecttable || '#' || xp.objectidname || '.' || xp.roletype) as roleName, (xp.roleObjectTable || '#' || xp.roleObjectIdName || '.' || xp.roleType) as roleName,
xp.permissionUuid, xp.op, xp.objecttable, xp.objectIdName, xp.objectuuid xp.permissionUuid, xp.op, xp.permissionObjectTable, xp.permissionObjectIdName, xp.permissionObjectUuid
from (select from (select
r.uuid as roleUuid, r.roletype, r.uuid as roleUuid, r.roletype, ro.objectTable as roleObjectTable,
p.uuid as permissionUuid, p.op, o.objecttable, findIdNameByObjectUuid(ro.objectTable, ro.uuid) as roleObjectIdName,
findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName, p.uuid as permissionUuid, p.op, po.objecttable as permissionObjectTable,
o.uuid as objectuuid findIdNameByObjectUuid(po.objectTable, po.uuid) as permissionObjectIdName,
from queryPermissionsGrantedToSubjectId( findRbacUserId(userName)) p po.uuid as permissionObjectUuid
join rbacgrants g on g.descendantuuid = p.uuid from queryPermissionsGrantedToSubjectId( targetUserId) as p
join rbacobject o on o.uuid = p.objectuuid join rbacgrants as g on g.descendantUuid = p.uuid
join rbacrole r on r.uuid = g.ascendantuuid join rbacobject as po on po.uuid = p.objectUuid
where isGranted(currentUserId(), r.uuid) 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; ) xp;
-- @formatter:on -- @formatter:on
end; $$; end; $$;

View File

@ -220,7 +220,7 @@ create or replace function addCustomerNotAllowedForCurrentSubjects()
language PLPGSQL language PLPGSQL
as $$ as $$
begin 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; $$; end; $$;
/** /**

View File

@ -16,6 +16,7 @@ create or replace procedure createPackageTestData(
pacName varchar; pacName varchar;
currentTask varchar; currentTask varchar;
custAdmin varchar; custAdmin varchar;
pac package;
begin begin
set hsadminng.currentUser to ''; set hsadminng.currentUser to '';
@ -37,7 +38,13 @@ create or replace procedure createPackageTestData(
insert insert
into package (name, customerUuid) 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;
end loop; end loop;

View File

@ -159,7 +159,7 @@ class CustomerRepositoryIntegrationTest {
// then // then
attempt.assertExceptionWithRootCauseMessage( attempt.assertExceptionWithRootCauseMessage(
JpaSystemException.class, 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 @Test

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.rbac.rbacrole; package net.hostsharing.hsadminng.rbac.rbacrole;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.test.Array;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -29,7 +30,7 @@ class RbacRoleRepositoryIntegrationTest {
@Nested @Nested
class FindAllRbacRoles { class FindAllRbacRoles {
private static final String[] ALL_TEST_DATA_ROLES = new String[] { private static final String[] ALL_TEST_DATA_ROLES = Array.of(
// @formatter:off // @formatter:off
"global#hostsharing.admin", "global#hostsharing.admin",
"customer#aaa.admin", "customer#aaa.owner", "customer#aaa.tenant", "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#aac01.admin", "package#aac01.owner", "package#aac01.tenant",
"package#aac02.admin", "package#aac02.owner", "package#aac02.tenant" "package#aac02.admin", "package#aac02.owner", "package#aac02.tenant"
// @formatter:on // @formatter:on
}; );
@Test @Test
public void hostsharingAdmin_withoutAssumedRole_canViewAllRbacRoles() { public void hostsharingAdmin_withoutAssumedRole_canViewAllRbacRoles() {
@ -116,7 +117,7 @@ class RbacRoleRepositoryIntegrationTest {
// then // then
attempt.assertExceptionWithRootCauseMessage( attempt.assertExceptionWithRootCauseMessage(
JpaSystemException.class, 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 @Test
@ -159,11 +160,10 @@ class RbacRoleRepositoryIntegrationTest {
assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";")); 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) assertThat(actualResult)
//.hasSize(rbacRoleNames.length)
.extracting(RbacRoleEntity::getRoleName) .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( public void assertExceptionWithRootCauseMessage(
final Class<? extends RuntimeException> expectedExceptionClass, final Class<? extends RuntimeException> expectedExceptionClass,
final String expectedRootCauseMessage) { final String... expectedRootCauseMessages) {
assertThat( assertThat(wasSuccessful()).isFalse();
firstRootCauseMessageLineOf(caughtException(expectedExceptionClass))) final String firstRootCauseMessageLine = firstRootCauseMessageLineOf(caughtException(expectedExceptionClass));
.matches(".*" + expectedRootCauseMessage + ".*"); for ( String expectedRootCauseMessage: expectedRootCauseMessages ) {
assertThat(firstRootCauseMessageLine).contains(expectedRootCauseMessage);
}
} }
} }