implements delete rbacuser

This commit is contained in:
Michael Hoennig 2022-09-01 16:57:16 +02:00
parent a06feff42e
commit df48bfc0da
8 changed files with 208 additions and 29 deletions

View File

@ -45,6 +45,20 @@ public class RbacUserController implements RbacusersApi {
return ResponseEntity.created(uri).body(map(saved, RbacUserResource.class));
}
@Override
@Transactional
public ResponseEntity<Void> deleteUserByUuid(
final String currentUser,
final String assumedRoles,
final UUID userUuid
) {
context.define(currentUser, assumedRoles);
rbacUserRepository.deleteByUuid(userUuid);
return ResponseEntity.noContent().build();
}
@Override
@Transactional(readOnly = true)
public ResponseEntity<RbacUserResource> getUserById(

View File

@ -41,4 +41,6 @@ public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> {
insert(rbacUserEntity);
return rbacUserEntity;
}
void deleteByUuid(UUID userUuid);
}

View File

@ -17,7 +17,7 @@ paths:
$ref: "./api-definition/rbac-users-with-id-permissions.yaml"
/api/rbac-users/{userUuid}:
$ref: "./api-definition/rbac-users-with-id.yaml"
$ref: "./api-definition/rbac-users-with-uuid.yaml"
/api/rbac-roles:
$ref: "./api-definition/rbac-roles.yaml"

View File

@ -24,3 +24,28 @@ get:
$ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './api-definition/error-responses.yaml#/components/responses/Forbidden'
delete:
tags:
- rbacusers
operationId: deleteUserByUuid
parameters:
- $ref: './api-definition/auth.yaml#/components/parameters/currentUser'
- $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles'
- name: userUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the user to delete.
responses:
"204":
description: No Content
"401":
$ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './api-definition/error-responses.yaml#/components/responses/Forbidden'
"404":
$ref: './api-definition/error-responses.yaml#/components/responses/NotFound'

View File

@ -209,7 +209,9 @@ create or replace view RbacUser_rv as
union
select users.*
from RbacUser as users
where cardinality(assumedRoles()) = 0 and currentUserUuid() = users.uuid
where cardinality(assumedRoles()) = 0 and
(currentUserUuid() = users.uuid or hasGlobalRoleGranted(currentUserUuid()))
) as unordered
-- @formatter:on
order by unordered.name;
@ -250,7 +252,35 @@ create trigger insertRbacUser_Trigger
on RbacUser_rv
for each row
execute function insertRbacUser();
--//
-- ============================================================================
--changeset rbac-views-USER-RV-DELETE-TRIGGER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Instead of delete trigger function for RbacUser_RV.
*/
create or replace function deleteRbacUser()
returns trigger
language plpgsql as $$
begin
if currentUserUuid() = old.uuid or hasGlobalRoleGranted(currentUserUuid()) then
delete from RbacUser where uuid = old.uuid;
return old;
end if;
raise exception '[403] User % not allowed to delete user uuid %', currentUser(), old.uuid;
end; $$;
/*
Creates an instead of delete trigger for the RbacUser_rv view.
*/
create trigger deleteRbacUser_Trigger
instead of delete
on RbacUser_rv
for each row
execute function deleteRbacUser();
--/
-- ============================================================================
--changeset rbac-views-OWN-GRANTED-PERMISSIONS-VIEW:1 endDelimiter:--//

View File

@ -397,10 +397,92 @@ class RbacUserControllerAcceptanceTest {
}
}
@Nested
class DeleteRbacUser {
@Test
@Accepts({ "USR:D(Create)" })
void anybody_canDeleteTheirOwnUser() {
// given
final var givenUser = givenANewUser();
// @formatter:off
final var location = RestAssured
.given()
.header("current-user", givenUser.getName())
.port(port)
.when()
.delete("http://localhost/api/rbac-users/" + givenUser.getUuid())
.then().log().all().assertThat()
.statusCode(204);
// @formatter:on
// finally, the user is actually deleted
assertThat(rbacUserRepository.findByName(givenUser.getName())).isNull();
}
@Test
@Accepts({ "USR:D(Create)", "USR:X(Access Control)" })
void customerAdmin_canNotDeleteOtherUser() {
// given
final var givenUser = givenANewUser();
// @formatter:off
final var location = RestAssured
.given()
.header("current-user", "customer-admin@xxx.example.com")
.port(port)
.when()
.delete("http://localhost/api/rbac-users/" + givenUser.getUuid())
.then().log().all().assertThat()
// that user cannot even see other users, thus the system won't even try to delete
.statusCode(204);
// @formatter:on
// finally, the user is still there
assertThat(rbacUserRepository.findByName(givenUser.getName())).isNotNull();
}
@Test
@Accepts({ "USR:D(Create)", "USR:X(Access Control)" })
void globalAdmin_canDeleteArbitraryUser() {
// given
final var givenUser = givenANewUser();
// @formatter:off
final var location = RestAssured
.given()
.header("current-user", "mike@example.org")
.port(port)
.when()
.delete("http://localhost/api/rbac-users/" + givenUser.getUuid())
.then().log().all().assertThat()
.statusCode(204);
// @formatter:on
// finally, the user is actually deleted
assertThat(rbacUserRepository.findByName(givenUser.getName())).isNull();
}
}
RbacUserEntity findRbacUserByName(final String userName) {
return jpaAttempt.transacted(() -> {
context.define("mike@example.org");
return rbacUserRepository.findByName(userName);
}).returnedValue();
}
RbacUserEntity givenANewUser() {
final var givenUserName = "test-user-" + System.currentTimeMillis() + "@example.com";
final var givenUser = jpaAttempt.transacted(() -> {
context.define(null);
return rbacUserRepository.create(new RbacUserEntity(UUID.randomUUID(), givenUserName));
}).assumeSuccessful().returnedValue();
assertThat(rbacUserRepository.findByName(givenUser.getName())).isNotNull();
return givenUser;
}
}

