improve test code coverage

This commit is contained in:
Michael Hoennig 2022-08-25 17:27:17 +02:00
parent 2531b9071f
commit 1a18ba4a3d
16 changed files with 485 additions and 107 deletions

View File

@ -156,11 +156,8 @@ jacocoTestReport {
classDirectories.setFrom(files(classDirectories.files.collect { classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: [ fileTree(dir: it, exclude: [
"net/hostsharing/hsadminng/generated/**/*.class", "net/hostsharing/hsadminng/generated/**/*.class",
"net/hostsharing/hsadminng/TestController.class",
// TODO: improve test code coverage for these classes: "net/hostsharing/hsadminng/hs/hscustomer/HsadminNgApplication.class"
"net/hostsharing/hsadminng/rbac/rbacuser/UserController.class",
"net/hostsharing/hsadminng/rbac/rbacgrant/GrantController.class",
"net/hostsharing/hsadminng/hs/hscustomer/CustomerController.class"
]) ])
})) }))
} }
@ -174,7 +171,7 @@ jacocoTestCoverageVerification {
rule { rule {
excludes = ['net.hostsharing.hsadminng.generated.**'] excludes = ['net.hostsharing.hsadminng.generated.**']
limit { limit {
minimum = 0.7 // TODO: increase to 0.9 minimum = 0.8 // TODO: increase to 0.9
} }
} }
@ -190,14 +187,13 @@ jacocoTestCoverageVerification {
'net.hostsharing.hsadminng.TestController', 'net.hostsharing.hsadminng.TestController',
// TODO: improve test code coverage: // TODO: improve test code coverage:
'net.hostsharing.hsadminng.rbac.rbacuser.UserController', 'net.hostsharing.hsadminng.Mapper',
'net.hostsharing.hsadminng.hs.hscustomer.CustomerController'
] ]
limit { limit {
counter = 'LINE' counter = 'LINE'
value = 'COVEREDRATIO' value = 'COVEREDRATIO'
minimum = 0.7 minimum = 0.95
} }
} }
rule { rule {
@ -205,13 +201,7 @@ jacocoTestCoverageVerification {
excludes = [ excludes = [
'net.hostsharing.hsadminng.generated.**', 'net.hostsharing.hsadminng.generated.**',
'net.hostsharing.hsadminng.HsadminNgApplication.*', 'net.hostsharing.hsadminng.HsadminNgApplication.*',
'net.hostsharing.hsadminng.TestController.*']
// TODO: improve test code coverage:
'net.hostsharing.hsadminng.rbac.rbacuser.RbacUserController.listUsers(*)',
'net.hostsharing.hsadminng.rbac.rbacuser.RbacUserController.listUserPermissions(*)',
'net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantController.listUserGrants(*)',
'net.hostsharing.hsadminng.hs.hscustomer.CustomerController.addCustomer(java.lang.String, java.lang.String, net.hostsharing.hsadminng.generated.api.v1.model.CustomerResource)'
]
limit { limit {
counter = 'BRANCH' counter = 'BRANCH'

View File

@ -1,3 +1,11 @@
# Spring BOM overrides # Spring BOM overrides
postgresql.version = 42.4.1 postgresql.version = 42.4.1
# TODO: can be removed if all dependencies are JDK 16 compliant
#org.gradle.jvmargs= \
# --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
# --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
# --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
# --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
# --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hscustomer;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.generated.api.v1.api.CustomersApi; import net.hostsharing.hsadminng.generated.api.v1.api.CustomersApi;
import net.hostsharing.hsadminng.generated.api.v1.model.CustomerResource; import net.hostsharing.hsadminng.generated.api.v1.model.CustomerResource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -33,7 +34,7 @@ public class CustomerController implements CustomersApi {
String prefix String prefix
) { ) {
context.setCurrentUser(userName); context.setCurrentUser(userName);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
@ -51,7 +52,7 @@ public class CustomerController implements CustomersApi {
context.setCurrentTask("create new customer: #" + customer.getReference() + " / " + customer.getPrefix()); context.setCurrentTask("create new customer: #" + customer.getReference() + " / " + customer.getPrefix());
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
if (customer.getUuid() == null) { if (customer.getUuid() == null) {
@ -62,7 +63,7 @@ public class CustomerController implements CustomersApi {
final var uri = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
.path("/api/rbac-users/{id}") .path("/api/customers/{id}")
.buildAndExpand(customer.getUuid()) .buildAndExpand(customer.getUuid())
.toUri(); .toUri();
return ResponseEntity.created(uri).body(map(saved, CustomerResource.class)); return ResponseEntity.created(uri).body(map(saved, CustomerResource.class));

View File

@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.generated.api.v1.api.PackagesApi; import net.hostsharing.hsadminng.generated.api.v1.api.PackagesApi;
import net.hostsharing.hsadminng.generated.api.v1.model.PackageResource; import net.hostsharing.hsadminng.generated.api.v1.model.PackageResource;
import net.hostsharing.hsadminng.generated.api.v1.model.PackageUpdateResource; import net.hostsharing.hsadminng.generated.api.v1.model.PackageUpdateResource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -33,7 +34,7 @@ public class PackageController implements PackagesApi {
String name String name
) { ) {
context.setCurrentUser(userName); context.setCurrentUser(userName);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
final var result = packageRepository.findAllByOptionalNameLike(name); final var result = packageRepository.findAllByOptionalNameLike(name);
@ -49,7 +50,7 @@ public class PackageController implements PackagesApi {
final PackageUpdateResource body) { final PackageUpdateResource body) {
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
final var current = packageRepository.findByUuid(packageUuid); final var current = packageRepository.findByUuid(packageUuid);

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.rbacgrant;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.generated.api.v1.api.RbacgrantsApi; import net.hostsharing.hsadminng.generated.api.v1.api.RbacgrantsApi;
import net.hostsharing.hsadminng.generated.api.v1.model.RbacGrantResource; import net.hostsharing.hsadminng.generated.api.v1.model.RbacGrantResource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -38,7 +39,7 @@ public class RbacGrantController implements RbacgrantsApi {
final UUID granteeUserUuid) { final UUID granteeUserUuid) {
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
@ -57,7 +58,7 @@ public class RbacGrantController implements RbacgrantsApi {
final String assumedRoles) { final String assumedRoles) {
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
return ResponseEntity.ok(mapList(rbacGrantRepository.findAll(), RbacGrantResource.class)); return ResponseEntity.ok(mapList(rbacGrantRepository.findAll(), RbacGrantResource.class));
@ -72,7 +73,7 @@ public class RbacGrantController implements RbacgrantsApi {
context.setCurrentTask("granting role to user"); context.setCurrentTask("granting role to user");
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
@ -98,7 +99,7 @@ public class RbacGrantController implements RbacgrantsApi {
context.setCurrentTask("revoking role from user"); context.setCurrentTask("revoking role from user");
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.rbacrole;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.generated.api.v1.api.RbacrolesApi; import net.hostsharing.hsadminng.generated.api.v1.api.RbacrolesApi;
import net.hostsharing.hsadminng.generated.api.v1.model.RbacRoleResource; import net.hostsharing.hsadminng.generated.api.v1.model.RbacRoleResource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -29,7 +30,7 @@ public class RbacRoleController implements RbacrolesApi {
final String assumedRoles) { final String assumedRoles) {
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
return ResponseEntity.ok(mapList(rbacRoleRepository.findAll(), RbacRoleResource.class)); return ResponseEntity.ok(mapList(rbacRoleRepository.findAll(), RbacRoleResource.class));

View File

@ -2,8 +2,11 @@ package net.hostsharing.hsadminng.rbac.rbacuser;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.generated.api.v1.api.RbacusersApi; import net.hostsharing.hsadminng.generated.api.v1.api.RbacusersApi;
import net.hostsharing.hsadminng.generated.api.v1.model.RbacGrantResource;
import net.hostsharing.hsadminng.generated.api.v1.model.RbacUserPermissionResource; import net.hostsharing.hsadminng.generated.api.v1.model.RbacUserPermissionResource;
import net.hostsharing.hsadminng.generated.api.v1.model.RbacUserResource; import net.hostsharing.hsadminng.generated.api.v1.model.RbacUserResource;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantId;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -28,7 +31,7 @@ public class RbacUserController implements RbacusersApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<RbacUserResource> createUser( public ResponseEntity<RbacUserResource> createUser(
@RequestBody final RbacUserResource body final RbacUserResource body
) { ) {
context.setCurrentTask("creating new user: " + body.getName()); context.setCurrentTask("creating new user: " + body.getName());
context.setCurrentUser(body.getName()); context.setCurrentUser(body.getName());
@ -47,22 +50,33 @@ public class RbacUserController implements RbacusersApi {
} }
@Override @Override
public ResponseEntity<List<RbacUserPermissionResource>> getUserById( @Transactional(readOnly = true)
public ResponseEntity<RbacUserResource> getUserById(
final String currentUser, final String currentUser,
final String assumedRoles, final String assumedRoles,
final String userName) { final UUID userUuid) {
return null; // TODO implement getUserById
context.setCurrentUser(currentUser);
if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles);
}
final var result = rbacUserRepository.findByUuid(userUuid);
if (result == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(map(result, RbacUserResource.class));
} }
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<RbacUserResource>> listUsers( public ResponseEntity<List<RbacUserResource>> listUsers(
@RequestHeader(name = "current-user") final String currentUserName, final String currentUserName,
@RequestHeader(name = "assumed-roles", required = false) final String assumedRoles, final String assumedRoles,
@RequestParam(name = "name", required = false) final String userName final String userName
) { ) {
context.setCurrentUser(currentUserName); context.setCurrentUser(currentUserName);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
return ResponseEntity.ok(mapList(rbacUserRepository.findByOptionalNameLike(userName), RbacUserResource.class)); return ResponseEntity.ok(mapList(rbacUserRepository.findByOptionalNameLike(userName), RbacUserResource.class));
@ -71,14 +85,14 @@ public class RbacUserController implements RbacusersApi {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<RbacUserPermissionResource>> listUserPermissions( public ResponseEntity<List<RbacUserPermissionResource>> listUserPermissions(
@RequestHeader(name = "current-user") final String currentUserName, final String currentUserName,
@RequestHeader(name = "assumed-roles", required = false) final String assumedRoles, final String assumedRoles,
@PathVariable(name = "userName") final String userName final UUID userUuid
) { ) {
context.setCurrentUser(currentUserName); context.setCurrentUser(currentUserName);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (!StringUtils.isBlank(assumedRoles)) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
return ResponseEntity.ok(mapList(rbacUserRepository.findPermissionsOfUser(userName), RbacUserPermissionResource.class)); return ResponseEntity.ok(mapList(rbacUserRepository.findPermissionsOfUserByUuid(userUuid), RbacUserPermissionResource.class));
} }
} }

View File

@ -22,8 +22,8 @@ public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> {
RbacUserEntity findByUuid(UUID uuid); RbacUserEntity findByUuid(UUID uuid);
@Query(value = "select * from grantedPermissions(:userName)", nativeQuery = true) @Query(value = "select * from grantedPermissions(:userUuid)", nativeQuery = true)
List<RbacUserPermission> findPermissionsOfUser(String userName); List<RbacUserPermission> findPermissionsOfUserByUuid(UUID userUuid);
/* /*
Can't use save/saveAndFlush from SpringData because the uuid is not generated on the entity level, Can't use save/saveAndFlush from SpringData because the uuid is not generated on the entity level,

View File

@ -13,12 +13,12 @@ paths:
/api/rbac-users: /api/rbac-users:
$ref: "./api-definition/rbac-users.yaml" $ref: "./api-definition/rbac-users.yaml"
/api/rbac-users/{userUuid}:
$ref: "./api-definition/rbac-users-with-id.yaml"
/api/rbac-users/{userUuid}/permissions: /api/rbac-users/{userUuid}/permissions:
$ref: "./api-definition/rbac-users-with-id-permissions.yaml" $ref: "./api-definition/rbac-users-with-id-permissions.yaml"
/api/rbac-users/{userUuid}:
$ref: "./api-definition/rbac-users-with-id.yaml"
/api/rbac-roles: /api/rbac-roles:
$ref: "./api-definition/rbac-roles.yaml" $ref: "./api-definition/rbac-roles.yaml"

View File

@ -6,11 +6,12 @@ get:
parameters: parameters:
- $ref: './api-definition/auth.yaml#/components/parameters/currentUser' - $ref: './api-definition/auth.yaml#/components/parameters/currentUser'
- $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles' - $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles'
- name: userName - name: userUuid
in: path in: path
required: true required: true
schema: schema:
type: string type: string
format: uuid
responses: responses:
"200": "200":
description: OK description: OK

View File

@ -6,20 +6,19 @@ get:
parameters: parameters:
- $ref: './api-definition/auth.yaml#/components/parameters/currentUser' - $ref: './api-definition/auth.yaml#/components/parameters/currentUser'
- $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles' - $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles'
- name: userName - name: userUuid
in: path in: path
required: true required: true
schema: schema:
type: string type: string
format: uuid
responses: responses:
"200": "200":
description: OK description: OK
content: content:
'application/json': 'application/json':
schema: schema:
type: array $ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUser'
items:
$ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUserPermission'
"401": "401":
$ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized' $ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized'

View File

@ -205,24 +205,18 @@ grant all privileges on RbacOwnGrantedPermissions_rv to restricted;
*/ */
create or replace function grantedPermissions(userName varchar) create or replace function grantedPermissions(targetUserUuid uuid)
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 declare
targetUserId uuid;
currentUserId uuid; currentUserId uuid;
begin begin
-- @formatter:off -- @formatter:off
if cardinality(assumedRoles()) > 0 then
raise exception '[400] grantedPermissions(...) does not support assumed roles';
end if;
targetUserId := findRbacUserId(userName);
currentUserId := currentUserId(); currentUserId := currentUserId();
if hasGlobalRoleGranted(targetUserId) and not hasGlobalRoleGranted(currentUserId) then if hasGlobalRoleGranted(targetUserUuid) and not hasGlobalRoleGranted(currentUserId) then
raise exception '[403] permissions of user "%" are not accessible to user "%"', userName, currentUser(); raise exception '[403] permissions of user "%" are not accessible to user "%"', targetUserUuid, currentUser();
end if; end if;
return query select return query select
@ -235,12 +229,12 @@ begin
p.uuid as permissionUuid, p.op, po.objecttable as permissionObjectTable, p.uuid as permissionUuid, p.op, po.objecttable as permissionObjectTable,
findIdNameByObjectUuid(po.objectTable, po.uuid) as permissionObjectIdName, findIdNameByObjectUuid(po.objectTable, po.uuid) as permissionObjectIdName,
po.uuid as permissionObjectUuid po.uuid as permissionObjectUuid
from queryPermissionsGrantedToSubjectId( targetUserId) as p from queryPermissionsGrantedToSubjectId( targetUserUuid) as p
join rbacgrants as g on g.descendantUuid = p.uuid join rbacgrants as g on g.descendantUuid = p.uuid
join rbacobject as po on po.uuid = p.objectUuid join rbacobject as po on po.uuid = p.objectUuid
join rbacrole_rv as r on r.uuid = g.ascendantUuid join rbacrole_rv as r on r.uuid = g.ascendantUuid
join rbacobject as ro on ro.uuid = r.objectUuid join rbacobject as ro on ro.uuid = r.objectUuid
where isGranted(targetUserId, r.uuid) where isGranted(targetUserUuid, r.uuid)
) xp; ) xp;
-- @formatter:on -- @formatter:on
end; $$; end; $$;

View File

@ -141,6 +141,36 @@ class CustomerControllerAcceptanceTest {
.hasValueSatisfying(c -> assertThat(c.getPrefix()).isEqualTo("vvv")); .hasValueSatisfying(c -> assertThat(c.getPrefix()).isEqualTo("vvv"));
} }
@Test
void hostsharingAdmin_withAssumedCustomerAdminRole_canNotCreateCustomer() throws Exception {
RestAssured // @formatter:off
.given()
.header("current-user", "mike@hostsharing.net")
.header("assumed-roles", "customer#xxx.admin")
.contentType(ContentType.JSON)
.body("""
{
"reference": 90010,
"prefix": "uuu",
"adminUserName": "customer-admin@uuu.example.com"
}
""")
.port(port)
.when()
.post("http://localhost/api/customers")
.then().assertThat()
.statusCode(403)
.contentType(ContentType.JSON)
.statusCode(403)
.body("message", containsString("add-customer not permitted for customer#xxx.admin"));
// @formatter:on
// finally, the new customer was not created
context.setCurrentUser("sven@hostsharing.net");
assertThat(customerRepository.findCustomerByOptionalPrefixLike("uuu")).hasSize(0);
}
@Test @Test
void customerAdmin_withoutAssumedRole_canNotCreateCustomer() throws Exception { void customerAdmin_withoutAssumedRole_canNotCreateCustomer() throws Exception {

View File

@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat; import static org.assertj.core.api.Assumptions.assumeThat;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
@ -55,6 +56,108 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest {
@Autowired @Autowired
JpaAttempt jpaAttempt; JpaAttempt jpaAttempt;
@Nested
class ListGrants {
@Test
@Accepts("GRT:L(List)")
void hostsharingAdmin_withoutAssumedRole_canViewAllGrants() {
RestAssured // @formatter:off
.given()
.header("current-user", "mike@hostsharing.net")
.port(port)
.when()
.get("http://localhost/api/rbac-grants")
.then().log().all().assertThat()
.statusCode(200)
.contentType("application/json")
.body("", hasItem(
allOf(
hasEntry("grantedByRoleIdName", "global#hostsharing.admin"),
hasEntry("grantedRoleIdName", "customer#xxx.admin"),
hasEntry("granteeUserName", "customer-admin@xxx.example.com")
)
))
.body("", hasItem(
allOf(
hasEntry("grantedByRoleIdName", "global#hostsharing.admin"),
hasEntry("grantedRoleIdName", "customer#yyy.admin"),
hasEntry("granteeUserName", "customer-admin@yyy.example.com")
)
))
.body("", hasItem(
allOf(
hasEntry("grantedByRoleIdName", "global#hostsharing.admin"),
hasEntry("grantedRoleIdName", "global#hostsharing.admin"),
hasEntry("granteeUserName", "sven@hostsharing.net")
)
))
.body("", hasItem(
allOf(
hasEntry("grantedByRoleIdName", "customer#xxx.admin"),
hasEntry("grantedRoleIdName", "package#xxx00.admin"),
hasEntry("granteeUserName", "pac-admin-xxx00@xxx.example.com")
)
))
.body("", hasItem(
allOf(
hasEntry("grantedByRoleIdName", "customer#zzz.admin"),
hasEntry("grantedRoleIdName", "package#zzz02.admin"),
hasEntry("granteeUserName", "pac-admin-zzz02@zzz.example.com")
)
))
.body("size()", greaterThanOrEqualTo(14));
// @formatter:on
}
@Test
@Accepts({ "GRT:L(List)", "GRT:X(Access Control)" })
void hostsharingAdmin_withAssumedPackageAdminRole_canViewPacketRelatedGrants() {
RestAssured // @formatter:off
.given()
.header("current-user", "mike@hostsharing.net")
.header("assumed-roles", "package#yyy00.admin")
.port(port)
.when()
.get("http://localhost/api/rbac-grants")
.then().log().all().assertThat()
.statusCode(200)
.contentType("application/json")
.body("", hasItem(
allOf(
hasEntry("grantedByRoleIdName", "customer#yyy.admin"),
hasEntry("grantedRoleIdName", "package#yyy00.admin"),
hasEntry("granteeUserName", "pac-admin-yyy00@yyy.example.com")
)
))
.body("size()", is(1));
// @formatter:on
}
@Test
@Accepts({ "GRT:L(List)", "GRT:X(Access Control)" })
void packageAdmin_withoutAssumedRole_canViewPacketRelatedGrants() {
RestAssured // @formatter:off
.given()
.header("current-user", "pac-admin-yyy00@yyy.example.com")
.port(port)
.when()
.get("http://localhost/api/rbac-grants")
.then().log().all().assertThat()
.statusCode(200)
.contentType("application/json")
.body("", hasItem(
allOf(
hasEntry("grantedByRoleIdName", "customer#yyy.admin"),
hasEntry("grantedRoleIdName", "package#yyy00.admin"),
hasEntry("granteeUserName", "pac-admin-yyy00@yyy.example.com")
)
))
.body("size()", is(1));
// @formatter:on
}
}
@Nested @Nested
class GetGrantById { class GetGrantById {

View File

@ -2,8 +2,10 @@ package net.hostsharing.hsadminng.rbac.rbacuser;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.Accepts;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.test.JpaAttempt;
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;
@ -19,7 +21,7 @@ import static org.hamcrest.Matchers.*;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = HsadminNgApplication.class classes = { HsadminNgApplication.class, JpaAttempt.class }
) )
@Transactional @Transactional
class RbacUserControllerAcceptanceTest { class RbacUserControllerAcceptanceTest {
@ -30,6 +32,9 @@ class RbacUserControllerAcceptanceTest {
@Autowired @Autowired
EntityManager em; EntityManager em;
@Autowired
JpaAttempt jpaAttempt;
@Autowired @Autowired
Context context; Context context;
@ -37,9 +42,125 @@ class RbacUserControllerAcceptanceTest {
RbacUserRepository rbacUserRepository; RbacUserRepository rbacUserRepository;
@Nested @Nested
class ApiRbacUsersGet { class CreateRbacUser {
@Test @Test
@Accepts({ "USR:C(Create)", "USR:X(Access Control)" })
void anybody_canCreateANewUser() {
// @formatter:off
final var location = RestAssured
.given()
.contentType(ContentType.JSON)
.body("""
{
"name": "new-user@example.com"
}
""")
.port(port)
.when()
.post("http://localhost/api/rbac-users")
.then().assertThat()
.statusCode(201)
.contentType(ContentType.JSON)
.body("name", is("new-user@example.com"))
.header("Location", startsWith("http://localhost"))
.extract().header("Location");
// @formatter:on
// finally, the user can view its own record
final var newUserUuid = UUID.fromString(
location.substring(location.lastIndexOf('/') + 1));
context.setCurrentUser("new-user@example.com");
assertThat(rbacUserRepository.findByUuid(newUserUuid))
.extracting(RbacUserEntity::getName).isEqualTo("new-user@example.com");
}
}
@Nested
class GetRbacUser {
@Test
@Accepts({ "USR:R(Read)" })
void hostsharingAdmin_withoutAssumedRole_canGetArbitraryUser() {
final var givenUser = findRbacUserByName("pac-admin-xxx00@xxx.example.com");
// @formatter:off
RestAssured
.given()
.header("current-user", "mike@hostsharing.net")
.port(port)
.when()
.get("http://localhost/api/rbac-users/" + givenUser.getUuid())
.then().log().body().assertThat()
.statusCode(200)
.contentType("application/json")
.body("name", is("pac-admin-xxx00@xxx.example.com"));
// @formatter:on
}
@Test
@Accepts({ "USR:R(Read)", "USR:X(Access Control)" })
void hostsharingAdmin_withAssumedCustomerAdminRole_canGetUserWithinInItsRealm() {
final var givenUser = findRbacUserByName("pac-admin-yyy00@yyy.example.com");
// @formatter:off
RestAssured
.given()
.header("current-user", "mike@hostsharing.net")
.header("assumed-roles", "customer#yyy.admin")
.port(port)
.when()
.get("http://localhost/api/rbac-users/" + givenUser.getUuid())
.then().log().body().assertThat()
.statusCode(200)
.contentType("application/json")
.body("name", is("pac-admin-yyy00@yyy.example.com"));
// @formatter:on
}
@Test
@Accepts({ "USR:R(Read)", "USR:X(Access Control)" })
void customerAdmin_withoutAssumedRole_canGetUserWithinInItsRealm() {
final var givenUser = findRbacUserByName("pac-admin-yyy00@yyy.example.com");
// @formatter:off
RestAssured
.given()
.header("current-user", "customer-admin@yyy.example.com")
.port(port)
.when()
.get("http://localhost/api/rbac-users/" + givenUser.getUuid())
.then().log().body().assertThat()
.statusCode(200)
.contentType("application/json")
.body("name", is("pac-admin-yyy00@yyy.example.com"));
// @formatter:on
}
@Test
@Accepts({ "USR:R(Read)", "USR:X(Access Control)" })
void customerAdmin_withoutAssumedRole_canNotGetUserOutsideOfItsRealm() {
final var givenUser = findRbacUserByName("pac-admin-yyy00@yyy.example.com");
// @formatter:off
RestAssured
.given()
.header("current-user", "customer-admin@xxx.example.com")
.port(port)
.when()
.get("http://localhost/api/rbac-users/" + givenUser.getUuid())
.then().log().body().assertThat()
.statusCode(404);
// @formatter:on
}
}
@Nested
class ListRbacUsers {
@Test
@Accepts({ "USR:L(List)" })
void hostsharingAdmin_withoutAssumedRole_canViewAllUsers() { void hostsharingAdmin_withoutAssumedRole_canViewAllUsers() {
// @formatter:off // @formatter:off
@ -65,6 +186,7 @@ class RbacUserControllerAcceptanceTest {
} }
@Test @Test
@Accepts({ "USR:F(Filter)" })
void hostsharingAdmin_withoutAssumedRole_canViewAllUsersByName() { void hostsharingAdmin_withoutAssumedRole_canViewAllUsersByName() {
// @formatter:off // @formatter:off
@ -85,6 +207,30 @@ class RbacUserControllerAcceptanceTest {
} }
@Test @Test
@Accepts({ "USR:L(List)", "USR:X(Access Control)" })
void hostsharingAdmin_withAssumedCustomerAdminRole_canViewUsersInItsRealm() {
// @formatter:off
RestAssured
.given()
.header("current-user", "mike@hostsharing.net")
.header("assumed-roles", "customer#yyy.admin")
.port(port)
.when()
.get("http://localhost/api/rbac-users")
.then().assertThat()
.statusCode(200)
.contentType("application/json")
.body("[0].name", is("customer-admin@yyy.example.com"))
.body("[1].name", is("pac-admin-yyy00@yyy.example.com"))
.body("[2].name", is("pac-admin-yyy01@yyy.example.com"))
.body("[3].name", is("pac-admin-yyy02@yyy.example.com"))
.body("size()", is(4));
// @formatter:on
}
@Test
@Accepts({ "USR:L(List)", "USR:X(Access Control)" })
void customerAdmin_withoutAssumedRole_canViewUsersInItsRealm() { void customerAdmin_withoutAssumedRole_canViewUsersInItsRealm() {
// @formatter:off // @formatter:off
@ -106,6 +252,7 @@ class RbacUserControllerAcceptanceTest {
} }
@Test @Test
@Accepts({ "USR:L(List)", "USR:X(Access Control)" })
void packetAdmin_withoutAssumedRole_canViewAllUsersOfItsPackage() { void packetAdmin_withoutAssumedRole_canViewAllUsersOfItsPackage() {
// @formatter:off // @formatter:off
@ -125,37 +272,135 @@ class RbacUserControllerAcceptanceTest {
} }
@Nested @Nested
class ApiRbacUsersPost { class ListRbacUserPermissions {
@Test @Test
void anybody_canCreateANewUser() { @Accepts({ "PRM:L(List)" })
void hostsharingAdmin_withoutAssumedRole_canViewArbitraryUsersPermissions() {
final var givenUser = findRbacUserByName("pac-admin-yyy00@yyy.example.com");
// @formatter:off // @formatter:off
final var location = RestAssured RestAssured
.given() .given()
.contentType(ContentType.JSON) .header("current-user", "mike@hostsharing.net")
.body("""
{
"name": "new-user@example.com"
}
""")
.port(port) .port(port)
.when() .when()
.post("http://localhost/api/rbac-users") .get("http://localhost/api/rbac-users/" + givenUser.getUuid() + "/permissions")
.then().assertThat() .then().log().body().assertThat()
.statusCode(201) .statusCode(200)
.contentType(ContentType.JSON) .contentType("application/json")
.body("name", is("new-user@example.com")) .body("", hasItem(
.header("Location", startsWith("http://localhost")) allOf(
.extract().header("Location"); hasEntry("roleName", "customer#yyy.tenant"),
hasEntry("op", "view"))
))
.body("", hasItem(
allOf(
hasEntry("roleName", "package#yyy00.admin"),
hasEntry("op", "add-unixuser"))
))
.body("", hasItem(
allOf(
hasEntry("roleName", "unixuser#yyy00-aaaa.owner"),
hasEntry("op", "*"))
))
.body("size()", is(8));
// @formatter:on // @formatter:on
}
// finally, the user can view its own record @Test
final var newUserUuid = UUID.fromString( @Accepts({ "PRM:L(List)" })
location.substring(location.lastIndexOf('/') + 1)); void hostsharingAdmin_withAssumedCustomerAdminRole_canViewArbitraryUsersPermissions() {
context.setCurrentUser("new-user@example.com"); final var givenUser = findRbacUserByName("pac-admin-yyy00@yyy.example.com");
assertThat(rbacUserRepository.findByUuid(newUserUuid))
.extracting(RbacUserEntity::getName).isEqualTo("new-user@example.com"); // @formatter:off
RestAssured
.given()
.header("current-user", "mike@hostsharing.net")
.header("assumed-roles", "package#yyy00.admin")
.port(port)
.when()
.get("http://localhost/api/rbac-users/" + givenUser.getUuid() + "/permissions")
.then().log().body().assertThat()
.statusCode(200)
.contentType("application/json")
.body("", hasItem(
allOf(
hasEntry("roleName", "customer#yyy.tenant"),
hasEntry("op", "view"))
))
.body("", hasItem(
allOf(
hasEntry("roleName", "package#yyy00.admin"),
hasEntry("op", "add-unixuser"))
))
.body("", hasItem(
allOf(
hasEntry("roleName", "unixuser#yyy00-aaaa.owner"),
hasEntry("op", "*"))
))
.body("size()", is(8));
// @formatter:on
}
@Test
@Accepts({ "PRM:L(List)" })
void packageAdmin_withoutAssumedRole_canViewPermissionsOfUsersInItsRealm() {
final var givenUser = findRbacUserByName("pac-admin-yyy00@yyy.example.com");
// @formatter:off
RestAssured
.given()
.header("current-user", "pac-admin-yyy00@yyy.example.com")
.port(port)
.when()
.get("http://localhost/api/rbac-users/" + givenUser.getUuid() + "/permissions")
.then().log().body().assertThat()
.statusCode(200)
.contentType("application/json")
.body("", hasItem(
allOf(
hasEntry("roleName", "customer#yyy.tenant"),
hasEntry("op", "view"))
))
.body("", hasItem(
allOf(
hasEntry("roleName", "package#yyy00.admin"),
hasEntry("op", "add-unixuser"))
))
.body("", hasItem(
allOf(
hasEntry("roleName", "unixuser#yyy00-aaaa.owner"),
hasEntry("op", "*"))
))
.body("size()", is(8));
// @formatter:on
}
@Test
@Accepts({ "PRM:L(List)" })
void packageAdmin_canViewPermissionsOfUsersOutsideOfItsRealm() {
final var givenUser = findRbacUserByName("pac-admin-xxx00@xxx.example.com");
// @formatter:off
RestAssured
.given()
.header("current-user", "pac-admin-yyy00@yyy.example.com")
.port(port)
.when()
.get("http://localhost/api/rbac-users/" + givenUser.getUuid() + "/permissions")
.then().log().body().assertThat()
.statusCode(200)
.contentType("application/json")
.body("size()", is(0));
// @formatter:on
} }
} }
RbacUserEntity findRbacUserByName(final String userName) {
return jpaAttempt.transacted(() -> {
context.setCurrentUser("mike@hostsharing.net");
return rbacUserRepository.findByName(userName);
}).returnedValue();
}
} }

View File

@ -231,35 +231,19 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest {
context("mike@hostsharing.net"); context("mike@hostsharing.net");
// when // when
final var result = rbacUserRepository.findPermissionsOfUser("mike@hostsharing.net"); final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("mike@hostsharing.net"));
// then // then
allTheseRbacPermissionsAreReturned(result, ALL_USER_PERMISSIONS); allTheseRbacPermissionsAreReturned(result, ALL_USER_PERMISSIONS);
} }
@Test
public void hostsharingAdmin_withAssumedHostmastersRole_willThrowException() {
// given
context("mike@hostsharing.net", "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 @Test
public void customerAdmin_withoutAssumedRole_canViewTheirOwnPermissions() { public void customerAdmin_withoutAssumedRole_canViewTheirOwnPermissions() {
// given // given
context("customer-admin@xxx.example.com"); context("customer-admin@xxx.example.com");
// when // when
final var result = rbacUserRepository.findPermissionsOfUser("customer-admin@xxx.example.com"); final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("customer-admin@xxx.example.com"));
// then // then
allTheseRbacPermissionsAreReturned( allTheseRbacPermissionsAreReturned(
@ -299,16 +283,18 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest {
public void customerAdmin_withoutAssumedRole_isNotAllowedToViewGlobalAdminsPermissions() { public void customerAdmin_withoutAssumedRole_isNotAllowedToViewGlobalAdminsPermissions() {
// given // given
context("customer-admin@xxx.example.com"); context("customer-admin@xxx.example.com");
final UUID userUuid = userUUID("mike@hostsharing.net");
// when // when
final var result = attempt(em, () -> final var result = attempt(em, () ->
rbacUserRepository.findPermissionsOfUser("mike@hostsharing.net") rbacUserRepository.findPermissionsOfUserByUuid(userUuid)
); );
// then // then
result.assertExceptionWithRootCauseMessage( result.assertExceptionWithRootCauseMessage(
JpaSystemException.class, JpaSystemException.class,
"[403] permissions of user \"mike@hostsharing.net\" are not accessible to user \"customer-admin@xxx.example.com\""); "[403] permissions of user \"" + userUuid
+ "\" are not accessible to user \"customer-admin@xxx.example.com\"");
} }
@Test @Test
@ -317,7 +303,7 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest {
context("customer-admin@xxx.example.com"); context("customer-admin@xxx.example.com");
// when // when
final var result = rbacUserRepository.findPermissionsOfUser("pac-admin-xxx00@xxx.example.com"); final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("pac-admin-xxx00@xxx.example.com"));
// then // then
allTheseRbacPermissionsAreReturned( allTheseRbacPermissionsAreReturned(
@ -353,7 +339,7 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest {
context("customer-admin@xxx.example.com"); context("customer-admin@xxx.example.com");
// when // when
final var result = rbacUserRepository.findPermissionsOfUser("pac-admin-yyy00@yyy.example.com"); final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("pac-admin-yyy00@yyy.example.com"));
// then // then
noRbacPermissionsAreReturned(result); noRbacPermissionsAreReturned(result);
@ -365,7 +351,7 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest {
context("pac-admin-xxx00@xxx.example.com"); context("pac-admin-xxx00@xxx.example.com");
// when // when
final var result = rbacUserRepository.findPermissionsOfUser("pac-admin-xxx00@xxx.example.com"); final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("pac-admin-xxx00@xxx.example.com"));
// then // then
allTheseRbacPermissionsAreReturned( allTheseRbacPermissionsAreReturned(
@ -397,6 +383,10 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest {
} }
} }
UUID userUUID(final String userName) {
return rbacUserRepository.findByName(userName).getUuid();
}
void exactlyTheseRbacUsersAreReturned(final List<RbacUserEntity> actualResult, final String... expectedUserNames) { void exactlyTheseRbacUsersAreReturned(final List<RbacUserEntity> actualResult, final String... expectedUserNames) {
assertThat(actualResult) assertThat(actualResult)
.extracting(RbacUserEntity::getName) .extracting(RbacUserEntity::getName)