implements revoking role from user at repository level

This commit is contained in:
Michael Hoennig 2022-08-16 17:51:51 +02:00
parent c8e835f880
commit 2cb9375d03
6 changed files with 189 additions and 40 deletions

View File

@ -10,4 +10,5 @@ public interface RbacGrantRepository extends Repository<RbacGrantEntity, RbacGra
void save(final RbacGrantEntity grant); void save(final RbacGrantEntity grant);
void delete(final RbacGrantEntity grant);
} }

View File

@ -79,6 +79,7 @@ begin
raise exception '[403] Revoking role % is forbidden for %.', grantedRoleUuid, currentSubjects(); raise exception '[403] Revoking role % is forbidden for %.', grantedRoleUuid, currentSubjects();
end if; end if;
--raise exception 'isGranted(%, %)', currentSubjectIds(), grantedByRoleUuid;
if NOT isGranted(currentSubjectIds(), grantedByRoleUuid) then if NOT isGranted(currentSubjectIds(), grantedByRoleUuid) then
raise exception '[403] Revoking role granted by % is forbidden for %.', grantedByRoleUuid, currentSubjects(); raise exception '[403] Revoking role granted by % is forbidden for %.', grantedByRoleUuid, currentSubjects();
end if; end if;

View File

@ -99,8 +99,8 @@ create or replace function deleteRbacGrant()
returns trigger returns trigger
language plpgsql as $$ language plpgsql as $$
begin begin
call revokeRoleFromUser(assumedRoleUuid(), old.grantedRoleUuid, old.userUuid); call revokeRoleFromUser(old.grantedByRoleUuid, old.grantedRoleUuid, old.userUuid);
return null; return old;
end; $$; end; $$;
/* /*

View File

@ -11,6 +11,7 @@ import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserEntity;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository; import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository;
import net.hostsharing.test.JpaAttempt; import net.hostsharing.test.JpaAttempt;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
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;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
@ -54,8 +55,11 @@ class RbacGrantControllerAcceptanceTest {
@Autowired @Autowired
JpaAttempt jpaAttempt; JpaAttempt jpaAttempt;
@Nested
class GrantRoleToUser {
@Test @Test
@Accepts({ "ROL:C(Create)" }) @Accepts({ "GRT:C(Create)" })
void packageAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() { void packageAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() {
// given // given
@ -96,7 +100,7 @@ class RbacGrantControllerAcceptanceTest {
} }
@Test @Test
@Accepts({ "ROL:C(Create)", "ROL:X(Access Control)" }) @Accepts({ "GRT:C(Create)", "GRT:X(Access Control)" })
void packageAdmin_canNotGrantAlienPackageAdminRole_toArbitraryUser() { void packageAdmin_canNotGrantAlienPackageAdminRole_toArbitraryUser() {
// given // given
@ -135,6 +139,7 @@ class RbacGrantControllerAcceptanceTest {
.extracting(RbacGrantEntity::getGranteeUserName) .extracting(RbacGrantEntity::getGranteeUserName)
.doesNotContain(givenNewUserName); .doesNotContain(givenNewUserName);
} }
}
List<RbacGrantEntity> findAllGrantsOfUser(final String userName) { List<RbacGrantEntity> findAllGrantsOfUser(final String userName) {
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {

View File

@ -17,11 +17,13 @@ import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import static net.hostsharing.test.JpaAttempt.attempt; import static net.hostsharing.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
@DataJpaTest @DataJpaTest
@ComponentScan(basePackageClasses = { RbacGrantRepository.class, Context.class, JpaAttempt.class }) @ComponentScan(basePackageClasses = { RbacGrantRepository.class, Context.class, JpaAttempt.class })
@ -48,7 +50,7 @@ class RbacGrantRepositoryIntegrationTest {
JpaAttempt jpaAttempt; JpaAttempt jpaAttempt;
@Nested @Nested
class FindAllRbacGrants { class FindAllGrantsOfUser {
@Test @Test
@Accepts({ "GRT:L(List)" }) @Accepts({ "GRT:L(List)" })
@ -101,10 +103,9 @@ class RbacGrantRepositoryIntegrationTest {
} }
@Nested @Nested
class CreateRbacGrant { class GrantRoleToUser {
@Test @Test
@Accepts({ "GRT:C(Create)" })
public void customerAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() { public void customerAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() {
// given // given
currentUser("admin@aaa.example.com"); currentUser("admin@aaa.example.com");
@ -129,16 +130,17 @@ class RbacGrantRepositoryIntegrationTest {
} }
@Test @Test
@Accepts({ "GRT:C(Create)" })
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
public void packageAdmin_canNotGrantPackageOwnerRole() { public void packageAdmin_canNotGrantPackageOwnerRole() {
// given // given
record Given(RbacUserEntity arbitraryUser, UUID packageOwnerRoleUuid) {} record Given(RbacUserEntity arbitraryUser, UUID packageOwnerRoleUuid) {
}
final var given = jpaAttempt.transacted(() -> { final var given = jpaAttempt.transacted(() -> {
// to find the uuids of we need to have access rights to these // to find the uuids of we need to have access rights to these
currentUser("admin@aaa.example.com"); currentUser("admin@aaa.example.com");
return new Given( return new Given(
createNewUser(), // eigene Transaktion? createNewUser(),
rbacRoleRepository.findByRoleName("package#aaa00.owner").getUuid() rbacRoleRepository.findByRoleName("package#aaa00.owner").getUuid()
); );
}).returnedValue(); }).returnedValue();
@ -162,15 +164,144 @@ class RbacGrantRepositoryIntegrationTest {
"ERROR: [403] Access to granted role " + given.packageOwnerRoleUuid "ERROR: [403] Access to granted role " + given.packageOwnerRoleUuid
+ " forbidden for {package#aaa00.admin}"); + " forbidden for {package#aaa00.admin}");
jpaAttempt.transacted(() -> { jpaAttempt.transacted(() -> {
// finally, we use the new user to make sure, no roles were granted
currentUser(given.arbitraryUser.getName()); currentUser(given.arbitraryUser.getName());
assertThat(rbacGrantRepository.findAll()) assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::toDisplay) .extracting(RbacGrantEntity::toDisplay)
.hasSize(0); .hasSize(0);
// "{ grant assumed role package#aaa00.admin to user aac00@aac.example.com by role customer#aaa.admin }");
}); });
} }
} }
@Nested
class RevokeRoleFromUser {
@Test
public void customerAdmin_canRevokeSelfGrantedPackageAdminRole() {
// given
final var grant = create(grant()
.byUser("admin@aaa.example.com").withAssumedRole("customer#aaa.admin")
.grantingRole("package#aaa00.admin").toUser("aac00@aac.example.com"));
// when
currentUser("admin@aaa.example.com");
assumedRoles("customer#aaa.admin");
final var revokeAttempt = attempt(em, () -> {
rbacGrantRepository.delete(grant);
});
// then
currentUser("admin@aaa.example.com");
assumedRoles("customer#aaa.admin");
assertThat(revokeAttempt.caughtExceptionsRootCause()).isNull();
assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::getGranteeUserName)
.doesNotContain("aac00@aac.example.com");
}
@Test
public void packageAdmin_canRevokeOwnPackageAdminRoleGrantedByAnotherAdminOfThatPackage() {
// given
final var grant = create(grant()
.byUser("admin@aaa.example.com").withAssumedRole("package#aaa00.admin")
.grantingRole("package#aaa00.admin").toUser(createNewUser().getName()));
// when
currentUser("aaa00@aaa.example.com");
assumedRoles("package#aaa00.admin");
final var revokeAttempt = attempt(em, () -> {
rbacGrantRepository.delete(grant);
});
// then
assertThat(revokeAttempt.caughtExceptionsRootCause()).isNull();
currentUser("admin@aaa.example.com");
assumedRoles("customer#aaa.admin");
assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::getGranteeUserName)
.doesNotContain("aac00@aac.example.com");
}
@Test
public void packageAdmin_canNotRevokeOwnPackageAdminRoleGrantedByOwnerRoleOfThatPackage() {
// given
final var grant = create(grant()
.byUser("admin@aaa.example.com").withAssumedRole("package#aaa00.owner")
.grantingRole("package#aaa00.admin").toUser("aac00@aac.example.com"));
final var grantedByRole = rbacRoleRepository.findByRoleName("package#aaa00.owner");
// when
currentUser("aaa00@aaa.example.com");
assumedRoles("package#aaa00.admin");
final var revokeAttempt = attempt(em, () -> {
rbacGrantRepository.delete(grant);
});
// then
revokeAttempt.assertExceptionWithRootCauseMessage(
PersistenceException.class,
"ERROR: [403] Revoking role created by %s is forbidden for {package#aaa00.admin}." .formatted(
grantedByRole.getUuid()
));
}
private RbacGrantEntity create(GrantBuilder with) {
currentUser(with.byUserName);
assumedRoles(with.assumedRole);
final var givenArbitraryUserUuid = rbacUserRepository.findUuidByName(with.granteeUserName);
final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName(with.grantedRole).getUuid();
final var grant = RbacGrantEntity.builder()
.granteeUserUuid(givenArbitraryUserUuid).grantedRoleUuid(givenOwnPackageRoleUuid)
.assumed(true)
.build();
final var grantAttempt = attempt(em, () ->
rbacGrantRepository.save(grant)
);
assumeThat(grantAttempt.caughtException()).isNull();
assumeThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::toDisplay)
.contains("{ grant assumed role %s to user %s by role %s }" .formatted(
with.grantedRole, with.granteeUserName, with.assumedRole
));
return grant;
}
private GrantBuilder grant() {
return new GrantBuilder();
}
static class GrantBuilder {
String byUserName;
String assumedRole = "";
String grantedRole;
String granteeUserName;
GrantBuilder byUser(final String userName) {
byUserName = userName;
return this;
}
GrantBuilder withAssumedRole(final String assumedRole) {
this.assumedRole = assumedRole != null ? assumedRole : "";
return this;
}
GrantBuilder grantingRole(final String grantingRole) {
this.grantedRole = grantingRole;
return this;
}
GrantBuilder toUser(final String toUser) {
this.granteeUserName = toUser;
return this;
}
}
}
private RbacUserEntity createNewUser() { private RbacUserEntity createNewUser() {
return rbacUserRepository.create( return rbacUserRepository.create(
new RbacUserEntity(null, "test-user-" + System.currentTimeMillis() + "@example.com")); new RbacUserEntity(null, "test-user-" + System.currentTimeMillis() + "@example.com"));

View File

@ -4,6 +4,7 @@ import junit.framework.AssertionFailedError;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NestedExceptionUtils; import org.springframework.core.NestedExceptionUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
@ -11,6 +12,7 @@ import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
/** /**
* Wraps the 'when' part of a DataJpaTest to improve readability of tests. * Wraps the 'when' part of a DataJpaTest to improve readability of tests.
@ -62,6 +64,7 @@ public class JpaAttempt {
public JpaResult<Void> transacted(final Runnable code) { public JpaResult<Void> transacted(final Runnable code) {
try { try {
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.execute(transactionStatus -> { transactionTemplate.execute(transactionStatus -> {
code.run(); code.run();
return null; return null;
@ -115,6 +118,10 @@ public class JpaAttempt {
throw new AssertionFailedError("expected " + expectedExceptionClass + " but got " + exception); throw new AssertionFailedError("expected " + expectedExceptionClass + " but got " + exception);
} }
public Throwable caughtExceptionsRootCause() {
return exception == null ? null : NestedExceptionUtils.getRootCause(exception);
}
public void assertExceptionWithRootCauseMessage( public void assertExceptionWithRootCauseMessage(
final Class<? extends RuntimeException> expectedExceptionClass, final Class<? extends RuntimeException> expectedExceptionClass,
final String... expectedRootCauseMessages) { final String... expectedRootCauseMessages) {
@ -125,6 +132,10 @@ public class JpaAttempt {
} }
} }
public void assertSuccessful() {
assertThat(exception).isNull();;
}
private String firstRootCauseMessageLineOf(final RuntimeException exception) { private String firstRootCauseMessageLineOf(final RuntimeException exception) {
final var rootCause = NestedExceptionUtils.getRootCause(exception); final var rootCause = NestedExceptionUtils.getRootCause(exception);
return Optional.ofNullable(rootCause) return Optional.ofNullable(rootCause)