View File

@ -43,28 +43,9 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest {
@Nested
class CreateUser {
@Test
public void anyoneCanCreateTheirOwnUser() {
// given
final var givenNewUserName = "test-user-" + System.currentTimeMillis() + "@example.com";
context(null);
// when
final var result = rbacUserRepository.create(
new RbacUserEntity(null, givenNewUserName));
// then the persisted user is returned
assertThat(result).isNotNull().extracting(RbacUserEntity::getName).isEqualTo(givenNewUserName);
// and the new user entity can be fetched by the user itself
context(givenNewUserName);
assertThat(em.find(RbacUserEntity.class, result.getUuid()))
.isNotNull().extracting(RbacUserEntity::getName).isEqualTo(givenNewUserName);
}
@Test
@Transactional(propagation = Propagation.NEVER)
void anyoneCanCreateTheirOwnUser_committed() {
void anyoneCanCreateTheirOwnUser() {
// given:
final var givenUuid = UUID.randomUUID();
@ -72,7 +53,7 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest {
// when:
final var result = jpaAttempt.transacted(() -> {
context("customer-admin@xxx.example.com");
context(null);
return rbacUserRepository.create(new RbacUserEntity(givenUuid, newUserName));
});
@ -80,11 +61,36 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest {
assertThat(result.wasSuccessful()).isTrue();
assertThat(result.returnedValue()).isNotNull()
.extracting(RbacUserEntity::getUuid).isEqualTo(givenUuid);
jpaAttempt.transacted(() -> {
context(newUserName);
assertThat(em.find(RbacUserEntity.class, givenUuid))
.isNotNull().extracting(RbacUserEntity::getName).isEqualTo(newUserName);
assertThat(rbacUserRepository.findByName(result.returnedValue().getName())).isNotNull();
// jpaAttempt.transacted(() -> {
// context(givenUser.getName());
// assertThat(em.find(RbacUserEntity.class, givenUser.getUuid()))
// .isNotNull().extracting(RbacUserEntity::getName).isEqualTo(givenUser.getName());
// }).assertSuccessful();
}
}
@Nested
class DeleteUser {
@Test
@Transactional(propagation = Propagation.NEVER)
public void anyoneCanDeleteTheirOwnUser() {
// given
final RbacUserEntity givenUser = givenANewUser();
// when
final var result = jpaAttempt.transacted(() -> {
context(givenUser.getName());
rbacUserRepository.deleteByUuid(givenUser.getUuid());
});
// then the user is deleted
result.assertSuccessful();
assertThat(rbacUserRepository.findByName(givenUser.getName())).isNull();
// jpaAttempt.transacted(() -> {
// assertThat(rbacUserRepository.findByName(givenUser.getName())).isNull();
// }).assertSuccessful();
}
}
@ -392,6 +398,16 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest {
return rbacUserRepository.findByName(userName).getUuid();
}
RbacUserEntity givenANewUser() {
final var givenUserName = "test-user-" + System.currentTimeMillis() + "@example.com";
final var givenUser = jpaAttempt.transacted(() -> {
context(null);
return rbacUserRepository.create(new RbacUserEntity(UUID.randomUUID(), givenUserName));
}).assumeSuccessful().returnedValue();
assertThat(rbacUserRepository.findByName(givenUser.getName())).isNotNull();
return givenUser;
}
void exactlyTheseRbacUsersAreReturned(final List<RbacUserEntity> actualResult, final String... expectedUserNames) {
assertThat(actualResult)
.extracting(RbacUserEntity::getName)

View File

@ -11,6 +11,7 @@ import java.util.Optional;
import java.util.function.Supplier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
/**
* Wraps the 'when' part of a DataJpaTest to improve readability of tests.
@ -53,6 +54,7 @@ public class JpaAttempt {
public <T> JpaResult<T> transacted(final Supplier<T> code) {
try {
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
return JpaResult.forValue(
transactionTemplate.execute(transactionStatus -> code.get()));
} catch (final RuntimeException exc) {
@ -131,11 +133,19 @@ public class JpaAttempt {
}
public JpaResult<T> assumeSuccessful() {
assertThat(exception).isNull();
;
assumeThat(exception).as(getSensibleMessage(exception)).isNull();
return this;
}
public JpaResult<T> assertSuccessful() {
assertThat(exception).as(getSensibleMessage(exception)).isNull();
return this;
}
private String getSensibleMessage(final RuntimeException exception) {
return exception != null ? NestedExceptionUtils.getRootCause(exception).getMessage() : null;
}
private String firstRootCauseMessageLineOf(final RuntimeException exception) {
final var rootCause = NestedExceptionUtils.getRootCause(exception);
return Optional.ofNullable(rootCause)