1
0
hs.hsadmin.ng/src/test/java/net/hostsharing/hsadminng/rbac/grant/RbacGrantControllerAcceptanceTest.java
Michael Hoennig c1d3d583e7 feature/run-office-module-without-booking-and-hosting (#148)
Co-authored-by: Michael Hoennig <michael@hoennig.de>
Co-authored-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
Co-authored-by: Timotheus Pokorra <timotheus.pokorra@solidcharity.com>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/148
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
2025-01-21 14:36:49 +01:00

510 lines
22 KiB
Java

package net.hostsharing.hsadminng.rbac.grant;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.ValidatableResponse;
import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
import net.hostsharing.hsadminng.rbac.role.RbacRoleEntity;
import net.hostsharing.hsadminng.rbac.role.RbacRoleRepository;
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.*;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
)
@ActiveProfiles("test")
@Transactional(readOnly = true, propagation = Propagation.NEVER)
@Tag("generalIntegrationTest")
class RbacGrantControllerAcceptanceTest extends ContextBasedTest {
@LocalServerPort
Integer port;
@PersistenceContext
EntityManager em;
@Autowired
RbacSubjectRepository rbacSubjectRepository;
@Autowired
RbacRoleRepository rbacRoleRepository;
@Autowired
RbacGrantRepository rbacGrantRepository;
@Autowired
JpaAttempt jpaAttempt;
@Nested
class ListGrants {
@Test
void globalAdmin_withoutAssumedRole_canViewAllGrants() {
RestAssured // @formatter:off
.given()
.header("current-subject", "superuser-alex@hostsharing.net")
.port(port)
.when()
.get("http://localhost/api/rbac/grants")
.then().log().all().assertThat()
.statusCode(200)
.contentType("application/json")
.body("", hasItem(
allOf(
// TODO: should there be a grantedByRole or just a grantedByTrigger?
hasEntry("grantedByRoleIdName", "rbactest.customer#xxx:OWNER"),
hasEntry("grantedRoleIdName", "rbactest.customer#xxx:ADMIN"),
hasEntry("granteeSubjectName", "customer-admin@xxx.example.com")
)
))
.body("", hasItem(
allOf(
// TODO: should there be a grantedByRole or just a grantedByTrigger?
hasEntry("grantedByRoleIdName", "rbactest.customer#yyy:OWNER"),
hasEntry("grantedRoleIdName", "rbactest.customer#yyy:ADMIN"),
hasEntry("granteeSubjectName", "customer-admin@yyy.example.com")
)
))
.body("", hasItem(
allOf(
hasEntry("grantedByRoleIdName", "rbac.global#global:ADMIN"),
hasEntry("grantedRoleIdName", "rbac.global#global:ADMIN"),
hasEntry("granteeSubjectName", "superuser-fran@hostsharing.net")
)
))
.body("", hasItem(
allOf(
hasEntry("grantedByRoleIdName", "rbactest.customer#xxx:ADMIN"),
hasEntry("grantedRoleIdName", "rbactest.package#xxx00:ADMIN"),
hasEntry("granteeSubjectName", "pac-admin-xxx00@xxx.example.com")
)
))
.body("", hasItem(
allOf(
hasEntry("grantedByRoleIdName", "rbactest.customer#zzz:ADMIN"),
hasEntry("grantedRoleIdName", "rbactest.package#zzz02:ADMIN"),
hasEntry("granteeSubjectName", "pac-admin-zzz02@zzz.example.com")
)
))
.body("size()", greaterThanOrEqualTo(14));
// @formatter:on
}
@Test
void globalAdmin_withAssumedPackageAdminRole_canViewPacketRelatedGrants() {
RestAssured // @formatter:off
.given()
.header("current-subject", "superuser-alex@hostsharing.net")
.header("assumed-roles", "rbactest.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", "rbactest.customer#yyy:ADMIN"),
hasEntry("grantedRoleIdName", "rbactest.package#yyy00:ADMIN"),
hasEntry("granteeSubjectName", "pac-admin-yyy00@yyy.example.com")
)
))
.body("size()", is(1));
// @formatter:on
}
@Test
void packageAdmin_withoutAssumedRole_canViewPacketRelatedGrants() {
RestAssured // @formatter:off
.given()
.header("current-subject", "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", "rbactest.customer#yyy:ADMIN"),
hasEntry("grantedRoleIdName", "rbactest.package#yyy00:ADMIN"),
hasEntry("granteeSubjectName", "pac-admin-yyy00@yyy.example.com")
)
))
.body("[0].grantedByRoleIdName", is("rbactest.customer#yyy:ADMIN"))
.body("[0].grantedRoleIdName", is("rbactest.package#yyy00:ADMIN"))
.body("[0].granteeSubjectName", is("pac-admin-yyy00@yyy.example.com"));
// @formatter:on
}
}
@Nested
class GetListOfGrantsByUuid {
@Test
void customerAdmin_withAssumedPacketAdminRole_canReadPacketAdminsGrantById() {
// given
final var givencurrentSubjectAsPackageAdmin = new Subject("customer-admin@xxx.example.com");
final var givenGranteeUser = findRbacSubjectByName("pac-admin-xxx00@xxx.example.com");
final var givenGrantedRole = getRbacRoleByName("rbactest.package#xxx00:ADMIN");
// when
final var grant = givencurrentSubjectAsPackageAdmin.getListOfGrantsByUuid()
.forGrantedRole(givenGrantedRole).toGranteeUser(givenGranteeUser);
// then
grant.assertThat()
.statusCode(200)
.body("grantedByRoleIdName", is("rbactest.customer#xxx:ADMIN"))
.body("grantedRoleIdName", is("rbactest.package#xxx00:ADMIN"))
.body("granteeSubjectName", is("pac-admin-xxx00@xxx.example.com"));
}
@Test
void packageAdmin_withoutAssumedRole_canReadItsOwnGrantById() {
// given
final var givencurrentSubjectAsPackageAdmin = new Subject("pac-admin-xxx00@xxx.example.com");
final var givenGranteeUser = findRbacSubjectByName("pac-admin-xxx00@xxx.example.com");
final var givenGrantedRole = getRbacRoleByName("rbactest.package#xxx00:ADMIN");
// when
final var grant = givencurrentSubjectAsPackageAdmin.getListOfGrantsByUuid()
.forGrantedRole(givenGrantedRole).toGranteeUser(givenGranteeUser);
// then
grant.assertThat()
.statusCode(200)
.body("grantedByRoleIdName", is("rbactest.customer#xxx:ADMIN"))
.body("grantedRoleIdName", is("rbactest.package#xxx00:ADMIN"))
.body("granteeSubjectName", is("pac-admin-xxx00@xxx.example.com"));
}
@Test
void packageAdmin_withAssumedPackageAdmin_canStillReadItsOwnGrantById() {
// given
final var givencurrentSubjectAsPackageAdmin = new Subject(
"pac-admin-xxx00@xxx.example.com",
"rbactest.package#xxx00:ADMIN");
final var givenGranteeUser = findRbacSubjectByName("pac-admin-xxx00@xxx.example.com");
final var givenGrantedRole = getRbacRoleByName("rbactest.package#xxx00:ADMIN");
// when
final var grant = givencurrentSubjectAsPackageAdmin.getListOfGrantsByUuid()
.forGrantedRole(givenGrantedRole).toGranteeUser(givenGranteeUser);
// then
grant.assertThat()
.statusCode(200)
.body("grantedByRoleIdName", is("rbactest.customer#xxx:ADMIN"))
.body("grantedRoleIdName", is("rbactest.package#xxx00:ADMIN"))
.body("granteeSubjectName", is("pac-admin-xxx00@xxx.example.com"));
}
@Test
void packageAdmin_withAssumedPackageTenantRole_canNotReadItsOwnGrantByIdAnymore() {
// given
final var givencurrentSubjectAsPackageAdmin = new Subject(
"pac-admin-xxx00@xxx.example.com",
"rbactest.package#xxx00:TENANT");
final var givenGranteeUser = findRbacSubjectByName("pac-admin-xxx00@xxx.example.com");
final var givenGrantedRole = getRbacRoleByName("rbactest.package#xxx00:ADMIN");
final var grant = givencurrentSubjectAsPackageAdmin.getListOfGrantsByUuid()
.forGrantedRole(givenGrantedRole).toGranteeUser(givenGranteeUser);
// then
grant.assertThat()
.statusCode(404);
}
}
@Nested
class GrantRoleToSubject {
@Test
void packageAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() {
// given
final var givenNewUser = createRbacSubject();
final var givenRoleToGrant = "rbactest.package#xxx00:ADMIN";
final var givencurrentSubjectAsPackageAdmin = new Subject("pac-admin-xxx00@xxx.example.com", givenRoleToGrant);
final var givenOwnPackageAdminRole =
getRbacRoleByName(givencurrentSubjectAsPackageAdmin.assumedRole);
// when
final var response = givencurrentSubjectAsPackageAdmin
.grantsRole(givenOwnPackageAdminRole).assumed()
.toUser(givenNewUser);
// then
response.assertThat()
.statusCode(201)
.body("grantedByRoleIdName", is("rbactest.package#xxx00:ADMIN"))
.body("assumed", is(true))
.body("grantedRoleIdName", is("rbactest.package#xxx00:ADMIN"))
.body("granteeSubjectName", is(givenNewUser.getName()));
assertThat(findAllGrantsOf(givencurrentSubjectAsPackageAdmin))
.extracting(RbacGrantEntity::toDisplay)
.contains("{ grant role:" + givenOwnPackageAdminRole.getRoleName() +
" to user:" + givenNewUser.getName() +
" by role:" + givenRoleToGrant + " and assume }");
}
@Test
void packageAdmin_canNotGrantAlienPackageAdminRole_toArbitraryUser() {
// given
final var givenNewUser = createRbacSubject();
final var givenRoleToGrant = "rbactest.package#xxx00:ADMIN";
final var givencurrentSubjectAsPackageAdmin = new Subject("pac-admin-xxx00@xxx.example.com", givenRoleToGrant);
final var givenAlienPackageAdminRole = getRbacRoleByName("rbactest.package#yyy00:ADMIN");
// when
final var result = givencurrentSubjectAsPackageAdmin
.grantsRole(givenAlienPackageAdminRole).assumed()
.toUser(givenNewUser);
// then
result.assertThat()
.statusCode(403)
.body("message", containsString("Access to granted role"))
.body("message", containsString("forbidden for rbactest.package#xxx00:ADMIN"));
assertThat(findAllGrantsOf(givencurrentSubjectAsPackageAdmin))
.extracting(RbacGrantEntity::getGranteeSubjectName)
.doesNotContain(givenNewUser.getName());
}
}
@Nested
class RevokeRoleFromSubject {
@Test
@Transactional(propagation = Propagation.NEVER)
void packageAdmin_canRevokePackageAdminRole_grantedByPackageAdmin_fromArbitraryUser() {
// given
final var givenArbitraryUser = createRbacSubject();
final var givenRoleToGrant = "rbactest.package#xxx00:ADMIN";
final var givenCurrentSubjectAsPackageAdmin = new Subject("pac-admin-xxx00@xxx.example.com", givenRoleToGrant);
final var givenOwnPackageAdminRole = getRbacRoleByName("rbactest.package#xxx00:ADMIN");
// and given an existing grant
assumeCreated(givenCurrentSubjectAsPackageAdmin
.grantsRole(givenOwnPackageAdminRole).assumed()
.toUser(givenArbitraryUser));
assumeGrantExists(
givenCurrentSubjectAsPackageAdmin,
"{ grant role:%s to user:%s by role:%s and assume }".formatted(
givenOwnPackageAdminRole.getRoleName(),
givenArbitraryUser.getName(),
givenCurrentSubjectAsPackageAdmin.assumedRole));
// when
final var revokeResponse = givenCurrentSubjectAsPackageAdmin
.revokesRole(givenOwnPackageAdminRole)
.fromUser(givenArbitraryUser);
// then
revokeResponse.assertThat().statusCode(204);
assertThat(findAllGrantsOf(givenCurrentSubjectAsPackageAdmin))
.extracting(RbacGrantEntity::getGranteeSubjectName)
.doesNotContain(givenArbitraryUser.getName());
}
}
private void assumeCreated(final ValidatableResponse response) {
assertThat(response.extract().response().statusCode()).isEqualTo(201);
}
class Subject {
final String currentSubject;
final String assumedRole;
public Subject(final String currentSubject, final String assumedRole) {
this.currentSubject = currentSubject;
this.assumedRole = assumedRole;
}
public Subject(final String currentSubject) {
this(currentSubject, "");
}
GrantFixture grantsRole(final RbacRoleEntity givenOwnPackageAdminRole) {
return new GrantFixture(givenOwnPackageAdminRole);
}
RevokeFixture revokesRole(final RbacRoleEntity givenOwnPackageAdminRole) {
return new RevokeFixture(givenOwnPackageAdminRole);
}
GetListOfGrantsByUuidFixture getListOfGrantsByUuid() {
return new GetListOfGrantsByUuidFixture();
}
class GrantFixture {
private Subject grantingSubject = Subject.this;
private final RbacRoleEntity grantedRole;
private boolean assumed;
private RbacSubjectEntity granteeUser;
public GrantFixture(final RbacRoleEntity roleToGrant) {
this.grantedRole = roleToGrant;
}
GrantFixture assumed() {
this.assumed = true;
return this;
}
ValidatableResponse toUser(final RbacSubjectEntity granteeUser) {
this.granteeUser = granteeUser;
return RestAssured // @formatter:ff
.given()
.header("current-subject", grantingSubject.currentSubject)
.header("assumed-roles", grantingSubject.assumedRole)
.contentType(ContentType.JSON)
.body("""
{
"assumed": true,
"grantedRole.uuid": "%s",
"granteeSubject.uuid": "%s"
}
""".formatted(
grantedRole.getUuid(),
granteeUser.getUuid())
)
.port(port)
.when()
.post("http://localhost/api/rbac/grants")
.then().log().all(); // @formatter:on
}
}
class RevokeFixture {
private Subject currentSubject = Subject.this;
private final RbacRoleEntity grantedRole;
private boolean assumed;
private RbacSubjectEntity granteeUser;
public RevokeFixture(final RbacRoleEntity roleToGrant) {
this.grantedRole = roleToGrant;
}
ValidatableResponse fromUser(final RbacSubjectEntity granteeUser) {
this.granteeUser = granteeUser;
return RestAssured // @formatter:ff
.given()
.header("current-subject", currentSubject.currentSubject)
.header("assumed-roles", currentSubject.assumedRole)
.contentType(ContentType.JSON)
.body("""
{
"assumed": true,
"grantedRole.uuid": "%s",
"granteeSubject.uuid": "%s"
}
""".formatted(
grantedRole.getUuid(),
granteeUser.getUuid())
)
.port(port)
.when()
.delete("http://localhost/api/rbac/grants/%s/%s".formatted(
grantedRole.getUuid(), granteeUser.getUuid()
))
.then().log().all(); // @formatter:on
}
}
private class GetListOfGrantsByUuidFixture {
private Subject currentSubject = Subject.this;
private RbacRoleEntity grantedRole;
GetListOfGrantsByUuidFixture forGrantedRole(final RbacRoleEntity grantedRole) {
this.grantedRole = grantedRole;
return this;
}
ValidatableResponse toGranteeUser(final RbacSubjectEntity granteeUser) {
return RestAssured // @formatter:ff
.given()
.header("current-subject", currentSubject.currentSubject)
.header("assumed-roles", currentSubject.assumedRole)
.port(port)
.when()
.get("http://localhost/api/rbac/grants/%s/%s".formatted(
grantedRole.getUuid(), granteeUser.getUuid()
))
.then().log().all();
// @formatter:on
}
}
}
private void assumeGrantExists(final Subject grantingSubject, final String expectedGrant) {
assertThat(findAllGrantsOf(grantingSubject))
.extracting(RbacGrantEntity::toDisplay)
.contains(expectedGrant);
}
List<RbacGrantEntity> findAllGrantsOf(final Subject grantingSubject) {
return jpaAttempt.transacted(() -> {
context(grantingSubject.currentSubject, null);
return rbacGrantRepository.findAll();
}).returnedValue();
}
RbacSubjectEntity createRbacSubject() {
return jpaAttempt.transacted(() -> {
final String newUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
context(null);
return rbacSubjectRepository.create(new RbacSubjectEntity(UUID.randomUUID(), newUserName));
}).returnedValue();
}
RbacSubjectEntity findRbacSubjectByName(final String userName) {
return jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", null);
return rbacSubjectRepository.findByName(userName);
}).assertNotNull().returnedValue();
}
RbacRoleEntity getRbacRoleByName(final String roleName) {
return jpaAttempt.transacted(() -> {
context("superuser-alex@hostsharing.net", null);
return rbacRoleRepository.findByRoleName(roleName);
}).assertNotNull().returnedValue();
}
}