diff --git a/build.gradle b/build.gradle index fb86296d..86c3b5d2 100644 --- a/build.gradle +++ b/build.gradle @@ -156,11 +156,8 @@ jacocoTestReport { classDirectories.setFrom(files(classDirectories.files.collect { fileTree(dir: it, exclude: [ "net/hostsharing/hsadminng/generated/**/*.class", - - // TODO: improve test code coverage for these classes: - "net/hostsharing/hsadminng/rbac/rbacuser/UserController.class", - "net/hostsharing/hsadminng/rbac/rbacgrant/GrantController.class", - "net/hostsharing/hsadminng/hs/hscustomer/CustomerController.class" + "net/hostsharing/hsadminng/TestController.class", + "net/hostsharing/hsadminng/hs/hscustomer/HsadminNgApplication.class" ]) })) } @@ -174,7 +171,7 @@ jacocoTestCoverageVerification { rule { excludes = ['net.hostsharing.hsadminng.generated.**'] 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', // TODO: improve test code coverage: - 'net.hostsharing.hsadminng.rbac.rbacuser.UserController', - 'net.hostsharing.hsadminng.hs.hscustomer.CustomerController' + 'net.hostsharing.hsadminng.Mapper', ] limit { counter = 'LINE' value = 'COVEREDRATIO' - minimum = 0.7 + minimum = 0.95 } } rule { @@ -205,13 +201,7 @@ jacocoTestCoverageVerification { excludes = [ 'net.hostsharing.hsadminng.generated.**', 'net.hostsharing.hsadminng.HsadminNgApplication.*', - - // 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)' - ] + 'net.hostsharing.hsadminng.TestController.*'] limit { counter = 'BRANCH' diff --git a/gradle.properties b/gradle.properties index 84b40d41..eca52c92 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,11 @@ # Spring BOM overrides 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 diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerController.java b/src/main/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerController.java index a167bc74..52337815 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerController.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hscustomer; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.generated.api.v1.api.CustomersApi; 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.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -33,7 +34,7 @@ public class CustomerController implements CustomersApi { String prefix ) { context.setCurrentUser(userName); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } @@ -51,7 +52,7 @@ public class CustomerController implements CustomersApi { context.setCurrentTask("create new customer: #" + customer.getReference() + " / " + customer.getPrefix()); context.setCurrentUser(currentUser); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } if (customer.getUuid() == null) { @@ -62,7 +63,7 @@ public class CustomerController implements CustomersApi { final var uri = MvcUriComponentsBuilder.fromController(getClass()) - .path("/api/rbac-users/{id}") + .path("/api/customers/{id}") .buildAndExpand(customer.getUuid()) .toUri(); return ResponseEntity.created(uri).body(map(saved, CustomerResource.class)); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hspackage/PackageController.java b/src/main/java/net/hostsharing/hsadminng/hs/hspackage/PackageController.java index d95b1916..8a3d6566 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hspackage/PackageController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hspackage/PackageController.java @@ -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.model.PackageResource; 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.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -33,7 +34,7 @@ public class PackageController implements PackagesApi { String name ) { context.setCurrentUser(userName); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } final var result = packageRepository.findAllByOptionalNameLike(name); @@ -49,7 +50,7 @@ public class PackageController implements PackagesApi { final PackageUpdateResource body) { context.setCurrentUser(currentUser); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } final var current = packageRepository.findByUuid(packageUuid); diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantController.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantController.java index 09b5ee83..4ff43744 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantController.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.rbacgrant; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.generated.api.v1.api.RbacgrantsApi; 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.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -38,7 +39,7 @@ public class RbacGrantController implements RbacgrantsApi { final UUID granteeUserUuid) { context.setCurrentUser(currentUser); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } @@ -57,7 +58,7 @@ public class RbacGrantController implements RbacgrantsApi { final String assumedRoles) { context.setCurrentUser(currentUser); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } return ResponseEntity.ok(mapList(rbacGrantRepository.findAll(), RbacGrantResource.class)); @@ -72,7 +73,7 @@ public class RbacGrantController implements RbacgrantsApi { context.setCurrentTask("granting role to user"); context.setCurrentUser(currentUser); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } @@ -98,7 +99,7 @@ public class RbacGrantController implements RbacgrantsApi { context.setCurrentTask("revoking role from user"); context.setCurrentUser(currentUser); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleController.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleController.java index 4cf34f85..d3c4200e 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleController.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.rbac.rbacrole; import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.generated.api.v1.api.RbacrolesApi; 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.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -29,7 +30,7 @@ public class RbacRoleController implements RbacrolesApi { final String assumedRoles) { context.setCurrentUser(currentUser); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } return ResponseEntity.ok(mapList(rbacRoleRepository.findAll(), RbacRoleResource.class)); diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java index 4b05ec34..5e913320 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java @@ -2,8 +2,11 @@ package net.hostsharing.hsadminng.rbac.rbacuser; import net.hostsharing.hsadminng.context.Context; 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.RbacUserResource; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantId; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; @@ -28,7 +31,7 @@ public class RbacUserController implements RbacusersApi { @Override @Transactional public ResponseEntity createUser( - @RequestBody final RbacUserResource body + final RbacUserResource body ) { context.setCurrentTask("creating new user: " + body.getName()); context.setCurrentUser(body.getName()); @@ -47,22 +50,33 @@ public class RbacUserController implements RbacusersApi { } @Override - public ResponseEntity> getUserById( + @Transactional(readOnly = true) + public ResponseEntity getUserById( final String currentUser, final String assumedRoles, - final String userName) { - return null; // TODO implement getUserById + final UUID userUuid) { + + 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 @Transactional(readOnly = true) public ResponseEntity> listUsers( - @RequestHeader(name = "current-user") final String currentUserName, - @RequestHeader(name = "assumed-roles", required = false) final String assumedRoles, - @RequestParam(name = "name", required = false) final String userName + final String currentUserName, + final String assumedRoles, + final String userName ) { context.setCurrentUser(currentUserName); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } return ResponseEntity.ok(mapList(rbacUserRepository.findByOptionalNameLike(userName), RbacUserResource.class)); @@ -71,14 +85,14 @@ public class RbacUserController implements RbacusersApi { @Override @Transactional(readOnly = true) public ResponseEntity> listUserPermissions( - @RequestHeader(name = "current-user") final String currentUserName, - @RequestHeader(name = "assumed-roles", required = false) final String assumedRoles, - @PathVariable(name = "userName") final String userName + final String currentUserName, + final String assumedRoles, + final UUID userUuid ) { context.setCurrentUser(currentUserName); - if (assumedRoles != null && !assumedRoles.isBlank()) { + if (!StringUtils.isBlank(assumedRoles)) { context.assumeRoles(assumedRoles); } - return ResponseEntity.ok(mapList(rbacUserRepository.findPermissionsOfUser(userName), RbacUserPermissionResource.class)); + return ResponseEntity.ok(mapList(rbacUserRepository.findPermissionsOfUserByUuid(userUuid), RbacUserPermissionResource.class)); } } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java index 03c99afa..9c107f8c 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java @@ -22,8 +22,8 @@ public interface RbacUserRepository extends Repository { RbacUserEntity findByUuid(UUID uuid); - @Query(value = "select * from grantedPermissions(:userName)", nativeQuery = true) - List findPermissionsOfUser(String userName); + @Query(value = "select * from grantedPermissions(:userUuid)", nativeQuery = true) + List findPermissionsOfUserByUuid(UUID userUuid); /* Can't use save/saveAndFlush from SpringData because the uuid is not generated on the entity level, diff --git a/src/main/resources/api-definition.yaml b/src/main/resources/api-definition.yaml index db7cbcde..d2f43735 100644 --- a/src/main/resources/api-definition.yaml +++ b/src/main/resources/api-definition.yaml @@ -13,12 +13,12 @@ paths: /api/rbac-users: $ref: "./api-definition/rbac-users.yaml" - /api/rbac-users/{userUuid}: - $ref: "./api-definition/rbac-users-with-id.yaml" - /api/rbac-users/{userUuid}/permissions: $ref: "./api-definition/rbac-users-with-id-permissions.yaml" + /api/rbac-users/{userUuid}: + $ref: "./api-definition/rbac-users-with-id.yaml" + /api/rbac-roles: $ref: "./api-definition/rbac-roles.yaml" diff --git a/src/main/resources/api-definition/rbac-users-with-id-permissions.yaml b/src/main/resources/api-definition/rbac-users-with-id-permissions.yaml index 8ac92372..d3900363 100644 --- a/src/main/resources/api-definition/rbac-users-with-id-permissions.yaml +++ b/src/main/resources/api-definition/rbac-users-with-id-permissions.yaml @@ -6,11 +6,12 @@ get: parameters: - $ref: './api-definition/auth.yaml#/components/parameters/currentUser' - $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles' - - name: userName + - name: userUuid in: path required: true schema: type: string + format: uuid responses: "200": description: OK diff --git a/src/main/resources/api-definition/rbac-users-with-id.yaml b/src/main/resources/api-definition/rbac-users-with-id.yaml index 6e8d0f28..39f8ec27 100644 --- a/src/main/resources/api-definition/rbac-users-with-id.yaml +++ b/src/main/resources/api-definition/rbac-users-with-id.yaml @@ -6,20 +6,19 @@ get: parameters: - $ref: './api-definition/auth.yaml#/components/parameters/currentUser' - $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles' - - name: userName + - name: userUuid in: path required: true schema: type: string + format: uuid responses: "200": description: OK content: 'application/json': schema: - type: array - items: - $ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUserPermission' + $ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUser' "401": $ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized' diff --git a/src/main/resources/db/changelog/055-rbac-views.sql b/src/main/resources/db/changelog/055-rbac-views.sql index 8f33d83b..72fdb222 100644 --- a/src/main/resources/db/changelog/055-rbac-views.sql +++ b/src/main/resources/db/changelog/055-rbac-views.sql @@ -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 null on null input language plpgsql as $$ declare - targetUserId uuid; currentUserId uuid; begin -- @formatter:off - if cardinality(assumedRoles()) > 0 then - 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(); + if hasGlobalRoleGranted(targetUserUuid) and not hasGlobalRoleGranted(currentUserId) then + raise exception '[403] permissions of user "%" are not accessible to user "%"', targetUserUuid, currentUser(); end if; return query select @@ -235,12 +229,12 @@ begin 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 + from queryPermissionsGrantedToSubjectId( targetUserUuid) 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) + where isGranted(targetUserUuid, r.uuid) ) xp; -- @formatter:on end; $$; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerControllerAcceptanceTest.java index dd0cc200..bc0ec5f1 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hscustomer/CustomerControllerAcceptanceTest.java @@ -141,6 +141,36 @@ class CustomerControllerAcceptanceTest { .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 void customerAdmin_withoutAssumedRole_canNotCreateCustomer() throws Exception { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java index a057fc73..b73f1d4b 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java @@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assumptions.assumeThat; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @@ -55,6 +56,108 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { @Autowired 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 class GetGrantById { diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java index 3c634c0a..7234d8a9 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java @@ -2,8 +2,10 @@ package net.hostsharing.hsadminng.rbac.rbacuser; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import net.hostsharing.hsadminng.Accepts; import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -19,7 +21,7 @@ import static org.hamcrest.Matchers.*; @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - classes = HsadminNgApplication.class + classes = { HsadminNgApplication.class, JpaAttempt.class } ) @Transactional class RbacUserControllerAcceptanceTest { @@ -30,6 +32,9 @@ class RbacUserControllerAcceptanceTest { @Autowired EntityManager em; + @Autowired + JpaAttempt jpaAttempt; + @Autowired Context context; @@ -37,9 +42,125 @@ class RbacUserControllerAcceptanceTest { RbacUserRepository rbacUserRepository; @Nested - class ApiRbacUsersGet { + class CreateRbacUser { @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() { // @formatter:off @@ -65,6 +186,7 @@ class RbacUserControllerAcceptanceTest { } @Test + @Accepts({ "USR:F(Filter)" }) void hostsharingAdmin_withoutAssumedRole_canViewAllUsersByName() { // @formatter:off @@ -85,6 +207,30 @@ class RbacUserControllerAcceptanceTest { } @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() { // @formatter:off @@ -106,6 +252,7 @@ class RbacUserControllerAcceptanceTest { } @Test + @Accepts({ "USR:L(List)", "USR:X(Access Control)" }) void packetAdmin_withoutAssumedRole_canViewAllUsersOfItsPackage() { // @formatter:off @@ -125,37 +272,135 @@ class RbacUserControllerAcceptanceTest { } @Nested - class ApiRbacUsersPost { + class ListRbacUserPermissions { @Test - void anybody_canCreateANewUser() { + @Accepts({ "PRM:L(List)" }) + void hostsharingAdmin_withoutAssumedRole_canViewArbitraryUsersPermissions() { + final var givenUser = findRbacUserByName("pac-admin-yyy00@yyy.example.com"); // @formatter:off - final var location = RestAssured + RestAssured .given() - .contentType(ContentType.JSON) - .body(""" - { - "name": "new-user@example.com" - } - """) + .header("current-user", "mike@hostsharing.net") .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"); + .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 + } - // 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"); + @Test + @Accepts({ "PRM:L(List)" }) + void hostsharingAdmin_withAssumedCustomerAdminRole_canViewArbitraryUsersPermissions() { + final var givenUser = findRbacUserByName("pac-admin-yyy00@yyy.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(); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java index 7a29a29b..edcc3184 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java @@ -231,35 +231,19 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { context("mike@hostsharing.net"); // when - final var result = rbacUserRepository.findPermissionsOfUser("mike@hostsharing.net"); + final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("mike@hostsharing.net")); // then 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 public void customerAdmin_withoutAssumedRole_canViewTheirOwnPermissions() { // given context("customer-admin@xxx.example.com"); // when - final var result = rbacUserRepository.findPermissionsOfUser("customer-admin@xxx.example.com"); + final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("customer-admin@xxx.example.com")); // then allTheseRbacPermissionsAreReturned( @@ -299,16 +283,18 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { public void customerAdmin_withoutAssumedRole_isNotAllowedToViewGlobalAdminsPermissions() { // given context("customer-admin@xxx.example.com"); + final UUID userUuid = userUUID("mike@hostsharing.net"); // when final var result = attempt(em, () -> - rbacUserRepository.findPermissionsOfUser("mike@hostsharing.net") + rbacUserRepository.findPermissionsOfUserByUuid(userUuid) ); // then result.assertExceptionWithRootCauseMessage( 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 @@ -317,7 +303,7 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { context("customer-admin@xxx.example.com"); // 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 allTheseRbacPermissionsAreReturned( @@ -353,7 +339,7 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { context("customer-admin@xxx.example.com"); // 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 noRbacPermissionsAreReturned(result); @@ -365,7 +351,7 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { context("pac-admin-xxx00@xxx.example.com"); // 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 allTheseRbacPermissionsAreReturned( @@ -397,6 +383,10 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { } } + UUID userUUID(final String userName) { + return rbacUserRepository.findByName(userName).getUuid(); + } + void exactlyTheseRbacUsersAreReturned(final List actualResult, final String... expectedUserNames) { assertThat(actualResult) .extracting(RbacUserEntity::getName)