1
0

implements user granting roles to other users

This commit is contained in:
Michael Hoennig
2022-08-16 10:46:41 +02:00
parent 7869d07d30
commit c8e835f880
21 changed files with 425 additions and 227 deletions

View File

@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.annotation.DirtiesContext;
import javax.transaction.Transactional;
@ -11,6 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = Context.class)
@DirtiesContext
class ContextIntegrationTests {
@Autowired

View File

@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
@ -19,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class })
@DirtiesContext
class CustomerRepositoryIntegrationTest {
@Autowired

View File

@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
@ -18,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class })
@DirtiesContext
class PackageRepositoryIntegrationTest {
@Autowired

View File

@ -15,19 +15,22 @@ 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.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.CoreMatchers.containsString;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class }
)
@Accepts({ "ROL:S(Schema)" })
@Transactional(propagation = Propagation.NEVER)
class RbacGrantControllerAcceptanceTest {
@LocalServerPort
@ -57,23 +60,25 @@ class RbacGrantControllerAcceptanceTest {
// given
final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
final String givenPackageAdmin = "aaa00@aaa.example.com";
final String givenCurrentUserPackageAdmin = "aaa00@aaa.example.com";
final String givenAssumedRole = "package#aaa00.admin";
final var givenOwnPackageAdminRole = "package#aaa00.admin";
// when
RestAssured // @formatter:off
.given()
.header("current-user", givenPackageAdmin)
.header("current-user", givenCurrentUserPackageAdmin)
.header("assumed-roles", givenAssumedRole)
.contentType(ContentType.JSON)
.body("""
{
"userUuid": "%s",
"roleUuid": "%s",
"assumed": true,
"empowered": false
"grantedRoleUuid": "%s",
"granteeUserUuid": "%s"
}
""".formatted(
createRBacUser(givenNewUserName).getUuid().toString(),
findRbacRoleByName(givenOwnPackageAdminRole).getUuid().toString())
findRbacRoleByName(givenOwnPackageAdminRole).getUuid().toString(),
createRBacUser(givenNewUserName).getUuid().toString())
)
.port(port)
.when()
@ -83,9 +88,11 @@ class RbacGrantControllerAcceptanceTest {
// @formatter:on
// then
assertThat(findAllGrantsOfUser(givenPackageAdmin))
assertThat(findAllGrantsOfUser(givenCurrentUserPackageAdmin))
.extracting(RbacGrantEntity::toDisplay)
.contains("grant( " + givenNewUserName + " -> " + givenOwnPackageAdminRole + ": assumed )");
.contains("{ grant assumed role " + givenOwnPackageAdminRole +
" to user " + givenNewUserName +
" by role " + givenAssumedRole + " }");
}
@Test
@ -94,35 +101,38 @@ class RbacGrantControllerAcceptanceTest {
// given
final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
final String givenPackageAdmin = "aaa00@aaa.example.com";
final String givenCurrentUserPackageAdmin = "aaa00@aaa.example.com";
final String givenAssumedRole = "package#aaa00.admin";
final var givenAlienPackageAdminRole = "package#aab00.admin";
// when
RestAssured // @formatter:off
.given()
.header("current-user", givenPackageAdmin)
.contentType(ContentType.JSON)
.body("""
{
"userUuid": "%s",
"roleUuid": "%s",
"assumed": true,
"empowered": false
}
""".formatted(
createRBacUser(givenNewUserName).getUuid().toString(),
findRbacRoleByName(givenAlienPackageAdminRole).getUuid().toString())
)
.port(port)
.header("current-user", givenCurrentUserPackageAdmin)
.header("assumed-roles", givenAssumedRole)
.contentType(ContentType.JSON)
.body("""
{
"assumed": true,
"grantedRoleUuid": "%s",
"granteeUserUuid": "%s"
}
""".formatted(
findRbacRoleByName(givenAlienPackageAdminRole).getUuid().toString(),
createRBacUser(givenNewUserName).getUuid().toString())
)
.port(port)
.when()
.post("http://localhost/api/rbac-grants")
.post("http://localhost/api/rbac-grants")
.then().assertThat()
.statusCode(403);
.body("message", containsString("Access to granted role"))
.body("message", containsString("forbidden for {package#aaa00.admin}"))
.statusCode(403);
// @formatter:on
// then
assertThat(findAllGrantsOfUser(givenPackageAdmin))
.extracting(RbacGrantEntity::getUserName)
assertThat(findAllGrantsOfUser(givenCurrentUserPackageAdmin))
.extracting(RbacGrantEntity::getGranteeUserName)
.doesNotContain(givenNewUserName);
}
@ -134,9 +144,9 @@ class RbacGrantControllerAcceptanceTest {
}
RbacUserEntity createRBacUser(final String userName) {
return jpaAttempt.transacted(() -> {
return rbacUserRepository.create(new RbacUserEntity(UUID.randomUUID(), userName));
}).returnedValue();
return jpaAttempt.transacted(() ->
rbacUserRepository.create(new RbacUserEntity(UUID.randomUUID(), userName))
).returnedValue();
}
RbacRoleEntity findRbacRoleByName(final String roleName) {
@ -145,5 +155,4 @@ class RbacGrantControllerAcceptanceTest {
return rbacRoleRepository.findByRoleName(roleName);
}).returnedValue();
}
}

