implements revoking role from user at repository level
This commit is contained in:
parent
c8e835f880
commit
2cb9375d03
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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; $$;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -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,18 +55,21 @@ class RbacGrantControllerAcceptanceTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
JpaAttempt jpaAttempt;
|
JpaAttempt jpaAttempt;
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
@Accepts({ "ROL:C(Create)" })
|
class GrantRoleToUser {
|
||||||
void packageAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() {
|
|
||||||
|
|
||||||
// given
|
@Test
|
||||||
final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
|
@Accepts({ "GRT:C(Create)" })
|
||||||
final String givenCurrentUserPackageAdmin = "aaa00@aaa.example.com";
|
void packageAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() {
|
||||||
final String givenAssumedRole = "package#aaa00.admin";
|
|
||||||
final var givenOwnPackageAdminRole = "package#aaa00.admin";
|
|
||||||
|
|
||||||
// when
|
// given
|
||||||
RestAssured // @formatter:off
|
final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@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()
|
.given()
|
||||||
.header("current-user", givenCurrentUserPackageAdmin)
|
.header("current-user", givenCurrentUserPackageAdmin)
|
||||||
.header("assumed-roles", givenAssumedRole)
|
.header("assumed-roles", givenAssumedRole)
|
||||||
@ -87,26 +91,26 @@ class RbacGrantControllerAcceptanceTest {
|
|||||||
.statusCode(201);
|
.statusCode(201);
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(findAllGrantsOfUser(givenCurrentUserPackageAdmin))
|
assertThat(findAllGrantsOfUser(givenCurrentUserPackageAdmin))
|
||||||
.extracting(RbacGrantEntity::toDisplay)
|
.extracting(RbacGrantEntity::toDisplay)
|
||||||
.contains("{ grant assumed role " + givenOwnPackageAdminRole +
|
.contains("{ grant assumed role " + givenOwnPackageAdminRole +
|
||||||
" to user " + givenNewUserName +
|
" to user " + givenNewUserName +
|
||||||
" by role " + givenAssumedRole + " }");
|
" by role " + givenAssumedRole + " }");
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
|
final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
|
||||||
final String givenCurrentUserPackageAdmin = "aaa00@aaa.example.com";
|
final String givenCurrentUserPackageAdmin = "aaa00@aaa.example.com";
|
||||||
final String givenAssumedRole = "package#aaa00.admin";
|
final String givenAssumedRole = "package#aaa00.admin";
|
||||||
final var givenAlienPackageAdminRole = "package#aab00.admin";
|
final var givenAlienPackageAdminRole = "package#aab00.admin";
|
||||||
|
|
||||||
// when
|
// when
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
.header("current-user", givenCurrentUserPackageAdmin)
|
.header("current-user", givenCurrentUserPackageAdmin)
|
||||||
.header("assumed-roles", givenAssumedRole)
|
.header("assumed-roles", givenAssumedRole)
|
||||||
@ -130,10 +134,11 @@ class RbacGrantControllerAcceptanceTest {
|
|||||||
.statusCode(403);
|
.statusCode(403);
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(findAllGrantsOfUser(givenCurrentUserPackageAdmin))
|
assertThat(findAllGrantsOfUser(givenCurrentUserPackageAdmin))
|
||||||
.extracting(RbacGrantEntity::getGranteeUserName)
|
.extracting(RbacGrantEntity::getGranteeUserName)
|
||||||
.doesNotContain(givenNewUserName);
|
.doesNotContain(givenNewUserName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<RbacGrantEntity> findAllGrantsOfUser(final String userName) {
|
List<RbacGrantEntity> findAllGrantsOfUser(final String userName) {
|
||||||
|
@ -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"));
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user