View File

@ -3,7 +3,9 @@ package net.hostsharing.hsadminng.rbac.rbacgrant;
import net.hostsharing.hsadminng.Accepts;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserEntity;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository;
import net.hostsharing.test.JpaAttempt;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@ -11,15 +13,18 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.UUID;
import static net.hostsharing.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, RbacGrantRepository.class })
@ComponentScan(basePackageClasses = { RbacGrantRepository.class, Context.class, JpaAttempt.class })
@DirtiesContext
@Accepts({ "GRT:S(Schema)" })
class RbacGrantRepositoryIntegrationTest {
@ -39,6 +44,9 @@ class RbacGrantRepositoryIntegrationTest {
@Autowired
EntityManager em;
@Autowired
JpaAttempt jpaAttempt;
@Nested
class FindAllRbacGrants {
@ -54,7 +62,7 @@ class RbacGrantRepositoryIntegrationTest {
// then
exactlyTheseRbacGrantsAreReturned(
result,
"grant( aaa00@aaa.example.com -> package#aaa00.admin: managed assumed empowered )");
"{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }");
}
@Test
@ -69,28 +77,26 @@ class RbacGrantRepositoryIntegrationTest {
// then
exactlyTheseRbacGrantsAreReturned(
result,
"grant( admin@aaa.example.com -> customer#aaa.admin: managed assumed empowered )",
"grant( aaa00@aaa.example.com -> package#aaa00.admin: managed assumed empowered )",
"grant( aaa01@aaa.example.com -> package#aaa01.admin: managed assumed empowered )",
"grant( aaa02@aaa.example.com -> package#aaa02.admin: managed assumed empowered )");
"{ grant assumed role customer#aaa.admin to user admin@aaa.example.com by role global#hostsharing.admin }",
"{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }",
"{ grant assumed role package#aaa01.admin to user aaa01@aaa.example.com by role customer#aaa.admin }",
"{ grant assumed role package#aaa02.admin to user aaa02@aaa.example.com by role customer#aaa.admin }");
}
@Test
@Accepts({ "GRT:L(List)" })
public void customerAdmin_withAssumedRole_cannotViewRbacGrants() {
public void customerAdmin_withAssumedRole_canOnlyViewRbacGrantsVisibleByAssumedRole() {
// given:
currentUser("admin@aaa.example.com");
assumedRoles("package#aab00.admin");
assumedRoles("package#aaa00.admin");
// when
final var result = attempt(
em,
() -> rbacGrantRepository.findAll());
final var result = rbacGrantRepository.findAll();
// then
result.assertExceptionWithRootCauseMessage(
JpaSystemException.class,
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin");
exactlyTheseRbacGrantsAreReturned(
result,
"{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }");
}
}
@ -102,24 +108,72 @@ class RbacGrantRepositoryIntegrationTest {
public void customerAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() {
// given
currentUser("admin@aaa.example.com");
final var userUuid = rbacUserRepository.findUuidByName("aac00@aac.example.com");
final var roleUuid = rbacRoleRepository.findByRoleName("package#aaa00.admin").getUuid();
assumedRoles("customer#aaa.admin");
final var givenArbitraryUserUuid = rbacUserRepository.findUuidByName("aac00@aac.example.com");
final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName("package#aaa00.admin").getUuid();
// when
final var grant = RbacGrantEntity.builder()
.userUuid(userUuid).roleUuid(roleUuid)
.assumed(true).empowered(false)
.granteeUserUuid(givenArbitraryUserUuid).grantedRoleUuid(givenOwnPackageRoleUuid)
.assumed(true)
.build();
final var attempt = attempt(em, () ->
rbacGrantRepository.save(grant)
);
// then
assertThat(attempt.wasSuccessful()).isTrue();
assertThat(attempt.caughtException()).isNull();
assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::toDisplay)
.contains("grant( aac00@aac.example.com -> package#aaa00.admin: assumed )");
.contains("{ grant assumed role package#aaa00.admin to user aac00@aac.example.com by role customer#aaa.admin }");
}
@Test
@Accepts({ "GRT:C(Create)" })
@Transactional(propagation = Propagation.NEVER)
public void packageAdmin_canNotGrantPackageOwnerRole() {
// given
record Given(RbacUserEntity arbitraryUser, UUID packageOwnerRoleUuid) {}
final var given = jpaAttempt.transacted(() -> {
// to find the uuids of we need to have access rights to these
currentUser("admin@aaa.example.com");
return new Given(
createNewUser(), // eigene Transaktion?
rbacRoleRepository.findByRoleName("package#aaa00.owner").getUuid()
);
}).returnedValue();
// when
final var attempt = jpaAttempt.transacted(() -> {
// now we try to use these uuids as a less privileged user
currentUser("aaa00@aaa.example.com");
assumedRoles("package#aaa00.admin");
final var grant = RbacGrantEntity.builder()
.granteeUserUuid(given.arbitraryUser.getUuid())
.grantedRoleUuid(given.packageOwnerRoleUuid)
.assumed(true)
.build();
rbacGrantRepository.save(grant);
});
// then
attempt.assertExceptionWithRootCauseMessage(
JpaSystemException.class,
"ERROR: [403] Access to granted role " + given.packageOwnerRoleUuid
+ " forbidden for {package#aaa00.admin}");
jpaAttempt.transacted(() -> {
currentUser(given.arbitraryUser.getName());
assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::toDisplay)
.hasSize(0);
// "{ grant assumed role package#aaa00.admin to user aac00@aac.example.com by role customer#aaa.admin }");
});
}
}
private RbacUserEntity createNewUser() {
return rbacUserRepository.create(
new RbacUserEntity(null, "test-user-" + System.currentTimeMillis() + "@example.com"));
}
void currentUser(final String currentUser) {
@ -134,7 +188,7 @@ class RbacGrantRepositoryIntegrationTest {
void exactlyTheseRbacGrantsAreReturned(final List<RbacGrantEntity> actualResult, final String... expectedGrant) {
assertThat(actualResult)
.filteredOn(g -> !g.getUserName().startsWith("test-user-")) // ignore test-users created by other tests
.filteredOn(g -> !g.getGranteeUserName().startsWith("test-user-")) // ignore test-users created by other tests
.extracting(RbacGrantEntity::toDisplay)
.containsExactlyInAnyOrder(expectedGrant);
}

View File

@ -10,6 +10,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.Commit;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@ -22,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = { RbacUserRepository.class, Context.class, JpaAttempt.class })
@DirtiesContext
class RbacUserRepositoryIntegrationTest {
@Autowired
@ -58,7 +60,7 @@ class RbacUserRepositoryIntegrationTest {
@Test
@Commit
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Transactional(propagation = Propagation.NEVER)
void anyoneCanCreateTheirOwnUser_committed() {
// given:

View File

@ -4,8 +4,7 @@ import junit.framework.AssertionFailedError;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import java.util.Optional;
@ -32,20 +31,16 @@ import static org.assertj.core.api.Assertions.assertThat;
public class JpaAttempt {
@Autowired
private final EntityManager em;
public JpaAttempt(final EntityManager em) {
this.em = em;
}
private TransactionTemplate transactionTemplate;
public static <T> JpaResult<T> attempt(final EntityManager em, final Supplier<T> code) {
try {
final var result = new JpaResult<T>(code.get(), null);
final var result = JpaResult.forValue(code.get());
em.flush();
em.clear();
return result;
} catch (RuntimeException exc) {
return new JpaResult<T>(null, exc);
} catch (final RuntimeException exc) {
return JpaResult.forException(exc);
}
}
@ -56,29 +51,50 @@ public class JpaAttempt {
});
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public <T> JpaResult<T> transacted(final Supplier<T> code) {
return attempt(em, code);
try {
return JpaResult.forValue(
transactionTemplate.execute(transactionStatus -> code.get()));
} catch (final RuntimeException exc) {
return JpaResult.forException(exc);
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void transacted(final Runnable code) {
attempt(em, () -> {
code.run();
return null;
});
public JpaResult<Void> transacted(final Runnable code) {
try {
transactionTemplate.execute(transactionStatus -> {
code.run();
return null;
});
return JpaResult.forVoidValue();
} catch (final RuntimeException exc) {
return new JpaResult<>(null, exc);
}
}
public static class JpaResult<T> {
final T result;
final RuntimeException exception;
private final T result;
private final RuntimeException exception;
public JpaResult(final T result, final RuntimeException exception) {
private JpaResult(final T result, final RuntimeException exception) {
this.result = result;
this.exception = exception;
}
static JpaResult<Void> forVoidValue() {
return new JpaResult<>(null, null);
}
public static <T> JpaResult<T> forValue(final T value) {
return new JpaResult<>(value, null);
}
public static <T> JpaResult<T> forException(final RuntimeException exception) {
return new JpaResult<>(null, exception);
}
public boolean wasSuccessful() {
return exception == null;
}